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