前言
當初寫台鐵時刻表的後端時,資料快取這塊我是直接將資料儲存在變數裡,並自行做 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 是如此簡單好用又強大,前端、後端和快取管理完全分離,非常清爽。