Redis 做資料快取的基本使用 (搭配Node.js)

Redis 做資料快取的基本使用 (搭配Node.js)

背景

有些需要耗費大量運算的結果
如果能夠在伺服器端做快取
可以對效能提升很有幫助
先看這次的結果

左邊為原始情況,右邊為 Redis 快取後
(極端案例? T__T

安裝

已經有安裝
或是只是想看介紹的可以跳過這部分喔~

Redis Server

建議採用官網這種下載壓縮檔再安裝的方式
不然直接用 apt-get 常常預設的不是最新版本
https://redis.io/download#installation

$ wget http://download.redis.io/releases/redis-6.0.5.tar.gz
$ tar xzf redis-6.0.5.tar.gz
$ cd redis-6.0.5
$ make

在背景啟動 redis-server

$ src/redis-server --daemonize yes

並且測試一下功能運作正常

$ redis> set foo bar
$ OK
$ redis> get foo
$ "bar"

Redis Client

我自己是使用 Node.js
所以先安裝 node-redis 套件
https://github.com/NodeRedis/node-redis

npm install redis

Github 上的範例也很簡潔清楚

<!-- 引入模組 -->
const redis = require("redis");
const client = redis.createClient();
<!-- 連接 redis server -->
client.on("error", function(error) {
  console.error(error);
});
<!-- 設置與取得 key / value -->
client.set("key", "value", redis.print);
client.get("key", redis.print);

實際使用

使用快取的邏輯

  • 查詢資料時,查看是否存在於快取之中
    • 是,則從快取中取得資料
      • 直接回傳給前端
    • 否,則從資料庫中查詢
      • 回傳給前端
      • 並且寫入快取之中
  • 更新資料時,查看是否存在於快取之中
    • 是,則一併更新快取資料
    • 否,則寫入快取中

      轉換為程式碼

// 取得圖表資料
router.get('/someData', (req, res) => {
  // 取得請求參數並傳換成 key
  const parm1 = req.query.parm1
  const parm2 = req.query.parm2
  const parm3 = req.query.parm3
  const key = (`someData:${parm1}/${parm2}/${parm3}`).trim()
  const expireDay = 60 * 60 * 24;
  // 檢查資料是否能夠從快取中取得
  redisClient.get(key, (err, rawdata) => {
    // 可以做錯誤處理
    if (err) {
      console.log(err)
    }
    // 如果快取中沒有這筆資料,會回傳 null 
    if (!!rawdata) {
      // redis 無法儲存 Javascript 的 Object
      // 這邊是直接存成 string
      // 所以需要轉換
      const data = JSON.parse(rawdata);
      return res.json(data);
    }
    // 這邊是呼叫你撈資料庫的 function
    yourSQL.queryData(parm1, parm2, parm3)
      .then(results => {
        // 寫入快取,並且設置有效期限為一天
        redisClient.set(key, JSON.stringify(results), 'EX', expireDay, err => {
          if (err) {
            console.log(err)
          }
        });
        // 回傳資料
        res.json(results);
      })
      .catch(err => {
        // 資料庫的查詢的錯誤處理
        console.log("not connected due to error: " + err);
        res.status(500).send('Can not connect to DB');
      });
  });
});

值得注意的地方

key 的取名

因為 key 的長度會影響到 Redis 查詢的效能
所以設計好的 key 是需要的
關乎取名的東西要複雜都可以很複雜
但我覺得最大原則就是內部溝通好、好管理就好
可以參考 SatckOverflow 上有人講到常見的命名方式
redis-key-naming-conventions

We use a colon (:) as namespace separator and a hash (#) for id-parts of keys, e.g.:
logistics:building#23

value 的資料型態

Redis 目前有五種資料型態:

  • string
  • hash
  • list
  • set
  • zset (sorted set)

我這邊原本是想直接把資料庫撈出來的結果 (Object型態) 存入
但是因為 Redis 並沒有這種資料型態
所以就直接粗暴的轉成字串
存入與取出時透過JSON.stringify, JSON.parse 來處理

應該有更好的做法啦

有效期限的設置

  • 如果不是需要常駐的資料
    盡量都要設置有效期限
    (需要常駐的資料也應該要記得存於實體資料庫中)
  • 設置合理的有效期限
    • 太長,占用記憶體空間
    • 太短,容易重覆讀寫浪費效能
  • 在範例的 Node.js 中
    語法為在 set 方法裡傳入參數 'EX', expire
      redisClient.set(key, value, 'EX', expire, err => {
          ...
      });

超過記憶體上限後的處理、淘汰機制

  • noeviction:
    即使記憶體上達到上限,也不置換 key-value
    也就是記憶體滿了之後,只能讀取資料,不能寫入
    若再新增資料會 return error

    這是預設值,我認為不適用於多數場合,所應該要去設定成下列其他的

  • allkeys-lru:
    優先刪除掉最近最少使用的key,用以保存新數據

    LRU = least recently used
    適用多數場合

  • volatile-lru:
    只從有設置有效期限 (expire) 的資料中
    選擇最近最少使用的 key-value 進行刪除

    與 allkeys-lru 的差別在於
    一個從全部的 key 中來選擇刪除
    一個是只會從有有效期限的 key 來選擇刪除

  • allkeys-random:
    全部的 key 中
    隨機選擇一些進行刪除
  • volatile-random:
    只從有設置有效期限 (expire) key 中
    隨機選擇一些 key 進行刪除
  • volatile-ttl:
    只從有設置有效期限 (expire) key 中
    選出剩餘存活時間 (TTL) 最短的 key 進行刪除

    TTL = Time To Live

其他參考資料