【筆記】Node 應用使用 Redis 進行快取管理

redis logo

前言

當初寫台鐵時刻表的後端時,資料快取這塊我是直接將資料儲存在變數裡,並自行做 TTL(Time To Live,存活時間)的驗證和持久化(Persistence)的機制。但用了一年後,我發現直接使用 redis 還是會比自造輪子來的方便些。本文將簡單記錄我的一些快取心得和 redis 的設定方式。

為何一開始要自造輪子

一開始我只是覺得伺服器記憶體已經很吃緊了,而我只是想要加入簡單的快取功能,心想導入一套完整的快取解決方案,怕是殺雞焉用牛刀;加上我也滿想自己去理解和嘗試快取機制,若只是將資料儲存在變數裡做快取,做個簡單的 TTL 和持久化應該不會花太久時間,所以就決定自造輪子。

自造輪子的困難

快取資料的過期驗證並不會太困難,資料持久化倒是麻煩了一點,但最困難的我覺得還是「驅逐策略」:到底要釋放哪些快取資料?是否得在儲存資料時多加上權重值?JS 有什麼辦法可以即時釋放記憶體?要自己去控管這些是真的挺麻煩的。

這一年來,我的快取策略雖然有些問題但還算勘用;但因為我這樣有缺陷的機制,有時候付費 API 就會超過用量上限,導致我得多付一倍的錢。而且一直看著應用一直因為記憶體不足而崩潰重啟也不是辦法,於是我決定向專業的記憶體快取解決方案 redis 靠攏。

redis 優勢

node.js 的記憶體是使用 V8 引擎管理,並未針對大型資料進行特別優化;redis 將資料儲存在自己的記憶體中,採用針對不同資料類型的壓縮和高效存儲格式。而且 redis 有完整的 TTL、持久化、資料壓縮甚至是分散式資料管理。所以在相同資料量的情況下,redis 是很有可能比 node.js 占用更少的記憶體的。

以下簡單記錄 redis 的安裝與使用方式。

安裝 redis-server

macOS: brew install redis
Ubuntu: sudo apt update && sudo apt install redis-server

啟動 redis-server

redis-server

限制 redis-server 最大記憶體使用以及驅逐策略

這是非常重要的步驟。

可以到 redis 設定檔中,調整 maxmemory 和 maxmemory-policy 這兩個欄位。

sudo vim /etc/redis/redis.conf

# 依你的記憶體大小決定
maxmemory 256mb

# 建議使用 allkeys-lru 策略
maxmemory-policy allkeys-lru

完整趨逐策略如下:

  • noeviction:當記憶體不足時,不再接受寫入請求。這是預設策略
  • allkeys-lru:從所有鍵中驅逐最近最少使用的鍵。建議使用
  • volatile-lru:從設置了 TTL 的鍵中驅逐最近最少使用的鍵。
  • allkeys-random:從所有鍵中隨機驅逐。
  • volatile-random:從設置了 TTL 的鍵中隨機驅逐。
  • volatile-ttl:從設置了 TTL 的鍵中驅逐即將過期的鍵(TTL 最短的優先)。

重啟 redis-server

改完 redis.conf 後務必重新啟動 redis。

sudo systemctl restart redis

確認 redis-server 是否已在運行

sudo systemctl status redis

檢查 redis-server 記憶體使用狀況

redis-cli info memory

查看 redis-server 的快取內容

# 列出所有符合模式的鍵(* 可自行調整)
redis-cli KEYS *

# 列出 100 個符合模式的鍵(推薦使用,* 和 100 可自行調整)
redis-cli SCAN 0 MATCH "*" COUNT 100

# 查看特定鍵
redis-cli GET key_name

在 node.js 應用中使用 redis-client

安裝 node-redis

npm install redis

使用 redis-client

以下使用 promise 示範。

import { createClient } from "redis";

// 建立 redis 物件
const client = createClient();
// 連接 redis
await client.connect();

const getData = async () => {
    const cacheKey = "cacheKey";
    let result;

    // 從 redis 取得快取
    const cachedData = await client.get(cacheKey);

    // 若沒有快取
    if (!cachedData) {
        // 從原先的方法取得資料(API、Database...)
        const response = await getDataFromAPI();

        // 將剛取回來的資料作為結果
        result = response;

        // 並將資料轉成字串後快取
        await client.set(cacheKey, JSON.stringify(response), {
            EX: 60 * 60 * 24, // 設定 EX 過期時間(單位:秒)
        });
    } else {
        // 若有快取,將快取轉回 JSON 後直接作為結果
        result = JSON.parse(cachedData);
    }

    return result;
}

// 在應用程式關閉時關閉與 redis 的連線
process.on("SIGINT", async () => {
    await client.quit();
    process.exit();
});

後記

在自造輪子後,才會知道 redis 是如此簡單好用又強大,前端、後端和快取管理完全分離,非常清爽。

0 0 votes
Article Rating
Subscribe
Notify of
guest
0 Comments
Inline Feedbacks
View all comments