---
# System prepended metadata

title: Data Caching with Redis
tags: [JavaScript, 'Node JS: Advanced Concepts', NodeJS]

---

# 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`
