當大量的請求進來時,快取可以用來降低資料庫的負擔。Moleculer 提供了一個內建的快取解決方案用來快取 Actions 的響應,可以在 ServiceBroker
的選項中設定 cacher
的類型,並在 Actions 中設置 cache: true
以啟用快取。
範例:Actions 快取
const { ServiceBroker } = require("moleculer");
// 建立 Broker
const broker = new ServiceBroker({
cacher: "Memory"
});
// 建立服務
broker.createService({
name: "users",
actions: {
list: {
// 在 Action 啟用快取
cache: true,
handler(ctx) {
this.logger.info("Handler called!");
return [
{ id: 1, name: "John" },
{ id: 2, name: "Jane" }
];
}
}
}
});
// 測試呼叫 Action
broker.start()
.then(() => {
// 第一次呼叫時快取是空的,會執行 Action 處理程序
return broker.call("users.list").then(
res => broker.logger.info("Users count:", res.length)
);
})
.then(() => {
// 第二次呼叫時快取有值,不會執行 Action 處理程序
return broker.call("users.list").then(
res => broker.logger.info("Users count from cache:", res.length)
);
});
主控台訊息:
[2022-09-17T23:19:05.010Z] INFO workboxy-32400/BROKER: ✔ ServiceBroker with 2 service(s) started successfully in 15ms.
[2022-09-17T23:19:05.012Z] INFO workboxy-32400/USERS: Handler called!
[2022-09-17T23:19:05.012Z] INFO workboxy-32400/BROKER: Users count: 2
[2022-09-17T23:19:05.014Z] INFO workboxy-32400/BROKER: Users count from cache: 2
你可以發現
Handler called!
只出現一次,因為第二次請求的響應是由快取中回傳的。
快取會根據服務名稱、 Action 名稱及參數的 context 來定義鍵值。
格式:
<服務名稱>.<Action 名稱>:<參數或參數的 Hash>
如果你呼叫 posts.list
並給定參數 { limit: 5, offset: 20 }
,這時候快取會根據參數來計算 Hash 值。當下次使用同樣的參數來呼叫同個動作時,快取就會依相同的鍵值來找到該結果。
範例:
posts.list:limit|5|offset|20
由於參數可能夾帶一些與快取無關的屬性,過長的鍵值也可能導致效能問題。因此建議設定好快取的參數屬性,可以有效縮短鍵值並移除無關的屬性。若需要在快取中使用 meta 資訊,鍵的名稱請使用
#
前綴。如果參數值未定義,則會以undefined
記錄在鍵值中。
範例:
如果參數為 { limit: 10, offset: 30 }
且 meta 為 { user: { id: 123 } }
,將得到快取鍵值 posts.list:10|30|123
。
module.exports = {
name: "posts",
actions: {
list: {
cache: {
//由 "limit" 、 "offset" 參數及 "user.id" meta 資訊產生鍵值
keys: ["limit", "offset", "#user.id"]
},
handler(ctx) {
return this.getList(ctx.params.limit, ctx.params.offset);
}
}
}
};
這個解決方案非常的快,官方推薦在生產環境中使用它。
某些情況下,有效的鍵值可能很長,這可能會導致效能問題。為了避免這種情況,請使用 maxParamsLength
選項來限制鍵值的最大長度。當鍵值長度大於設定的最大長度值時,快取由原始的鍵值計算一個 Hash 值(SHA256) ,然後添加到被裁切的鍵值末端。
maxParamsLength
的最小值為 44 (Base64 的 SHA256 長度),若要關閉此功能,請設為 0 或 null 。
範例:未限制長度的情況
cacher.getCacheKey("posts.find", { id: 2, title: "New post", content: "It can be very very looooooooooooooooooong content. So this key will also be too long" });
// 鍵值: 'posts.find:id|2|title|New post|content|It can be very very looooooooooooooooooong content. So this key will also be too long'
範例:限制長度為 60
const broker = new ServiceBroker({
cacher: {
type: "Memory",
options: {
maxParamsLength: 60
}
}
});
cacher.getCacheKey("posts.find", { id: 2, title: "New post", content: "It can be very very looooooooooooooooooong content. So this key will also be too long" });
// 鍵值: 'posts.find:id|2|title|New pL4ozUU24FATnNpDt1B0t1T5KP/T5/Y+JTIznKDspjT0='
快取也允許有條件的跳過快取機制來取得 新的
資料。你可以在呼叫 Action 之前,於 meta
中設定 $cache: false
來關閉此機制。
範例:呼叫 Action 時關閉快取
broker.call("greeter.hello", { name: "Moleculer" }, { meta: { $cache: false }}));
你也可以將快取視為一個選項,客製化一個控制函數來啟動快取。客製化函數能接收 context 實例作為參數,因此可以利用 ctx
來查詢參數或 meta 資訊。
範例:客製化快取條件
greeter.service.js
module.exports = {
name: "greeter",
actions: {
hello: {
cache: {
// 由 `noCache` 參數決定是否快取
enabled: ctx => ctx.params.noCache !== true,
keys: ["name"]
},
handler(ctx) {
this.logger.debug("Execute handler");
return `Hello ${ctx.params.name}`;
}
}
}
};
// 使用客製化 `enabled` 函數請求關閉快取
broker.call("greeter.hello", { name: "Moleculer", noCache: true });
你可以在 ServiceBroker
設定快取的存活時間 (Time To Live, TTL) ,也可以在 Action 選項中覆蓋此設定。
const { ServiceBroker } = require("moleculer");
const broker = new ServiceBroker({
cacher: {
type: "memory",
options: {
ttl: 30 // 30 秒
}
}
});
broker.createService({
name: "posts",
actions: {
list: {
cache: {
// 存活時間將被覆蓋為 5 秒
ttl: 5
},
handler(ctx) {
// ...
}
}
}
});
如果要客製化快取鍵值的生成方式,可以在 keygen
設定你自己的客製化函數,並使用 name
、 params
、 meta
與 keys
來建構函數規則。
const broker = new ServiceBroker({
cacher: {
type: "memory",
options: {
keygen(name, params, meta, keys) {
// 產生快取鍵值
// name - action 名稱
// params - ctx.params
// meta - ctx.meta
// keys - action 定義的快取鍵名稱
return "";
}
}
}
});
快取模組也可以手動使用。只需要呼叫 broker.cacher
的 get
、 set
與 del
方法即可。
// 儲存到快取
broker.cacher.set("mykey.a", { a: 5 });
// 取得快取
const obj = await broker.cacher.get("mykey.a")
// 刪除快取項目
await broker.cacher.del("mykey.a");
// 清除所有 'mykey' 項目
await broker.cacher.clean("mykey.**");
// 清除所有項目
await broker.cacher.clean();
範例:當使用內建 Redis
快取時,可以透過 broker.cacher.client
來使用 ioredis
套件的客戶端 API 。
// 建立 ioredis pipeline
const pipeline = broker.cacher.client.pipeline();
// 設定快取值
pipeline.set('mykey.a', 'myvalue.a');
pipeline.set('mykey.b', 'myvalue.b');
// 執行 pipeline
pipeline.exec();
當你在服務中建立新的資料模型時,你必須清除一些舊的快取模型項目,使下次請求能得到新的資訊,並建立更新後的快取資訊。
範例:在 Action 內清除快取
{
name: "users",
actions: {
create(ctx) {
// 建立新的使用者實體
const user = new User(ctx.params);
// 清除所有快取項目
this.broker.cacher.clean();
// 清除所有 `users.` 開頭的快取項目
this.broker.cacher.clean("users.**");
// 清除多種快取項目
this.broker.cacher.clean([ "users.**", "posts.**" ]);
// 刪除一個項目
this.broker.cacher.del("users.list");
// 刪除多個項目
this.broker.cacher.del([ "users.model:5", "users.model:8" ]);
}
}
}
如果要清除多個服務實例之間的快取項目,推薦的做法是使用廣播事件。注意,此方法僅適用於非集中管理的快取類型,如 Memory
或 MemoryLRU
。
範例:
module.exports = {
name: "users",
actions: {
create(ctx) {
// 建立新的使用者實體
const user = new User(ctx.params);
// 清除快取方法
this.cleanCache();
return user;
}
},
methods: {
cleanCache() {
// 發送廣播事件,包含自己的所有的服務實例都會收到事件
this.broker.broadcast("cache.clean.users");
}
},
events: {
"cache.clean.users"() {
if (this.broker.cacher) {
this.broker.cacher.clean("users.**");
}
}
}
};
服務相依是很常見的狀況。例如 posts
服務儲存了一些來自 users
服務的快取項目。
範例:
{
_id: 1,
title: "My post",
content: "Some content",
author: {
_id: 130,
fullName: "John Doe",
avatar: "https://..."
},
createdAt: 1519729167666
}
由於範例中的 author
儲存了一些來自 users
服務的資訊,所以當 users 服務清除了快取項目,連帶 posts
也應該要清除自己的快取項目。這種情況下,你應該也要在 posts
服務中訂閱 cache.clear.users
廣播事件來清除快取。
但有一個更簡單的方法,你可以建立一個 CacheCleaner
的混合函數,並在依賴的服務中加入。
cache.cleaner.mixin.js
module.exports = function (serviceNames) {
const events = {};
serviceNames.forEach(name => {
events[`cache.clean.${name}`] = function () {
if (this.broker.cacher) {
this.logger.debug(`Clear local '${this.name}' cache`);
this.broker.cacher.clean(`${this.name}.*`);
}
};
});
return {
events
};
};
posts.service.js
const CacheCleaner = require("./cache.cleaner.mixin");
module.exports = {
name: "posts",
mixins: [CacheCleaner([
"users",
"posts"
])],
actions: {
//...
}
};
Moleculer 還支援快取鎖定功能。詳情請參考 Add cache lock[2] 這個 PR。
範例:啟用鎖定
const broker = new ServiceBroker({
cacher: {
ttl: 60,
lock: true, // 啟用鎖定,預設為關閉。
}
});
範例:帶有存活時間的鎖定
const broker = new ServiceBroker({
cacher: {
ttl: 60,
lock: {
ttl: 15, // 鎖定最大的存活時間 (秒)
staleTime: 10, // 如果存活時間小於此時間,表示資源過期了
}
}
});
範例:禁用鎖定
const broker = new ServiceBroker({
cacher: {
ttl: 60,
lock: {
enable: false, // 關閉
ttl: 15, // 鎖定最大的存活時間 (秒)
staleTime: 10, // 如果存活時間小於此時間,表示資源過期了
}
}
});
範例:Redis 快取帶有 redlock 函式庫
const broker = new ServiceBroker({
cacher: {
type: "Redis",
options: {
// 鍵的前綴
prefix: "MOL",
// 存活時間
ttl: 30,
// Redis 客戶端監控
monitor: false,
// Redis 設定
redis: {
host: "redis-server",
port: 6379,
password: "1234",
db: 0
},
lock: {
ttl: 15, // 鎖定最大的存活時間 (秒)
staleTime: 10, // 如果存活時間小於此時間,表示資源過期了
},
// Redlock 設定
redlock: {
// Redis 客戶端。支援 node-redis 或 ioredis 。 預設只用 local 客戶端
clients: [client1, client2, client3],
// 預期時間浮動 (毫秒),請參閱:
// https://redis.io/docs/reference/patterns/distributed-locks/
driftFactor: 0.01,
// 在拋出錯誤前,嘗試鎖定資源的最大次數
retryCount: 10,
// 嘗試時的等待時間 (毫秒)
retryDelay: 200,
// 最大隨機時間,此時間會加到嘗試時間,以提升高度搶奪下的效率 (毫秒),請參閱:
// https://aws.amazon.com/tw/blogs/architecture/exponential-backoff-and-jitter/
retryJitter: 200
}
}
}
});
Memory
快取是一個內建的記憶體快取模組,它會將快取項目儲存在記憶體中。
範例:快速使用,可以設為 "Memory"
或 true
const broker = new ServiceBroker({
cacher: "Memory"
});
範例:選項設定方式
名稱 | 類型 | 預設值 | 說明 |
---|---|---|---|
ttl |
<Number> | null |
存活時間 (秒) |
clone |
<Boolean> | <Function> | false |
深度複製 |
keygen |
<Function> | null |
客製化鍵值產生器 |
maxParamsLength |
<Number> | null |
最大快取鍵長度 |
lock |
<Boolean> | <Object> | null |
啟用快取鎖定 |
const broker = new ServiceBroker({
cacher: {
type: "Memory",
options: {
ttl: 30 // 設定存活時間,若要關閉請設為 0 或 null 。
clone: true // 深拷貝
}
}
});
範例:客製化深度複製函數
快取會使用 Lodash 的 _.cloneDeep
方法來深度複製,如果不想使用,可將 clone
選項設為一個客製化函數來取代。
const broker = new ServiceBroker({
cacher: {
type: "Memory",
options: {
clone: data => JSON.parse(JSON.stringify(data))
}
}
});
MemoryLRU
是一個內建的 LRU 快取模組。它會刪除最近最少用的項目。
使用前請安裝 lru-cache[3] 套件
npm install lru-cache --save
。
範例:快速使用
const broker = new ServiceBroker({
cacher: "MemoryLRU"
});
範例:選項設定方式
名稱 | 類型 | 預設值 | 說明 |
---|---|---|---|
ttl |
<Number> | null |
存活時間 (秒) |
max |
<Number> | null |
快取中最大的項目數量 |
clone |
<Boolean> | <Function> | false |
深度複製 |
keygen |
<Function> | null |
客製化鍵值產生器 |
maxParamsLength |
<Number> | null |
最大快取鍵長度 |
lock |
<Boolean> | <Object> | null |
啟用快取鎖定 |
Redis
快取是一個內建基於 Redis 分散式的快取模組。如果你有多個服務實例,當一個服務實例儲存了一些快取,其它的實例也能夠透過 Redis 找到它。
使用前請安裝 ioredis[4] 套件
npm install ioredis --save
。
範例:快速使用,預設連線為 redis://localhost:6379
const broker = new ServiceBroker({
cacher: "Redis"
});
範例:連線到 Redis 服務器
const broker = new ServiceBroker({
cacher: "redis://redis-server:6379"
});
範例:選項設定方式
名稱 | 類型 | 預設值 | 說明 |
---|---|---|---|
prefix |
<String> | null |
鍵的前綴。 |
ttl |
<Number> | null |
存活時間 (秒)。 |
monitor |
<Boolean> | false |
Redis 客戶端監控[5] 。 |
redis |
<Object> | null |
客製化 Redis 選項[6] 。 |
keygen |
<Function> | null |
客製化鍵值產生器。 |
maxParamsLength |
<Number> | null |
最大快取鍵長度。 |
serializer |
<String> | "JSON" |
內建序列化器。 |
cluster |
<Object> | null |
Redis 客戶端叢集設定[7] 。 |
lock |
<Boolean> | <Object> | null |
啟用快取鎖定。 |
pingInterval |
<Number> | null |
每毫秒發出 Redis PING 命令。用於使可能閒置逾時的連線保持活躍狀態。 |
const broker = new ServiceBroker({
cacher: {
type: "Redis",
options: {
// 鍵的前綴
prefix: "MOL",
// 存活時間
ttl: 30,
// Redis 客戶端監控
monitor: false
// Redis 設定
redis: {
host: "redis-server",
port: 6379,
password: "1234",
db: 0
}
}
}
});
範例:使用 MessagePack 序列化器
你可以設定一個序列化器給 Redis 快取使用,它預設會使用 JSON
序列化器。
const broker = new ServiceBroker({
nodeID: "node-123",
cacher: {
type: "Redis",
options: {
ttl: 30,
// 使用 MessagePack 序列化器儲存資料
serializer: "MsgPack",
redis: {
host: "my-redis"
}
}
}
});
範例:使用 Redis 客戶端叢集
const broker = new ServiceBroker({
cacher: {
type: "Redis",
options: {
ttl: 30,
cluster: {
nodes: [
{ port: 6380, host: "127.0.0.1" },
{ port: 6381, host: "127.0.0.1" },
{ port: 6382, host: "127.0.0.1" }
],
options: {
// ...
}
}
}
}
});
你也可以建立客製化的快取模組,官方建議可以參考 記憶體快取[8] 或 Redis 快取[9] 的原始碼來修改,再實作 get
、 set
、 del
、 clean
方法。
範例:建立客製化快取
my-cacher.js
const BaseCacher = require("moleculer").Cachers.Base;
class MyCacher extends BaseCacher {
async get(key) { /*...*/ }
async set(key, data, ttl) { /*...*/ }
async del(key) { /*...*/ }
async clean(match = "**") { /*...*/ }
}
module.exports = MyCacher;
範例:使用客製化快取
const { ServiceBroker } = require("moleculer");
const MyCacher = require("./my-cacher");
const broker = new ServiceBroker({
cacher: new MyCacher()
});
[1] Caching, https://moleculer.services/docs/0.14/caching.html
[2] Add cache lock, https://github.com/moleculerjs/moleculer/pull/490
[3] lru-cache, https://github.com/isaacs/node-lru-cache
[4] ioredis, https://github.com/luin/ioredis
[5] ioredis Monitor, https://github.com/luin/ioredis#monitor
[6] ioredis Connect to Redis, https://github.com/luin/ioredis#connect-to-redis
[7] ioredis Cluster, https://github.com/luin/ioredis#cluster
[8] Moleculer Memory Cacher, https://github.com/moleculerjs/moleculer/blob/master/src/cachers/memory.js
[9] Moleculer Redis Cacher, https://github.com/moleculerjs/moleculer/blob/master/src/cachers/redis.js