baja-lite 1.8.11 → 1.8.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/cache.d.ts +189 -69
- package/cache.js +385 -170
- package/package.json +1 -1
package/cache.d.ts
CHANGED
|
@@ -1,9 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @internal 统一的 stale-allowed 注册表 key。
|
|
3
|
-
* 用带 TTL 的 string(而非全局 set),每个 stale-allowed 缓存独立管理自己的标记。
|
|
4
|
-
* key = [cache-meta]realKey,value = '1',TTL = 缓存 TTL。
|
|
5
|
-
* 好处:自动过期,不再需要全局 set,内存占用与缓存数量而非请求数量成正比。
|
|
6
|
-
*/
|
|
7
1
|
/**
|
|
8
2
|
* 缓存被清理时的行为策略。
|
|
9
3
|
* 由 @MethodCache 的 `staleMode` 配置决定,clearMethodCache 依此自动选择清理方式。
|
|
@@ -67,75 +61,173 @@ export declare function MethodLock<T = any>(config: {
|
|
|
67
61
|
lockMaxTime?: number;
|
|
68
62
|
}): (target: T, _propertyKey: string, descriptor: PropertyDescriptor) => void;
|
|
69
63
|
/**
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
* -
|
|
64
|
+
* 清理指定 key 的缓存。支持两种模式:
|
|
65
|
+
*
|
|
66
|
+
* **直接清理** — 传入具体缓存 key:
|
|
67
|
+
* - Purge 模式(sa=0):物理删除缓存
|
|
68
|
+
* - Stale 模式(sa=1):保留旧值,n 计数器 +1,后续请求读到 stale 数据并触发后台刷新
|
|
69
|
+
*
|
|
70
|
+
* **级联清理** — 传入 clearKey(关联清除 key):
|
|
71
|
+
* - 遍历所有注册了该 clearKey 的缓存,逐个执行上述清理逻辑
|
|
72
|
+
*
|
|
73
|
+
* @example <caption>直接清理某个缓存</caption>
|
|
74
|
+
* ```ts
|
|
75
|
+
* // Purge 模式:删除缓存,下次请求执行 fn 重建
|
|
76
|
+
* await clearMethodCache(`user-${userId}`);
|
|
77
|
+
*
|
|
78
|
+
* // Stale 模式:保留旧值 + 标记过期,后台异步刷新
|
|
79
|
+
* await clearMethodCache(`match-${matchId}`);
|
|
80
|
+
* ```
|
|
81
|
+
*
|
|
82
|
+
* @example <caption>级联清理 — 清理所有关联了某 clearKey 的缓存</caption>
|
|
83
|
+
* ```ts
|
|
84
|
+
* // 假设以下缓存都注册了 clearKey: ['match']
|
|
85
|
+
* // - match-${id}
|
|
86
|
+
* // - match-timer-${id}
|
|
87
|
+
* // - match-stat-${id}
|
|
74
88
|
*
|
|
75
|
-
*
|
|
89
|
+
* // 一次调用清理所有
|
|
90
|
+
* await clearMethodCache('match');
|
|
91
|
+
* ```
|
|
76
92
|
*/
|
|
77
93
|
export declare function clearMethodCache(key: string): Promise<void>;
|
|
78
94
|
/**
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
*
|
|
95
|
+
* 执行一个带缓存的方法。先查缓存 → 命中直接返回;未命中则加锁执行 fn 并缓存结果。
|
|
96
|
+
*
|
|
97
|
+
* 支持三种缓存策略(通过 `staleMode` 切换):
|
|
98
|
+
* - **Purge**(默认):清理时物理删除缓存,下次请求阻塞执行 fn 重建
|
|
99
|
+
* - **StaleWhileRevalidate**:清理时保留旧值 + 标记过期,后续请求零阻塞读到旧值,后台异步刷新
|
|
100
|
+
*
|
|
101
|
+
* 防击穿机制:
|
|
102
|
+
* - 缓存 miss 时通过 `[cache-sf]key` 原子锁(SETNX)选出"首飞"执行 fn
|
|
103
|
+
* - 其他并发请求轮询缓存,首飞写完后立即命中,避免重复执行 fn
|
|
104
|
+
*
|
|
105
|
+
* @example <caption>基础用法 — Purge 模式</caption>
|
|
106
|
+
* ```ts
|
|
107
|
+
* const result = await excuteWithCache({
|
|
108
|
+
* key: `user-${userId}`,
|
|
109
|
+
* autoClearTime: 60, // 60分钟后自动过期
|
|
110
|
+
* cacheNullValue: true, // null 也缓存(防穿透)
|
|
111
|
+
* }, () => loadUserFromDB(userId));
|
|
112
|
+
* ```
|
|
113
|
+
*
|
|
114
|
+
* @example <caption>Stale 模式 — 清理时保留旧值 + 后台刷新</caption>
|
|
115
|
+
* ```ts
|
|
116
|
+
* const result = await excuteWithCache({
|
|
117
|
+
* key: `match-${matchId}`,
|
|
118
|
+
* staleMode: CacheStaleMode.StaleWhileRevalidate,
|
|
119
|
+
* onCacheUpdated(value) {
|
|
120
|
+
* // 缓存刷新后触发:推送 WebSocket、预热下游等
|
|
121
|
+
* this.broadcast(value);
|
|
122
|
+
* },
|
|
123
|
+
* }, () => computeMatchData(matchId));
|
|
124
|
+
* ```
|
|
125
|
+
*
|
|
126
|
+
* @example <caption>cacheOnly — 只读缓存,不调用 fn</caption>
|
|
127
|
+
* ```ts
|
|
128
|
+
* const cached = await excuteWithCache({
|
|
129
|
+
* key: `config`,
|
|
130
|
+
* cacheOnly: true, // 没有缓存返回 null,不执行 fn
|
|
131
|
+
* }, () => loadConfig());
|
|
132
|
+
* ```
|
|
133
|
+
*
|
|
134
|
+
* @example <caption>forceRefresh — 跳过缓存直接执行 fn</caption>
|
|
135
|
+
* ```ts
|
|
136
|
+
* const fresh = await excuteWithCache({
|
|
137
|
+
* key: `match-${id}`,
|
|
138
|
+
* forceRefresh: true, // 忽略已有缓存,执行 fn 后写回(sa=0)
|
|
139
|
+
* }, () => computeFreshData(id));
|
|
140
|
+
* ```
|
|
141
|
+
*
|
|
142
|
+
* @example <caption>关联清除 — clearKey 级联清理</caption>
|
|
143
|
+
* ```ts
|
|
144
|
+
* const result = await excuteWithCache({
|
|
145
|
+
* key: `match-timer-${id}`,
|
|
146
|
+
* clearKey: ['match'], // clearMethodCache('match') 时级联清除
|
|
147
|
+
* }, () => computeTimer(id));
|
|
148
|
+
*
|
|
149
|
+
* // 清理所有关联了 'match' 的缓存
|
|
150
|
+
* await clearMethodCache('match');
|
|
151
|
+
* ```
|
|
83
152
|
*/
|
|
84
153
|
export declare function excuteWithCache<T>(config: {
|
|
85
|
-
/** 返回缓存key,参数=方法的参数+当前用户对象,可以用来清空缓存。 */
|
|
86
154
|
key: ((...args: any[]) => string) | string;
|
|
87
|
-
|
|
88
|
-
clearKey?: string[];
|
|
89
|
-
/**
|
|
90
|
-
* 自动清空缓存的时间,单位分钟。
|
|
91
|
-
* ⚠️ 不能与 `staleMode: StaleWhileRevalidate` 同时使用:
|
|
92
|
-
* stale 只是把缓存"标记为过期",旧值仍在;而 autoClearTime 到期后会物理删除 key。
|
|
93
|
-
* 两者语义冲突——stale 期望保留旧值等后台刷新,autoClearTime 却会直接删掉它。
|
|
94
|
-
*/
|
|
155
|
+
clearKey?: ((...args: any[]) => string[]) | string[];
|
|
95
156
|
autoClearTime?: number;
|
|
96
|
-
/** 是否缓存 null / undefined(负缓存防穿透),默认 false。 */
|
|
97
157
|
cacheNullValue?: boolean;
|
|
98
|
-
/** 负缓存 TTL(分钟)。默认沿用 autoClearTime;都不设时不写入。 */
|
|
99
158
|
nullCacheTime?: number;
|
|
100
|
-
/** 随着当前用户sesion的清空而一起清空 */
|
|
101
159
|
clearWithSession?: boolean;
|
|
102
|
-
/**
|
|
103
|
-
* 缓存被清理时的行为策略。默认 Purge。
|
|
104
|
-
* StaleWhileRevalidate 时设 sa=1 使 clearMethodCache 走"标记 stale + 保留旧值"路径。
|
|
105
|
-
*/
|
|
106
160
|
staleMode?: CacheStaleMode;
|
|
107
|
-
/**
|
|
108
|
-
* 缓存更新回调。异步方法,在 setMethodCache 成功写缓存后调用。
|
|
109
|
-
* 调用时三个参数:this → 方法的执行上下文(config.ctx);value → 缓存值;args → 方法输入参数。
|
|
110
|
-
*/
|
|
111
161
|
onCacheUpdated?: (this: any, value: T, ...args: any[]) => void | Promise<void>;
|
|
162
|
+
ctx?: any;
|
|
163
|
+
args?: any[];
|
|
164
|
+
devid?: string | false | undefined;
|
|
112
165
|
/**
|
|
113
|
-
*
|
|
114
|
-
* @example
|
|
115
|
-
* ```ts
|
|
116
|
-
* excuteWithCache({
|
|
117
|
-
* key: 'calibration',
|
|
118
|
-
* ctx: service,
|
|
119
|
-
* args: ['device001'],
|
|
120
|
-
* onCacheUpdated(this: MyService, value, args) {
|
|
121
|
-
* this.broadcast(value);
|
|
122
|
-
* },
|
|
123
|
-
* }, () => service.getCalibration('device001'));
|
|
124
|
-
* ```
|
|
166
|
+
* 只用缓存,绝不调用原始方法。无论缓存是否过期都直接返回;没有缓存则返回 null。
|
|
125
167
|
*/
|
|
126
|
-
|
|
168
|
+
cacheOnly?: boolean;
|
|
127
169
|
/**
|
|
128
|
-
*
|
|
129
|
-
* 用于告知回调是哪些参数触发了本次缓存更新。
|
|
170
|
+
* 忽略已有缓存,直接调用原始方法并更新缓存(写回时 sa=0)。
|
|
130
171
|
*/
|
|
131
|
-
|
|
172
|
+
forceRefresh?: boolean;
|
|
132
173
|
}, fn: () => Promise<T>): Promise<T>;
|
|
133
174
|
/**
|
|
134
|
-
*
|
|
135
|
-
*
|
|
136
|
-
*
|
|
137
|
-
*
|
|
138
|
-
*
|
|
175
|
+
* 方法装饰器:为方法添加 Redis 缓存能力。
|
|
176
|
+
*
|
|
177
|
+
* 工作流程:
|
|
178
|
+
* 1. 调用方法时先查 Redis 缓存
|
|
179
|
+
* 2. 命中 → 直接返回缓存值(若 stale 则触发后台异步刷新)
|
|
180
|
+
* 3. 未命中 → 原子锁(SETNX)选出"首飞"执行原方法 → 写缓存 → 返回
|
|
181
|
+
* 4. 其他并发请求轮询缓存,首飞写完后立即命中
|
|
182
|
+
*
|
|
183
|
+
* @example <caption>基础用法 — Purge 模式(默认)</caption>
|
|
184
|
+
* ```ts
|
|
185
|
+
* @MethodCache({
|
|
186
|
+
* key: (userId: string) => `user-${userId}`,
|
|
187
|
+
* autoClearTime: 60, // 60分钟后自动过期
|
|
188
|
+
* })
|
|
189
|
+
* async getUser(userId: string) {
|
|
190
|
+
* return await this.db.query(...);
|
|
191
|
+
* }
|
|
192
|
+
* ```
|
|
193
|
+
*
|
|
194
|
+
* @example <caption>Stale 模式 — 清理时保留旧值 + 后台刷新</caption>
|
|
195
|
+
* ```ts
|
|
196
|
+
* @MethodCache({
|
|
197
|
+
* key: (matchId: string) => `match-${matchId}`,
|
|
198
|
+
* staleMode: CacheStaleMode.StaleWhileRevalidate,
|
|
199
|
+
* onCacheUpdated(this: MyService, value) {
|
|
200
|
+
* // 缓存刷新后触发(this = 当前 service 实例)
|
|
201
|
+
* this.webSocket.broadcast(value);
|
|
202
|
+
* },
|
|
203
|
+
* })
|
|
204
|
+
* async getMatch(matchId: string) { ... }
|
|
205
|
+
*
|
|
206
|
+
* // 清理缓存 → 旧值保留,后台异步刷新
|
|
207
|
+
* await clearMethodCache(`match-${id}`);
|
|
208
|
+
* ```
|
|
209
|
+
*
|
|
210
|
+
* @example <caption>关联清除 — clearKey 级联清理</caption>
|
|
211
|
+
* ```ts
|
|
212
|
+
* @MethodCache({
|
|
213
|
+
* key: (matchId: string) => `match-timer-${matchId}`,
|
|
214
|
+
* clearKey: ['match'], // 声明依赖关系
|
|
215
|
+
* staleMode: CacheStaleMode.StaleWhileRevalidate,
|
|
216
|
+
* })
|
|
217
|
+
* async getMatchTimer(matchId: string) { ... }
|
|
218
|
+
*
|
|
219
|
+
* // 清理所有关联了 'match' 的缓存(包括 match-timer-*)
|
|
220
|
+
* await clearMethodCache('match');
|
|
221
|
+
* ```
|
|
222
|
+
*
|
|
223
|
+
* @example <caption>cacheOnly — 只读缓存,绝不调用原方法</caption>
|
|
224
|
+
* ```ts
|
|
225
|
+
* @MethodCache({
|
|
226
|
+
* key: 'app-config',
|
|
227
|
+
* cacheOnly: true, // 没有缓存返回 null
|
|
228
|
+
* })
|
|
229
|
+
* async loadConfig() { ... }
|
|
230
|
+
* ```
|
|
139
231
|
*/
|
|
140
232
|
export declare function MethodCache<T = any>(config: {
|
|
141
233
|
/** 返回缓存key,参数=方法的参数[注意:必须和主方法的参数数量、完全一致,同时会追加一个当前用户对象]+当前用户对象,可以用来清空缓存。 */
|
|
@@ -163,28 +255,54 @@ export declare function MethodCache<T = any>(config: {
|
|
|
163
255
|
* 调用时三个参数:this → 执行上下文;args → 方法输入参数;value → 缓存值。
|
|
164
256
|
*/
|
|
165
257
|
onCacheUpdated?: (this: any, value: T, ...args: any[]) => void | Promise<void>;
|
|
258
|
+
/**
|
|
259
|
+
* 只用缓存,绝不调用原始方法。无论缓存是否过期都直接返回;没有缓存则返回 null。
|
|
260
|
+
*/
|
|
261
|
+
cacheOnly?: boolean;
|
|
262
|
+
/** 忽略已有缓存,直接调用原始方法并更新缓存(写回时 sa=0)。 */
|
|
263
|
+
forceRefresh?: boolean;
|
|
166
264
|
}): (_target: T, _propertyKey: string, descriptor: PropertyDescriptor) => void;
|
|
167
265
|
/**
|
|
168
|
-
*
|
|
169
|
-
* (如外部事件驱动的数据变更、手动刷新等)。
|
|
266
|
+
* 外部直接写入缓存(绕过 @MethodCache 装饰器)。
|
|
170
267
|
*
|
|
171
|
-
*
|
|
268
|
+
* 与 @MethodCache 共享同一套存储格式 `{ t, d, sa, n }`,写入的缓存可被
|
|
269
|
+
* @MethodCache 装饰的方法正常读取(包括 stale 判断、计数器等)。
|
|
172
270
|
*
|
|
173
|
-
*
|
|
174
|
-
*
|
|
175
|
-
*
|
|
271
|
+
* 适用场景:
|
|
272
|
+
* - 外部事件驱动的数据变更(如 MQ 消息、定时任务)
|
|
273
|
+
* - 手动刷新缓存(如管理后台"清除并重建"按钮)
|
|
274
|
+
* - 跨服务同步缓存状态
|
|
176
275
|
*
|
|
177
|
-
* @example
|
|
276
|
+
* @example <caption>基础用法 — 写入缓存</caption>
|
|
178
277
|
* ```ts
|
|
179
|
-
*
|
|
180
|
-
*
|
|
278
|
+
* await setCache('match-timer-123', timer, {
|
|
279
|
+
* autoClearTime: 120, // 120分钟后过期
|
|
280
|
+
* });
|
|
281
|
+
* ```
|
|
181
282
|
*
|
|
182
|
-
*
|
|
283
|
+
* @example <caption>关联清除 — 注册 clearKey 级联关系</caption>
|
|
284
|
+
* ```ts
|
|
183
285
|
* await setCache('match-timer-123', timer, {
|
|
184
286
|
* autoClearTime: 120,
|
|
185
|
-
*
|
|
186
|
-
*
|
|
187
|
-
*
|
|
287
|
+
* clearKey: ['match'], // clearMethodCache('match') 时级联清除
|
|
288
|
+
* });
|
|
289
|
+
*
|
|
290
|
+
* // 函数式 clearKey(参数来自 options.args)
|
|
291
|
+
* await setCache('match-timer-123', timer, {
|
|
292
|
+
* clearKey: (matchId) => [`match-${matchId}`],
|
|
293
|
+
* args: [matchId],
|
|
294
|
+
* });
|
|
295
|
+
* ```
|
|
296
|
+
*
|
|
297
|
+
* @example <caption>回调 — 缓存写入后触发通知</caption>
|
|
298
|
+
* ```ts
|
|
299
|
+
* await setCache('match-timer-123', timer, {
|
|
300
|
+
* autoClearTime: 120,
|
|
301
|
+
* ctx: this,
|
|
302
|
+
* args: [matchId],
|
|
303
|
+
* onCacheUpdated(value, matchId) {
|
|
304
|
+
* this.webSocket.sendRoom(matchId, { action: 'timer', param: value });
|
|
305
|
+
* },
|
|
188
306
|
* });
|
|
189
307
|
* ```
|
|
190
308
|
*/
|
|
@@ -193,6 +311,8 @@ export declare function setCache<T = any>(cacheKey: string, value: T, options?:
|
|
|
193
311
|
autoClearTime?: number;
|
|
194
312
|
/** 是否缓存 null / undefined。默认 false。 */
|
|
195
313
|
cacheNullValue?: boolean;
|
|
314
|
+
/** 关联清除 key。clearMethodCache(clearKey) 时级联清除本缓存。 */
|
|
315
|
+
clearKey?: ((...args: any[]) => string[]) | string[];
|
|
196
316
|
/** 缓存更新回调 */
|
|
197
317
|
onCacheUpdated?: (this: any, value: T, ...args: any[]) => void | Promise<void>;
|
|
198
318
|
/** 回调的 this 上下文 */
|
package/cache.js
CHANGED
|
@@ -18,28 +18,26 @@ function cacheLog(message, ...params) {
|
|
|
18
18
|
*/
|
|
19
19
|
const globalStaleInflight = new Map();
|
|
20
20
|
/**
|
|
21
|
-
* @internal [cache] 前缀下的子 key
|
|
21
|
+
* @internal [cache] 前缀下的子 key 命名全部收拢到这里。
|
|
22
22
|
*
|
|
23
23
|
* 命名规范:
|
|
24
|
-
* - [cache]key 缓存值(JSON: {
|
|
24
|
+
* - [cache]key 缓存值(JSON: { t, d, sa?, n? })
|
|
25
25
|
* - [cache-sf]key single-flight 首飞标记(SETNX 抢锁用)
|
|
26
|
-
* - [cache-meta]key stale-allowed 标记(带 TTL,与缓存同生命周期)
|
|
27
26
|
* - [cache-parent]key / [cache-child]key 关联清除的 parent↔child 双向 set
|
|
28
27
|
*/
|
|
29
28
|
const CacheKey = {
|
|
30
29
|
value: (k) => `[cache]${k}`,
|
|
31
30
|
sf: (k) => `[cache-sf]${k}`,
|
|
32
|
-
meta: (k) => `[cache-meta]${k}`,
|
|
33
31
|
parent: (k) => `[cache-parent]${k}`,
|
|
34
32
|
child: (k) => `[cache-child]${k}`,
|
|
35
33
|
};
|
|
36
34
|
/** @internal 序列化缓存值 */
|
|
37
|
-
function serializeCacheValue(data, timestamp,
|
|
35
|
+
function serializeCacheValue(data, timestamp, staleAllowed = false, staleCount = 0) {
|
|
38
36
|
const v = { t: timestamp, d: data };
|
|
39
|
-
if (stale)
|
|
40
|
-
v.s = 1;
|
|
41
37
|
if (staleAllowed)
|
|
42
38
|
v.sa = 1;
|
|
39
|
+
if (staleCount > 0)
|
|
40
|
+
v.n = staleCount;
|
|
43
41
|
return JSON.stringify(v);
|
|
44
42
|
}
|
|
45
43
|
/** @internal 反序列化缓存值 */
|
|
@@ -53,12 +51,6 @@ function deserializeCacheValue(raw) {
|
|
|
53
51
|
return { t: 0, d: raw, isNullish: raw === 'null' || raw === 'undefined' };
|
|
54
52
|
}
|
|
55
53
|
}
|
|
56
|
-
/**
|
|
57
|
-
* @internal 统一的 stale-allowed 注册表 key。
|
|
58
|
-
* 用带 TTL 的 string(而非全局 set),每个 stale-allowed 缓存独立管理自己的标记。
|
|
59
|
-
* key = [cache-meta]realKey,value = '1',TTL = 缓存 TTL。
|
|
60
|
-
* 好处:自动过期,不再需要全局 set,内存占用与缓存数量而非请求数量成正比。
|
|
61
|
-
*/
|
|
62
54
|
/**
|
|
63
55
|
* 缓存被清理时的行为策略。
|
|
64
56
|
* 由 @MethodCache 的 `staleMode` 配置决定,clearMethodCache 依此自动选择清理方式。
|
|
@@ -124,7 +116,8 @@ export async function GetRedisLock(key, lockMaxActive) {
|
|
|
124
116
|
await initLock.release();
|
|
125
117
|
// eslint-disable-next-line no-empty
|
|
126
118
|
}
|
|
127
|
-
catch
|
|
119
|
+
catch {
|
|
120
|
+
// ignore
|
|
128
121
|
}
|
|
129
122
|
}
|
|
130
123
|
}
|
|
@@ -209,22 +202,28 @@ async function setMethodCache(config, devid) {
|
|
|
209
202
|
}
|
|
210
203
|
}
|
|
211
204
|
// 写入缓存值(带时间戳 + staleAllowed 标志)。
|
|
212
|
-
// 保留旧 sa:只有显式 StaleWhileRevalidate 才设 1,否则沿用旧值(防止覆盖已有的 sa=1)。
|
|
213
205
|
const timestamp = Date.now();
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
206
|
+
let staleAllowed;
|
|
207
|
+
if (config.staleAllowedOverride !== undefined) {
|
|
208
|
+
staleAllowed = config.staleAllowedOverride; // 显式指定
|
|
209
|
+
}
|
|
210
|
+
else {
|
|
211
|
+
// 保留旧 sa:显式 StaleWhileRevalidate → 1,否则沿用旧值
|
|
212
|
+
const existingSa = await db.get(CacheKey.value(config.key)).then(r => r ? deserializeCacheValue(r).sa : undefined);
|
|
213
|
+
staleAllowed = config.staleMode === CacheStaleMode.StaleWhileRevalidate ? true : (existingSa ?? false);
|
|
214
|
+
}
|
|
215
|
+
const payload = serializeCacheValue(dataStr, timestamp, staleAllowed);
|
|
217
216
|
if (ttl) { // 自动清空
|
|
218
217
|
await db.set(CacheKey.value(config.key), payload, 'EX', ttl * 60);
|
|
219
|
-
cacheLog(
|
|
218
|
+
cacheLog(`${config.key} seted t${timestamp}${isNullish ? ' (null)' : ''}!`);
|
|
220
219
|
}
|
|
221
220
|
else {
|
|
222
221
|
await db.set(CacheKey.value(config.key), payload);
|
|
223
|
-
cacheLog(
|
|
222
|
+
cacheLog(`${config.key} seted t${timestamp}${isNullish ? ' (null)' : ''}!`);
|
|
224
223
|
}
|
|
225
224
|
// staleAllowed 已存储在缓存值内部(CacheValue.sa),无需单独的 meta key
|
|
226
225
|
// 删除旧格式残留(兼容升级)
|
|
227
|
-
await db.del(
|
|
226
|
+
// await db.del(`[cache-meta]${config.key}`); // 清理旧格式残留
|
|
228
227
|
// 订阅:清空 clear list —— 同一 key 在缓存生命周期内只注册一次监听器,
|
|
229
228
|
// 否则每次 miss 都会 .on 一次,clearMethodCache 触发时回调会被执行 N 遍,
|
|
230
229
|
// 长期运行造成监听器泄漏。
|
|
@@ -233,7 +232,7 @@ async function setMethodCache(config, devid) {
|
|
|
233
232
|
if (globalThis[_EventBus].listenerCount(event) === 0) {
|
|
234
233
|
globalThis[_EventBus].on(event, async (key) => {
|
|
235
234
|
await clearCacheKey(key);
|
|
236
|
-
cacheLog(
|
|
235
|
+
cacheLog(`${key} clear by key!`);
|
|
237
236
|
});
|
|
238
237
|
}
|
|
239
238
|
}
|
|
@@ -243,7 +242,7 @@ async function setMethodCache(config, devid) {
|
|
|
243
242
|
if (globalThis[_EventBus].listenerCount(event) === 0) {
|
|
244
243
|
globalThis[_EventBus].on(event, async (key) => {
|
|
245
244
|
await clearCacheKey(key);
|
|
246
|
-
cacheLog(
|
|
245
|
+
cacheLog(`${key} clear by devid!`);
|
|
247
246
|
});
|
|
248
247
|
}
|
|
249
248
|
}
|
|
@@ -265,7 +264,7 @@ async function clearCacheKey(key) {
|
|
|
265
264
|
if (parentType === 'set') {
|
|
266
265
|
const childKeys = await db.smembers(CacheKey.parent(key));
|
|
267
266
|
for (const child of childKeys) {
|
|
268
|
-
cacheLog(
|
|
267
|
+
cacheLog(`${child} cleared via parent ${key}!`);
|
|
269
268
|
await clearCacheKey(child);
|
|
270
269
|
affectedChildren.push(child);
|
|
271
270
|
}
|
|
@@ -275,24 +274,29 @@ async function clearCacheKey(key) {
|
|
|
275
274
|
const valueRaw = await db.get(CacheKey.value(key));
|
|
276
275
|
const v = valueRaw ? deserializeCacheValue(valueRaw) : null;
|
|
277
276
|
if (v?.sa) {
|
|
278
|
-
// stale-allowed
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
if (ttlSec > 0) {
|
|
282
|
-
await db.set(CacheKey.value(key), newPayload, 'EX', ttlSec);
|
|
277
|
+
// stale-allowed:n + 1(原子 Lua;失败退化为普通写入)
|
|
278
|
+
try {
|
|
279
|
+
await db.eval(CLEAR_INCR_LUA, 1, CacheKey.value(key));
|
|
283
280
|
}
|
|
284
|
-
|
|
285
|
-
|
|
281
|
+
catch {
|
|
282
|
+
const newPayload = serializeCacheValue(v.d, v.t, true, (v.n ?? 0) + 1);
|
|
283
|
+
const ttlSec = await db.ttl(CacheKey.value(key));
|
|
284
|
+
if (ttlSec > 0) {
|
|
285
|
+
await db.set(CacheKey.value(key), newPayload, 'EX', ttlSec);
|
|
286
|
+
}
|
|
287
|
+
else {
|
|
288
|
+
await db.set(CacheKey.value(key), newPayload);
|
|
289
|
+
}
|
|
286
290
|
}
|
|
287
|
-
cacheLog(
|
|
291
|
+
cacheLog(`${key} clear → n+1`);
|
|
288
292
|
}
|
|
289
293
|
else {
|
|
290
294
|
// Purge:直接删缓存值
|
|
291
295
|
await db.del(CacheKey.value(key));
|
|
292
|
-
cacheLog(
|
|
296
|
+
cacheLog(`${key} purged!`);
|
|
293
297
|
}
|
|
294
298
|
// 清理旧格式残留
|
|
295
|
-
await db.del(
|
|
299
|
+
// await db.del(`[cache-meta]${key}`); // 清理旧格式残留
|
|
296
300
|
// 3. 清理 child→parent 反向关联
|
|
297
301
|
const childType = await db.type(CacheKey.child(key));
|
|
298
302
|
if (childType === 'set') {
|
|
@@ -308,12 +312,34 @@ async function clearCacheKey(key) {
|
|
|
308
312
|
return affectedChildren;
|
|
309
313
|
}
|
|
310
314
|
/**
|
|
311
|
-
*
|
|
312
|
-
*
|
|
313
|
-
*
|
|
314
|
-
* -
|
|
315
|
+
* 清理指定 key 的缓存。支持两种模式:
|
|
316
|
+
*
|
|
317
|
+
* **直接清理** — 传入具体缓存 key:
|
|
318
|
+
* - Purge 模式(sa=0):物理删除缓存
|
|
319
|
+
* - Stale 模式(sa=1):保留旧值,n 计数器 +1,后续请求读到 stale 数据并触发后台刷新
|
|
320
|
+
*
|
|
321
|
+
* **级联清理** — 传入 clearKey(关联清除 key):
|
|
322
|
+
* - 遍历所有注册了该 clearKey 的缓存,逐个执行上述清理逻辑
|
|
323
|
+
*
|
|
324
|
+
* @example <caption>直接清理某个缓存</caption>
|
|
325
|
+
* ```ts
|
|
326
|
+
* // Purge 模式:删除缓存,下次请求执行 fn 重建
|
|
327
|
+
* await clearMethodCache(`user-${userId}`);
|
|
315
328
|
*
|
|
316
|
-
*
|
|
329
|
+
* // Stale 模式:保留旧值 + 标记过期,后台异步刷新
|
|
330
|
+
* await clearMethodCache(`match-${matchId}`);
|
|
331
|
+
* ```
|
|
332
|
+
*
|
|
333
|
+
* @example <caption>级联清理 — 清理所有关联了某 clearKey 的缓存</caption>
|
|
334
|
+
* ```ts
|
|
335
|
+
* // 假设以下缓存都注册了 clearKey: ['match']
|
|
336
|
+
* // - match-${id}
|
|
337
|
+
* // - match-timer-${id}
|
|
338
|
+
* // - match-stat-${id}
|
|
339
|
+
*
|
|
340
|
+
* // 一次调用清理所有
|
|
341
|
+
* await clearMethodCache('match');
|
|
342
|
+
* ```
|
|
317
343
|
*/
|
|
318
344
|
export async function clearMethodCache(key) {
|
|
319
345
|
const redisDao = globalThis[_dao]?.[DBType.Redis]?.[_primaryDB];
|
|
@@ -342,32 +368,99 @@ const SINGLEFLIGHT_LOCK_TTL_MS = 30000; // 首飞标记 TTL,兜底首飞挂掉
|
|
|
342
368
|
const SINGLEFLIGHT_POLL_INTERVAL_MS = 50; // 等待者轮询缓存的间隔
|
|
343
369
|
const SINGLEFLIGHT_MAX_WAIT_MS = 30000; // 等待者最长等多久;超时就自己跑 fn(保底,不应该常发生)
|
|
344
370
|
/**
|
|
345
|
-
*
|
|
346
|
-
*
|
|
347
|
-
|
|
348
|
-
|
|
371
|
+
* Lua 脚本:clear —— 原子 n+1(保留旧值和 TTL)。
|
|
372
|
+
* 返回新 n 值;-1 表示 key 不存在或不是 sa。
|
|
373
|
+
*/
|
|
374
|
+
const CLEAR_INCR_LUA = `
|
|
375
|
+
local raw = redis.call('GET', KEYS[1])
|
|
376
|
+
if not raw then return -1 end
|
|
377
|
+
local ok, val = pcall(cjson.decode, raw)
|
|
378
|
+
if not ok then return -1 end
|
|
379
|
+
if not val.sa then return -1 end
|
|
380
|
+
val.n = (tonumber(val.n) or 0) + 1
|
|
381
|
+
local ttl = redis.call('TTL', KEYS[1])
|
|
382
|
+
if ttl > 0 then
|
|
383
|
+
redis.call('SET', KEYS[1], cjson.encode(val), 'EX', ttl)
|
|
384
|
+
else
|
|
385
|
+
redis.call('SET', KEYS[1], cjson.encode(val))
|
|
386
|
+
end
|
|
387
|
+
return val.n
|
|
388
|
+
`;
|
|
389
|
+
/**
|
|
390
|
+
* Lua 脚本:refresh 完成 —— 原子读当前 n、写新数据、n-1。
|
|
391
|
+
* 返回 [newN, status]:status=1 成功,-1 不存在,-2 非 sa。
|
|
392
|
+
*
|
|
393
|
+
* KEYS[1] = cache key
|
|
394
|
+
* ARGV[1] = 新数据的原始 JSON 字符串(将作为 CacheValue.d)
|
|
395
|
+
* ARGV[2] = TTL 秒数(0 表示保持原 TTL)
|
|
396
|
+
* ARGV[3] = 当前时间戳(毫秒,将作为 CacheValue.t)
|
|
349
397
|
*/
|
|
398
|
+
const REFRESH_DECR_LUA = `
|
|
399
|
+
local raw = redis.call('GET', KEYS[1])
|
|
400
|
+
if not raw then return {-2, 0} end
|
|
401
|
+
local ok, val = pcall(cjson.decode, raw)
|
|
402
|
+
if not ok then return {-2, 0} end
|
|
403
|
+
if not val.sa then return {-1, 0} end
|
|
404
|
+
local n = tonumber(val.n) or 0
|
|
405
|
+
local newN = math.max(0, n - 1)
|
|
406
|
+
-- 重建统一格式 {t, d, sa, n?}
|
|
407
|
+
local newData = {
|
|
408
|
+
t = tonumber(ARGV[3]) or 0,
|
|
409
|
+
d = ARGV[1],
|
|
410
|
+
sa = val.sa
|
|
411
|
+
}
|
|
412
|
+
if newN > 0 then newData.n = newN end
|
|
413
|
+
local ttl = tonumber(ARGV[2]) or 0
|
|
414
|
+
if ttl > 0 then
|
|
415
|
+
redis.call('SET', KEYS[1], cjson.encode(newData), 'EX', ttl)
|
|
416
|
+
else
|
|
417
|
+
redis.call('SET', KEYS[1], cjson.encode(newData))
|
|
418
|
+
end
|
|
419
|
+
return {newN, 1}
|
|
420
|
+
`;
|
|
350
421
|
async function staleBackgroundRefresh(opts, setOpts) {
|
|
351
422
|
const db = getRedisDB();
|
|
352
423
|
const sfKey = CacheKey.sf(opts.key);
|
|
353
424
|
try {
|
|
354
|
-
const
|
|
355
|
-
if (
|
|
356
|
-
return;
|
|
425
|
+
const locked = await db.set(sfKey, '1', 'PX', SINGLEFLIGHT_LOCK_TTL_MS, 'NX');
|
|
426
|
+
if (locked !== 'OK')
|
|
427
|
+
return;
|
|
357
428
|
}
|
|
358
429
|
catch {
|
|
359
|
-
return;
|
|
430
|
+
return;
|
|
360
431
|
}
|
|
361
432
|
try {
|
|
433
|
+
cacheLog(`${opts.key} refresh start...`);
|
|
362
434
|
const result = await opts.fn();
|
|
363
|
-
//
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
435
|
+
// 序列化新数据(不含 n)
|
|
436
|
+
const rawData = (result === null || result === undefined) ? 'null' : JSON.stringify(result);
|
|
437
|
+
const ttlSec = await db.ttl(CacheKey.value(opts.key));
|
|
438
|
+
// 原子地写新数据 + n-1(Lua 脚本内完成)
|
|
439
|
+
let r;
|
|
440
|
+
try {
|
|
441
|
+
r = await db.eval(REFRESH_DECR_LUA, 1, CacheKey.value(opts.key), rawData, String(Math.max(0, ttlSec)), String(Date.now()));
|
|
442
|
+
}
|
|
443
|
+
catch {
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
const [newN, status] = r ?? [0, 0];
|
|
447
|
+
if (status === -2 || status === -1) {
|
|
448
|
+
cacheLog(`${opts.key} refresh skipped (status=${status})`);
|
|
449
|
+
}
|
|
450
|
+
else {
|
|
451
|
+
cacheLog(`${opts.key} refresh done (n → ${newN})`);
|
|
452
|
+
void opts.onCacheUpdated?.call(opts.ctx, result, ...(opts.args ?? []));
|
|
453
|
+
// n > 0 → 刷新期间又被 clear,继续下一轮刷新
|
|
454
|
+
if (newN > 0 && !globalStaleInflight.has(opts.key)) {
|
|
455
|
+
globalStaleInflight.set(opts.key, staleBackgroundRefresh(opts, setOpts).finally(() => {
|
|
456
|
+
globalStaleInflight.delete(opts.key);
|
|
457
|
+
}));
|
|
458
|
+
}
|
|
459
|
+
}
|
|
368
460
|
}
|
|
369
461
|
catch (error) {
|
|
370
|
-
|
|
462
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
463
|
+
globalThis[_LoggerService].warn?.(`cache ${opts.key} background refresh failed: ${msg}`);
|
|
371
464
|
}
|
|
372
465
|
finally {
|
|
373
466
|
try {
|
|
@@ -376,97 +469,167 @@ async function staleBackgroundRefresh(opts, setOpts) {
|
|
|
376
469
|
catch { /* TTL 兜底 */ }
|
|
377
470
|
}
|
|
378
471
|
}
|
|
379
|
-
|
|
472
|
+
/**
|
|
473
|
+
* 执行一个带缓存的方法。先查缓存 → 命中直接返回;未命中则加锁执行 fn 并缓存结果。
|
|
474
|
+
*
|
|
475
|
+
* 支持三种缓存策略(通过 `staleMode` 切换):
|
|
476
|
+
* - **Purge**(默认):清理时物理删除缓存,下次请求阻塞执行 fn 重建
|
|
477
|
+
* - **StaleWhileRevalidate**:清理时保留旧值 + 标记过期,后续请求零阻塞读到旧值,后台异步刷新
|
|
478
|
+
*
|
|
479
|
+
* 防击穿机制:
|
|
480
|
+
* - 缓存 miss 时通过 `[cache-sf]key` 原子锁(SETNX)选出"首飞"执行 fn
|
|
481
|
+
* - 其他并发请求轮询缓存,首飞写完后立即命中,避免重复执行 fn
|
|
482
|
+
*
|
|
483
|
+
* @example <caption>基础用法 — Purge 模式</caption>
|
|
484
|
+
* ```ts
|
|
485
|
+
* const result = await excuteWithCache({
|
|
486
|
+
* key: `user-${userId}`,
|
|
487
|
+
* autoClearTime: 60, // 60分钟后自动过期
|
|
488
|
+
* cacheNullValue: true, // null 也缓存(防穿透)
|
|
489
|
+
* }, () => loadUserFromDB(userId));
|
|
490
|
+
* ```
|
|
491
|
+
*
|
|
492
|
+
* @example <caption>Stale 模式 — 清理时保留旧值 + 后台刷新</caption>
|
|
493
|
+
* ```ts
|
|
494
|
+
* const result = await excuteWithCache({
|
|
495
|
+
* key: `match-${matchId}`,
|
|
496
|
+
* staleMode: CacheStaleMode.StaleWhileRevalidate,
|
|
497
|
+
* onCacheUpdated(value) {
|
|
498
|
+
* // 缓存刷新后触发:推送 WebSocket、预热下游等
|
|
499
|
+
* this.broadcast(value);
|
|
500
|
+
* },
|
|
501
|
+
* }, () => computeMatchData(matchId));
|
|
502
|
+
* ```
|
|
503
|
+
*
|
|
504
|
+
* @example <caption>cacheOnly — 只读缓存,不调用 fn</caption>
|
|
505
|
+
* ```ts
|
|
506
|
+
* const cached = await excuteWithCache({
|
|
507
|
+
* key: `config`,
|
|
508
|
+
* cacheOnly: true, // 没有缓存返回 null,不执行 fn
|
|
509
|
+
* }, () => loadConfig());
|
|
510
|
+
* ```
|
|
511
|
+
*
|
|
512
|
+
* @example <caption>forceRefresh — 跳过缓存直接执行 fn</caption>
|
|
513
|
+
* ```ts
|
|
514
|
+
* const fresh = await excuteWithCache({
|
|
515
|
+
* key: `match-${id}`,
|
|
516
|
+
* forceRefresh: true, // 忽略已有缓存,执行 fn 后写回(sa=0)
|
|
517
|
+
* }, () => computeFreshData(id));
|
|
518
|
+
* ```
|
|
519
|
+
*
|
|
520
|
+
* @example <caption>关联清除 — clearKey 级联清理</caption>
|
|
521
|
+
* ```ts
|
|
522
|
+
* const result = await excuteWithCache({
|
|
523
|
+
* key: `match-timer-${id}`,
|
|
524
|
+
* clearKey: ['match'], // clearMethodCache('match') 时级联清除
|
|
525
|
+
* }, () => computeTimer(id));
|
|
526
|
+
*
|
|
527
|
+
* // 清理所有关联了 'match' 的缓存
|
|
528
|
+
* await clearMethodCache('match');
|
|
529
|
+
* ```
|
|
530
|
+
*/
|
|
531
|
+
export async function excuteWithCache(config, fn) {
|
|
532
|
+
const callArgs = config.args ?? [];
|
|
533
|
+
const key = typeof config.key === 'function' ? config.key(...callArgs) : config.key;
|
|
534
|
+
const clearKey = typeof config.clearKey === 'function' ? config.clearKey(...callArgs) : config.clearKey;
|
|
535
|
+
const { autoClearTime, cacheNullValue, nullCacheTime, staleMode, onCacheUpdated, ctx, devid, cacheOnly, forceRefresh } = config;
|
|
380
536
|
const db = getRedisDB();
|
|
381
|
-
const cacheKey = CacheKey.value(
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
537
|
+
const cacheKey = CacheKey.value(key);
|
|
538
|
+
/**
|
|
539
|
+
* cache命中后:检查是否 stale→触发后台刷新;最终返回缓存值(新鲜或过期均可)。
|
|
540
|
+
* 单一 keep 变量名:`v` 代表当前缓存值(类型 CacheValue)。
|
|
541
|
+
*/
|
|
542
|
+
const readAndMaybeRefresh = async () => {
|
|
543
|
+
const raw = await db.get(cacheKey);
|
|
544
|
+
if (raw === null)
|
|
545
|
+
return null;
|
|
385
546
|
const v = deserializeCacheValue(raw);
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
cacheNullValue: opts.cacheNullValue,
|
|
394
|
-
nullCacheTime: opts.nullCacheTime,
|
|
395
|
-
staleMode: opts.staleMode
|
|
396
|
-
};
|
|
397
|
-
if (!globalStaleInflight.has(opts.key)) {
|
|
398
|
-
const refreshP = staleBackgroundRefresh(opts, setOpts).finally(() => {
|
|
399
|
-
globalStaleInflight.delete(opts.key);
|
|
547
|
+
if (!cacheOnly) {
|
|
548
|
+
// 有未消耗的 clear 计数(n>0)且刷新尚未运行 → 触发后台刷新
|
|
549
|
+
if ((v.n ?? 0) > 0 && !globalStaleInflight.has(key)) {
|
|
550
|
+
const refreshOpts = { key, clearKey, autoClearTime, cacheNullValue, nullCacheTime, staleMode, fn, ctx, args: callArgs, onCacheUpdated };
|
|
551
|
+
const refreshSetOpts = { clearKey, autoClearTime, cacheNullValue, nullCacheTime, staleMode };
|
|
552
|
+
const refreshP = staleBackgroundRefresh(refreshOpts, refreshSetOpts).finally(() => {
|
|
553
|
+
globalStaleInflight.delete(key);
|
|
400
554
|
});
|
|
401
|
-
globalStaleInflight.set(
|
|
555
|
+
globalStaleInflight.set(key, refreshP);
|
|
402
556
|
}
|
|
557
|
+
cacheLog(`${key} hit (${(v.n ?? 0) > 0 ? `stale n=${v.n}` : 'purge'})`);
|
|
403
558
|
}
|
|
404
559
|
else {
|
|
405
|
-
cacheLog(
|
|
560
|
+
cacheLog(`${key} hit (cacheOnly)`);
|
|
406
561
|
}
|
|
407
562
|
return JSON.parse(v.d);
|
|
408
|
-
}
|
|
409
|
-
cacheLog(`cache ${opts.key} miss!`);
|
|
410
|
-
const setOpts = {
|
|
411
|
-
clearKey: opts.clearKey,
|
|
412
|
-
autoClearTime: opts.autoClearTime,
|
|
413
|
-
cacheNullValue: opts.cacheNullValue,
|
|
414
|
-
nullCacheTime: opts.nullCacheTime,
|
|
415
|
-
staleMode: opts.staleMode
|
|
416
563
|
};
|
|
417
|
-
//
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
564
|
+
// forceRefresh:跳过缓存读取,直接走执行路径
|
|
565
|
+
if (!forceRefresh) {
|
|
566
|
+
const cached = await readAndMaybeRefresh();
|
|
567
|
+
if (cached !== null)
|
|
568
|
+
return cached;
|
|
569
|
+
if (cacheOnly)
|
|
570
|
+
return null;
|
|
571
|
+
cacheLog(`${key} miss!`);
|
|
572
|
+
}
|
|
573
|
+
else {
|
|
574
|
+
cacheLog(`${key} force refresh start`);
|
|
575
|
+
}
|
|
576
|
+
// forceRefresh 写回时强制 sa=0;其他情况按 staleMode/旧值自动判断
|
|
577
|
+
const setOpts = { clearKey, autoClearTime, cacheNullValue, nullCacheTime, staleMode, staleAllowedOverride: forceRefresh ? false : undefined };
|
|
578
|
+
const sfKey = CacheKey.sf(key);
|
|
421
579
|
let isLeader = false;
|
|
422
580
|
try {
|
|
423
581
|
const ok = await db.set(sfKey, '1', 'PX', SINGLEFLIGHT_LOCK_TTL_MS, 'NX');
|
|
424
582
|
isLeader = ok === 'OK';
|
|
425
583
|
}
|
|
426
|
-
catch
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
void opts.onCacheUpdated?.call(opts.ctx, result, ...(opts.args ?? []));
|
|
584
|
+
catch {
|
|
585
|
+
const result = await fn();
|
|
586
|
+
await setMethodCache({ key, ...setOpts, result }, devid);
|
|
587
|
+
void onCacheUpdated?.call(ctx, result, ...callArgs);
|
|
588
|
+
cacheLog(`${key} force refresh end(error-force)`);
|
|
432
589
|
return result;
|
|
433
590
|
}
|
|
434
591
|
if (isLeader) {
|
|
435
|
-
// 首飞:跑 fn、写缓存、释放首飞标记
|
|
436
592
|
try {
|
|
437
|
-
const result = await
|
|
438
|
-
await setMethodCache({ key
|
|
439
|
-
void
|
|
593
|
+
const result = await fn();
|
|
594
|
+
await setMethodCache({ key, ...setOpts, result }, devid);
|
|
595
|
+
void onCacheUpdated?.call(ctx, result, ...callArgs);
|
|
596
|
+
cacheLog(`${key} force refresh end(leader)`);
|
|
440
597
|
return result;
|
|
441
598
|
}
|
|
442
599
|
finally {
|
|
443
600
|
try {
|
|
444
601
|
await db.del(sfKey);
|
|
445
602
|
}
|
|
446
|
-
catch {
|
|
447
|
-
// 标记 TTL 会兜底,删失败不影响正确性
|
|
448
|
-
}
|
|
603
|
+
catch { /* TTL 兜底 */ }
|
|
449
604
|
}
|
|
450
605
|
}
|
|
451
|
-
// 等待者:循环只看缓存,不抢锁
|
|
452
606
|
const waitStart = Date.now();
|
|
453
607
|
while (true) {
|
|
454
608
|
await sleep(SINGLEFLIGHT_POLL_INTERVAL_MS);
|
|
455
609
|
const recheck = await db.get(cacheKey);
|
|
456
610
|
if (recheck !== null) {
|
|
457
|
-
|
|
458
|
-
|
|
611
|
+
const v = deserializeCacheValue(recheck);
|
|
612
|
+
// 拿到数据后二次检查:若仍 stale(刷新期间又被 clear),触发下一轮刷新
|
|
613
|
+
if ((v.n ?? 0) > 0 && !globalStaleInflight.has(key)) {
|
|
614
|
+
const refreshOpts = { key, clearKey, autoClearTime, cacheNullValue, nullCacheTime, staleMode, fn, ctx, args: callArgs, onCacheUpdated };
|
|
615
|
+
const refreshSetOpts = { clearKey, autoClearTime, cacheNullValue, nullCacheTime, staleMode };
|
|
616
|
+
globalStaleInflight.set(key, staleBackgroundRefresh(refreshOpts, refreshSetOpts).finally(() => {
|
|
617
|
+
globalStaleInflight.delete(key);
|
|
618
|
+
}));
|
|
619
|
+
}
|
|
620
|
+
cacheLog(`${key} hit after wait!`);
|
|
621
|
+
return JSON.parse(v.d);
|
|
459
622
|
}
|
|
460
|
-
// 首飞挂了(TTL 到期,sf 标记自动消失)但缓存仍然空——抢做下一任首飞
|
|
461
623
|
const leaderGone = await db.get(sfKey);
|
|
462
624
|
if (leaderGone === null) {
|
|
463
625
|
const taken = await db.set(sfKey, '1', 'PX', SINGLEFLIGHT_LOCK_TTL_MS, 'NX');
|
|
464
626
|
if (taken === 'OK') {
|
|
465
|
-
cacheLog(
|
|
627
|
+
cacheLog(`${key}: previous leader gone, taking over`);
|
|
466
628
|
try {
|
|
467
|
-
const result = await
|
|
468
|
-
await setMethodCache({ key
|
|
469
|
-
|
|
629
|
+
const result = await fn();
|
|
630
|
+
await setMethodCache({ key, ...setOpts, result }, devid);
|
|
631
|
+
cacheLog(`${key} force refresh end(leader gone)`);
|
|
632
|
+
void onCacheUpdated?.call(ctx, result, ...callArgs);
|
|
470
633
|
return result;
|
|
471
634
|
}
|
|
472
635
|
finally {
|
|
@@ -476,101 +639,143 @@ async function redisCacheCore(opts) {
|
|
|
476
639
|
catch { /* TTL 兜底 */ }
|
|
477
640
|
}
|
|
478
641
|
}
|
|
479
|
-
// 没抢到说明又有别人接手了,继续等
|
|
480
642
|
}
|
|
481
|
-
// 等待上限保底:极端情况下首飞和接班都不正常时,自己直接跑,宁可重复执行也不挂死
|
|
482
643
|
if (Date.now() - waitStart > SINGLEFLIGHT_MAX_WAIT_MS) {
|
|
483
|
-
globalThis[_LoggerService].warn?.(`cache ${
|
|
484
|
-
const result = await
|
|
485
|
-
await setMethodCache({ key
|
|
486
|
-
|
|
644
|
+
globalThis[_LoggerService].warn?.(`cache ${key}: wait timed out, running fn without lock`);
|
|
645
|
+
const result = await fn();
|
|
646
|
+
await setMethodCache({ key, ...setOpts, result }, devid);
|
|
647
|
+
cacheLog(`${key} force refresh end(recheck)`);
|
|
648
|
+
void onCacheUpdated?.call(ctx, result, ...callArgs);
|
|
487
649
|
return result;
|
|
488
650
|
}
|
|
489
651
|
}
|
|
490
652
|
}
|
|
491
|
-
/** @internal 唯一的执行入口(Memory 后端已移除) */
|
|
492
|
-
async function excuteCacheCore(opts) {
|
|
493
|
-
return await redisCacheCore(opts);
|
|
494
|
-
}
|
|
495
653
|
/**
|
|
496
|
-
*
|
|
497
|
-
*
|
|
498
|
-
*
|
|
499
|
-
*
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
*
|
|
518
|
-
*
|
|
519
|
-
*
|
|
520
|
-
*
|
|
521
|
-
*
|
|
654
|
+
* 方法装饰器:为方法添加 Redis 缓存能力。
|
|
655
|
+
*
|
|
656
|
+
* 工作流程:
|
|
657
|
+
* 1. 调用方法时先查 Redis 缓存
|
|
658
|
+
* 2. 命中 → 直接返回缓存值(若 stale 则触发后台异步刷新)
|
|
659
|
+
* 3. 未命中 → 原子锁(SETNX)选出"首飞"执行原方法 → 写缓存 → 返回
|
|
660
|
+
* 4. 其他并发请求轮询缓存,首飞写完后立即命中
|
|
661
|
+
*
|
|
662
|
+
* @example <caption>基础用法 — Purge 模式(默认)</caption>
|
|
663
|
+
* ```ts
|
|
664
|
+
* @MethodCache({
|
|
665
|
+
* key: (userId: string) => `user-${userId}`,
|
|
666
|
+
* autoClearTime: 60, // 60分钟后自动过期
|
|
667
|
+
* })
|
|
668
|
+
* async getUser(userId: string) {
|
|
669
|
+
* return await this.db.query(...);
|
|
670
|
+
* }
|
|
671
|
+
* ```
|
|
672
|
+
*
|
|
673
|
+
* @example <caption>Stale 模式 — 清理时保留旧值 + 后台刷新</caption>
|
|
674
|
+
* ```ts
|
|
675
|
+
* @MethodCache({
|
|
676
|
+
* key: (matchId: string) => `match-${matchId}`,
|
|
677
|
+
* staleMode: CacheStaleMode.StaleWhileRevalidate,
|
|
678
|
+
* onCacheUpdated(this: MyService, value) {
|
|
679
|
+
* // 缓存刷新后触发(this = 当前 service 实例)
|
|
680
|
+
* this.webSocket.broadcast(value);
|
|
681
|
+
* },
|
|
682
|
+
* })
|
|
683
|
+
* async getMatch(matchId: string) { ... }
|
|
684
|
+
*
|
|
685
|
+
* // 清理缓存 → 旧值保留,后台异步刷新
|
|
686
|
+
* await clearMethodCache(`match-${id}`);
|
|
687
|
+
* ```
|
|
688
|
+
*
|
|
689
|
+
* @example <caption>关联清除 — clearKey 级联清理</caption>
|
|
690
|
+
* ```ts
|
|
691
|
+
* @MethodCache({
|
|
692
|
+
* key: (matchId: string) => `match-timer-${matchId}`,
|
|
693
|
+
* clearKey: ['match'], // 声明依赖关系
|
|
694
|
+
* staleMode: CacheStaleMode.StaleWhileRevalidate,
|
|
695
|
+
* })
|
|
696
|
+
* async getMatchTimer(matchId: string) { ... }
|
|
697
|
+
*
|
|
698
|
+
* // 清理所有关联了 'match' 的缓存(包括 match-timer-*)
|
|
699
|
+
* await clearMethodCache('match');
|
|
700
|
+
* ```
|
|
701
|
+
*
|
|
702
|
+
* @example <caption>cacheOnly — 只读缓存,绝不调用原方法</caption>
|
|
703
|
+
* ```ts
|
|
704
|
+
* @MethodCache({
|
|
705
|
+
* key: 'app-config',
|
|
706
|
+
* cacheOnly: true, // 没有缓存返回 null
|
|
707
|
+
* })
|
|
708
|
+
* async loadConfig() { ... }
|
|
709
|
+
* ```
|
|
522
710
|
*/
|
|
523
711
|
export function MethodCache(config) {
|
|
524
712
|
return function (_target, _propertyKey, descriptor) {
|
|
525
713
|
const fn = descriptor.value;
|
|
526
714
|
descriptor.value = async function (...args) {
|
|
527
|
-
const key = typeof config.key === 'function' ? config.key.call(this, ...args) : config.key;
|
|
528
|
-
const clearKey = config.clearKey
|
|
529
|
-
? (typeof config.clearKey === 'function' ? config.clearKey.call(this, ...args) : config.clearKey)
|
|
530
|
-
: undefined;
|
|
531
715
|
const devid = config.clearWithSession && this.ctx && this.ctx.me && this.ctx.me.devid;
|
|
532
|
-
return await
|
|
533
|
-
key,
|
|
534
|
-
clearKey,
|
|
716
|
+
return await excuteWithCache({
|
|
717
|
+
key: config.key,
|
|
718
|
+
clearKey: config.clearKey,
|
|
535
719
|
autoClearTime: config.autoClearTime,
|
|
536
720
|
cacheNullValue: config.cacheNullValue,
|
|
537
721
|
nullCacheTime: config.nullCacheTime,
|
|
538
722
|
staleMode: config.staleMode,
|
|
539
723
|
onCacheUpdated: config.onCacheUpdated,
|
|
540
|
-
|
|
541
|
-
|
|
724
|
+
cacheOnly: config.cacheOnly,
|
|
725
|
+
forceRefresh: config.forceRefresh,
|
|
726
|
+
ctx: this,
|
|
542
727
|
devid,
|
|
543
|
-
|
|
544
|
-
});
|
|
728
|
+
args,
|
|
729
|
+
}, async () => await fn.call(this, ...args));
|
|
545
730
|
};
|
|
546
731
|
};
|
|
547
732
|
}
|
|
548
733
|
/**
|
|
549
|
-
*
|
|
550
|
-
* (如外部事件驱动的数据变更、手动刷新等)。
|
|
734
|
+
* 外部直接写入缓存(绕过 @MethodCache 装饰器)。
|
|
551
735
|
*
|
|
552
|
-
*
|
|
736
|
+
* 与 @MethodCache 共享同一套存储格式 `{ t, d, sa, n }`,写入的缓存可被
|
|
737
|
+
* @MethodCache 装饰的方法正常读取(包括 stale 判断、计数器等)。
|
|
553
738
|
*
|
|
554
|
-
*
|
|
555
|
-
*
|
|
556
|
-
*
|
|
739
|
+
* 适用场景:
|
|
740
|
+
* - 外部事件驱动的数据变更(如 MQ 消息、定时任务)
|
|
741
|
+
* - 手动刷新缓存(如管理后台"清除并重建"按钮)
|
|
742
|
+
* - 跨服务同步缓存状态
|
|
557
743
|
*
|
|
558
|
-
* @example
|
|
744
|
+
* @example <caption>基础用法 — 写入缓存</caption>
|
|
559
745
|
* ```ts
|
|
560
|
-
*
|
|
561
|
-
*
|
|
746
|
+
* await setCache('match-timer-123', timer, {
|
|
747
|
+
* autoClearTime: 120, // 120分钟后过期
|
|
748
|
+
* });
|
|
749
|
+
* ```
|
|
562
750
|
*
|
|
563
|
-
*
|
|
751
|
+
* @example <caption>关联清除 — 注册 clearKey 级联关系</caption>
|
|
752
|
+
* ```ts
|
|
564
753
|
* await setCache('match-timer-123', timer, {
|
|
565
754
|
* autoClearTime: 120,
|
|
566
|
-
*
|
|
567
|
-
*
|
|
568
|
-
*
|
|
755
|
+
* clearKey: ['match'], // clearMethodCache('match') 时级联清除
|
|
756
|
+
* });
|
|
757
|
+
*
|
|
758
|
+
* // 函数式 clearKey(参数来自 options.args)
|
|
759
|
+
* await setCache('match-timer-123', timer, {
|
|
760
|
+
* clearKey: (matchId) => [`match-${matchId}`],
|
|
761
|
+
* args: [matchId],
|
|
762
|
+
* });
|
|
763
|
+
* ```
|
|
764
|
+
*
|
|
765
|
+
* @example <caption>回调 — 缓存写入后触发通知</caption>
|
|
766
|
+
* ```ts
|
|
767
|
+
* await setCache('match-timer-123', timer, {
|
|
768
|
+
* autoClearTime: 120,
|
|
769
|
+
* ctx: this,
|
|
770
|
+
* args: [matchId],
|
|
771
|
+
* onCacheUpdated(value, matchId) {
|
|
772
|
+
* this.webSocket.sendRoom(matchId, { action: 'timer', param: value });
|
|
773
|
+
* },
|
|
569
774
|
* });
|
|
570
775
|
* ```
|
|
571
776
|
*/
|
|
572
777
|
export async function setCache(cacheKey, value, options = {}) {
|
|
573
|
-
const { autoClearTime, cacheNullValue = false, onCacheUpdated, ctx, args = [] } = options;
|
|
778
|
+
const { autoClearTime, cacheNullValue = false, clearKey, onCacheUpdated, ctx, args = [] } = options;
|
|
574
779
|
const isNullish = value === null || value === undefined;
|
|
575
780
|
if (isNullish && !cacheNullValue) {
|
|
576
781
|
return;
|
|
@@ -580,14 +785,24 @@ export async function setCache(cacheKey, value, options = {}) {
|
|
|
580
785
|
const db = getRedisDB();
|
|
581
786
|
// 保留旧 sa:沿用已有标志(防止外部直写清掉 sa=1,导致 clearMethodCache 变 purge)
|
|
582
787
|
const existingSa = await db.get(CacheKey.value(cacheKey)).then(r => r ? deserializeCacheValue(r).sa : undefined);
|
|
583
|
-
const payload = serializeCacheValue(dataStr, timestamp,
|
|
788
|
+
const payload = serializeCacheValue(dataStr, timestamp, !!existingSa);
|
|
584
789
|
if (autoClearTime) {
|
|
585
790
|
await db.set(CacheKey.value(cacheKey), payload, 'EX', autoClearTime * 60);
|
|
586
791
|
}
|
|
587
792
|
else {
|
|
588
793
|
await db.set(CacheKey.value(cacheKey), payload);
|
|
589
794
|
}
|
|
795
|
+
// 注册关联清除关系(与 setMethodCache 一致)
|
|
796
|
+
if (clearKey) {
|
|
797
|
+
const clearKeys = typeof clearKey === 'function' ? clearKey(...args) : clearKey;
|
|
798
|
+
if (clearKeys && clearKeys.length > 0) {
|
|
799
|
+
for (const clear of clearKeys) {
|
|
800
|
+
await db.sadd(CacheKey.parent(clear), cacheKey);
|
|
801
|
+
await db.sadd(CacheKey.child(cacheKey), clear);
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
}
|
|
590
805
|
// 触发回调
|
|
591
806
|
void onCacheUpdated?.call(ctx, value, ...args);
|
|
592
|
-
cacheLog(
|
|
807
|
+
cacheLog(`${cacheKey} externally set`);
|
|
593
808
|
}
|