# Data Caching with Redis
### Get Start
基本的 redis 指令如下:
```javascript
const redis = require('redis')
const redisUrl = 'redis://127.0.0.1:6379'
const client = redis.creatClient(redisUrl)
client.set('colors', { red: 'rojo' }) // 錯誤示範 - 不要用 set 存非純值,會被強制轉成非你預期的字串
client.get('colors', console.log) // null '[object Object]'
client.set('colors', JSON.stringify{ red: 'rojo' })
client.get('colors', console.log) // null '{"red":"rojo"}'
client.hset('spanish', 'red', 'rojo')
client.hget('spanish', 'red', (err, val) => console.log(val)) // rojo
client.hset('german', 'red', 'rot')
client.get('german', 'red', console.log) // null 'rot'
client.flushall() // 清除全部資料
```
### Cache Solution
實際上在設計快取時需要考慮諸多情境:
1. Q: 如何通用地設定快取?
A: 以**裝飾者模式**劫持 ORM
2. Q: 如何避免快取無限膨脹?
A: 設定**快取時效**
3. Q: 如何設定快取的 Key ?
A: 用 **SQL query** 或一些關鍵唯一的資訊 hash
4. Q: 哪些請求需要快取? 哪些不用?
A: 主要來說更動頻率不高的 GET 最優先,其他不可逆操作如 POST、PATCH 等則不用。
### Practice
定義快取服務,主要功能有四個:
1. 自定義請求是否需被快取
2. 利用 user id 和 query key 當 hash 作簡易的快取
3. 具有時效性的快取
4. 清除對應 user 之快取
```javascript
// services/cache.js
const mongoose = require('mongoose')
const redis = require('redis')
const util = require('util')
const redisUrl = 'redis://127.0.0.1:6379'
const client = redis.createClient(redisUrl)
client.hget = util.promisify(client.hget)
// 透過裝飾者模式劫持 ORM
const exec = mongoose.Query.prototype.exec
// 用 function chaining 和物件變數設定請求是否需要被快取
mongoose.Query.prototype.cache = function (options = {}) {
this.useCache = true
this.hashKey = JSON.stringify(options.key || '')
return this
}
mongoose.Query.prototype.exec = function () {
if (!this.useCache) {
return exec.apply(this, arguments)
}
// 用 query 參數跟 collection 作快取 key
const key = JSON.stringify(
Object.assign({}, this.getQuery(), {
collection: this.mongooseCollection.name,
})
)
const cacheValue = await client.hget(this.hashKey, key)
if (cacheValue) {
const doc = JSON.parse(cacheValue)
// 如果 response 太 nested 可以考慮用遞迴遍歷處理
return Array.isArray(doc)
? doc.map((d) => new this.model(d))
: new this.model(doc)
}
const result = await exec.apply(this, arguments)
client.hset(this.hashKey, key, JSON.stringify(result), 'EX', 10)
return result
}
module.exports = {
clearHash(hashKey) {
client.del(JSON.stringify(hashKey))
},
}
```
並設計一個 middleware 方面提供給 POST、PATCH、DELETE 等 API 完成後執行,<br/>
確保使用者下次拿 GET 不會拿到過時的資料。
```javascript
// middlewares/cleanCache
const { clearHash } = require('../services/cache')
module.exports = async (req, res, next) => {
await next()
clearHash(req.user.id)
}
```
API 使用快取和清除快取的範例:
```javascript
// routes/blogRoutes.js
const mongoose = require('mongoose')
const requireLogin = require('../middlewares/requireLogin')
const { cleanHash } = require('../middlewares/cleanCache')
const Blog = mongoose.model('Blog')
module.exports = (app) => {
app.get('/api/blogs', requireLogin, async (req, res) => {
const blogs = await Blog.find({ _user: req.user.id }).cache({
key: req.user.id,
})
res.send(blogs)
})
app.post('/api/blogs', requireLogin, cleanCache, async (req, res) => {
const { title, content } = req.body
const blog = new Blog({
title,
content,
_user: req.user.id,
})
try {
await blog.save()
res.send(blog)
} catch (err) {
res.send(400, err)
}
})
}
```
###### tags: `Node JS: Advanced Concepts` `NodeJS` `JavaScript`