baja-lite 1.8.9 → 1.8.11
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 +122 -11
- package/cache.js +316 -238
- package/const/symbols.d.ts +0 -2
- package/const/symbols.js +0 -2
- package/const/types.d.ts +17 -1
- package/db/dao/mysql.js +32 -59
- package/db/dao/postgresql.js +36 -64
- package/db/dao/sqlite-remote.js +26 -41
- package/db/dao/sqlite.js +35 -53
- package/db/service.js +57 -57
- package/db/sql-template.js +13 -12
- package/db/stream-query.js +34 -34
- package/logger.d.ts +55 -28
- package/logger.js +100 -19
- package/package.json +1 -1
- package/sqlite.js +6 -6
package/cache.js
CHANGED
|
@@ -1,8 +1,86 @@
|
|
|
1
1
|
import { DBType } from 'baja-lite-field';
|
|
2
2
|
import { sleep } from './fn.js';
|
|
3
3
|
import { Throw } from './error.js';
|
|
4
|
-
import { _dao, _EventBus,
|
|
5
|
-
|
|
4
|
+
import { _dao, _EventBus, _LoggerService, _primaryDB, } from './const/symbols.js';
|
|
5
|
+
/** @internal 输出缓存分类日志(仅当 setLogLevels 包含 'cache' 时生效) */
|
|
6
|
+
function cacheLog(message, ...params) {
|
|
7
|
+
const logger = globalThis[_LoggerService];
|
|
8
|
+
// pino-pretty 的 messageFormat 会根据 level=24 自动添加 [CACHE] 前缀
|
|
9
|
+
logger.debugCategory?.('cache', message, ...params);
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* @internal 全局 stale 刷新 Promise 映射。
|
|
13
|
+
* key = 缓存 key(不含前缀),value = 正在进行的 stale 刷新 Promise。
|
|
14
|
+
* @MethodCache 内部的 staleBackgroundRefresh 通过这个映射注册刷新 Promise,
|
|
15
|
+
* 同一 key 的并发 stale 命中复用同一个 Promise,避免重复执行 fn()。
|
|
16
|
+
* Redis 后端下此映射是每个进程本地的;但因为 [cache-sf]key 在 Redis 里全局可见,
|
|
17
|
+
* 所以跨进程的 single-flight 仍然有效。
|
|
18
|
+
*/
|
|
19
|
+
const globalStaleInflight = new Map();
|
|
20
|
+
/**
|
|
21
|
+
* @internal [cache] 前缀下的子 key 命名全部收拢到这里,避免散落在各处 magic string。
|
|
22
|
+
*
|
|
23
|
+
* 命名规范:
|
|
24
|
+
* - [cache]key 缓存值(JSON: { v, d, s } — v=版本 d=数据 s=是否 stale)
|
|
25
|
+
* - [cache-sf]key single-flight 首飞标记(SETNX 抢锁用)
|
|
26
|
+
* - [cache-meta]key stale-allowed 标记(带 TTL,与缓存同生命周期)
|
|
27
|
+
* - [cache-parent]key / [cache-child]key 关联清除的 parent↔child 双向 set
|
|
28
|
+
*/
|
|
29
|
+
const CacheKey = {
|
|
30
|
+
value: (k) => `[cache]${k}`,
|
|
31
|
+
sf: (k) => `[cache-sf]${k}`,
|
|
32
|
+
meta: (k) => `[cache-meta]${k}`,
|
|
33
|
+
parent: (k) => `[cache-parent]${k}`,
|
|
34
|
+
child: (k) => `[cache-child]${k}`,
|
|
35
|
+
};
|
|
36
|
+
/** @internal 序列化缓存值 */
|
|
37
|
+
function serializeCacheValue(data, timestamp, stale = false, staleAllowed = false) {
|
|
38
|
+
const v = { t: timestamp, d: data };
|
|
39
|
+
if (stale)
|
|
40
|
+
v.s = 1;
|
|
41
|
+
if (staleAllowed)
|
|
42
|
+
v.sa = 1;
|
|
43
|
+
return JSON.stringify(v);
|
|
44
|
+
}
|
|
45
|
+
/** @internal 反序列化缓存值 */
|
|
46
|
+
function deserializeCacheValue(raw) {
|
|
47
|
+
try {
|
|
48
|
+
const v = JSON.parse(raw);
|
|
49
|
+
return { ...v, isNullish: v.d === 'null' || v.d === 'undefined' };
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
// 旧格式兼容:纯字符串(无 JSON 包装)
|
|
53
|
+
return { t: 0, d: raw, isNullish: raw === 'null' || raw === 'undefined' };
|
|
54
|
+
}
|
|
55
|
+
}
|
|
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
|
+
/**
|
|
63
|
+
* 缓存被清理时的行为策略。
|
|
64
|
+
* 由 @MethodCache 的 `staleMode` 配置决定,clearMethodCache 依此自动选择清理方式。
|
|
65
|
+
*/
|
|
66
|
+
export var CacheStaleMode;
|
|
67
|
+
(function (CacheStaleMode) {
|
|
68
|
+
/**
|
|
69
|
+
* 默认。清理时直接删除缓存值,下次请求触发 single-flight 重建。
|
|
70
|
+
* 优点:数据强一致,不会读到旧值。
|
|
71
|
+
* 缺点:重建期间所有请求被阻塞,直到 fn() 执行完毕。
|
|
72
|
+
*/
|
|
73
|
+
CacheStaleMode[CacheStaleMode["Purge"] = 0] = "Purge";
|
|
74
|
+
/**
|
|
75
|
+
* 清理时保留旧值并标记为过期(stale),同时设一个 stale 窗口(默认 30s)。
|
|
76
|
+
* 窗口内:请求仍读到旧值(零阻塞),同时后台 single-flight 异步刷新。
|
|
77
|
+
* 窗口过后:stale 标记自动消失,退化回 Purge 行为。
|
|
78
|
+
*
|
|
79
|
+
* 适用场景:读多写少、能容忍秒级旧数据、fn() 执行代价高。
|
|
80
|
+
* 注意:开启后业务层可能收到乱序数据,需自行处理(如按时间戳丢弃过期包)。
|
|
81
|
+
*/
|
|
82
|
+
CacheStaleMode[CacheStaleMode["StaleWhileRevalidate"] = 1] = "StaleWhileRevalidate";
|
|
83
|
+
})(CacheStaleMode || (CacheStaleMode = {}));
|
|
6
84
|
export function getRedisDB(db) {
|
|
7
85
|
const rd = globalThis[_dao][DBType.Redis][db ?? _primaryDB];
|
|
8
86
|
Throw.if(!rd, 'not found redis!');
|
|
@@ -37,7 +115,7 @@ export async function GetRedisLock(key, lockMaxActive) {
|
|
|
37
115
|
lastError = er;
|
|
38
116
|
// 指数退避,避免 redis 抖动时打死服务
|
|
39
117
|
const delay = Math.min(GET_REDIS_LOCK_BASE_DELAY * Math.pow(2, attempt), 1000);
|
|
40
|
-
globalThis[_LoggerService].
|
|
118
|
+
globalThis[_LoggerService].debugCategory?.('sql', `GetRedisLock ${key} attempt ${attempt + 1} failed: ${er?.message}, retry after ${delay}ms`);
|
|
41
119
|
await sleep(delay);
|
|
42
120
|
}
|
|
43
121
|
finally {
|
|
@@ -63,24 +141,24 @@ export async function excuteWithLock(config, fn__) {
|
|
|
63
141
|
const lock = await GetRedisLock(key, config.lockMaxActive);
|
|
64
142
|
if (lock === false) {
|
|
65
143
|
if (config.lockWait !== false && ((config.lockMaxWaitTime ?? 0) === 0 || (wait_time + (config.lockRetryInterval ?? 100)) <= (config.lockMaxWaitTime ?? 0))) {
|
|
66
|
-
globalThis[_LoggerService].
|
|
144
|
+
globalThis[_LoggerService].debugCategory?.('sql', `get lock ${key} fail, retry after ${config.lockRetryInterval ?? 100}ms...`);
|
|
67
145
|
await sleep(config.lockRetryInterval ?? 100);
|
|
68
146
|
wait_time += (config.lockRetryInterval ?? 100);
|
|
69
147
|
return await fn();
|
|
70
148
|
}
|
|
71
149
|
else {
|
|
72
|
-
globalThis[_LoggerService].
|
|
150
|
+
globalThis[_LoggerService].debugCategory?.('sql', `get lock ${key} fail`);
|
|
73
151
|
throw new Error(config.errorMessage || `get lock fail: ${key}`);
|
|
74
152
|
}
|
|
75
153
|
}
|
|
76
154
|
else {
|
|
77
|
-
globalThis[_LoggerService].
|
|
155
|
+
globalThis[_LoggerService].debugCategory?.('sql', `get lock ${key} ok!`);
|
|
78
156
|
await db.pexpire(key, config.lockMaxTime ?? 60000);
|
|
79
157
|
try {
|
|
80
158
|
return await fn__();
|
|
81
159
|
}
|
|
82
160
|
finally {
|
|
83
|
-
globalThis[_LoggerService].
|
|
161
|
+
globalThis[_LoggerService].debugCategory?.('sql', `unlock ${key} ok!`);
|
|
84
162
|
await db.decr(key);
|
|
85
163
|
}
|
|
86
164
|
}
|
|
@@ -105,114 +183,10 @@ export function MethodLock(config) {
|
|
|
105
183
|
};
|
|
106
184
|
};
|
|
107
185
|
}
|
|
108
|
-
/** 全局 LRU 上限。可在 boot 时通过 `globalThis[_GlobalSqlOption].memCacheMaxSize` 覆盖。 */
|
|
109
|
-
const MEMORY_CACHE_DEFAULT_MAX_SIZE = 10000;
|
|
110
|
-
function getMemCache() {
|
|
111
|
-
let m = globalThis[_memCache];
|
|
112
|
-
if (!m) {
|
|
113
|
-
m = new Map();
|
|
114
|
-
globalThis[_memCache] = m;
|
|
115
|
-
}
|
|
116
|
-
return m;
|
|
117
|
-
}
|
|
118
|
-
function getMemInflight() {
|
|
119
|
-
let m = globalThis[_memInflight];
|
|
120
|
-
if (!m) {
|
|
121
|
-
m = new Map();
|
|
122
|
-
globalThis[_memInflight] = m;
|
|
123
|
-
}
|
|
124
|
-
return m;
|
|
125
|
-
}
|
|
126
|
-
function getMemCacheMaxSize() {
|
|
127
|
-
return globalThis[_GlobalSqlOption]?.memCacheMaxSize ?? MEMORY_CACHE_DEFAULT_MAX_SIZE;
|
|
128
|
-
}
|
|
129
|
-
/** 内存版的 parent→children 关联表,对应 redis 的 [cache-parent]* / [cache-child]* set。 */
|
|
130
|
-
const memParentChild = {
|
|
131
|
-
parentToChildren: new Map(),
|
|
132
|
-
childToParents: new Map(),
|
|
133
|
-
link(parents, child) {
|
|
134
|
-
for (const p of parents) {
|
|
135
|
-
let s = this.parentToChildren.get(p);
|
|
136
|
-
if (!s) {
|
|
137
|
-
s = new Set();
|
|
138
|
-
this.parentToChildren.set(p, s);
|
|
139
|
-
}
|
|
140
|
-
s.add(child);
|
|
141
|
-
let r = this.childToParents.get(child);
|
|
142
|
-
if (!r) {
|
|
143
|
-
r = new Set();
|
|
144
|
-
this.childToParents.set(child, r);
|
|
145
|
-
}
|
|
146
|
-
r.add(p);
|
|
147
|
-
}
|
|
148
|
-
},
|
|
149
|
-
unlinkChild(child) {
|
|
150
|
-
const parents = this.childToParents.get(child);
|
|
151
|
-
if (!parents)
|
|
152
|
-
return;
|
|
153
|
-
for (const p of parents) {
|
|
154
|
-
this.parentToChildren.get(p)?.delete(child);
|
|
155
|
-
}
|
|
156
|
-
this.childToParents.delete(child);
|
|
157
|
-
},
|
|
158
|
-
drainParent(parent) {
|
|
159
|
-
const children = this.parentToChildren.get(parent);
|
|
160
|
-
if (!children)
|
|
161
|
-
return [];
|
|
162
|
-
const list = [...children];
|
|
163
|
-
for (const c of list) {
|
|
164
|
-
this.childToParents.get(c)?.delete(parent);
|
|
165
|
-
}
|
|
166
|
-
this.parentToChildren.delete(parent);
|
|
167
|
-
return list;
|
|
168
|
-
}
|
|
169
|
-
};
|
|
170
186
|
/**
|
|
171
|
-
*
|
|
172
|
-
*
|
|
173
|
-
* 超过上限时从头部(最少最近使用)开始淘汰。
|
|
174
|
-
* - `cacheNullValue=true` 时,null/undefined 也会以 null 形式写入(防穿透)。
|
|
187
|
+
* 内存 LRU 条目,存储格式与 Redis 一致(CacheValue)。
|
|
188
|
+
* `expiresAt = 0` 表示永不过期。
|
|
175
189
|
*/
|
|
176
|
-
function memSet(key, result, config) {
|
|
177
|
-
const isNullish = result === null || result === undefined;
|
|
178
|
-
if (isNullish && !config.cacheNullValue) {
|
|
179
|
-
return;
|
|
180
|
-
}
|
|
181
|
-
const ttlMinutes = isNullish ? (config.nullCacheTime ?? config.autoClearTime) : config.autoClearTime;
|
|
182
|
-
const expiresAt = ttlMinutes ? Date.now() + ttlMinutes * 60000 : 0;
|
|
183
|
-
const value = isNullish ? null : result;
|
|
184
|
-
const cache = getMemCache();
|
|
185
|
-
if (cache.has(key))
|
|
186
|
-
cache.delete(key);
|
|
187
|
-
cache.set(key, { value, expiresAt });
|
|
188
|
-
const max = getMemCacheMaxSize();
|
|
189
|
-
while (cache.size > max) {
|
|
190
|
-
const oldest = cache.keys().next().value;
|
|
191
|
-
if (oldest === undefined)
|
|
192
|
-
break;
|
|
193
|
-
cache.delete(oldest);
|
|
194
|
-
memParentChild.unlinkChild(oldest);
|
|
195
|
-
}
|
|
196
|
-
if (config.clearKey && config.clearKey.length > 0) {
|
|
197
|
-
memParentChild.link(config.clearKey, key);
|
|
198
|
-
}
|
|
199
|
-
globalThis[_LoggerService].debug?.(`memcache ${key} seted${isNullish ? ' (null)' : ''}!`);
|
|
200
|
-
}
|
|
201
|
-
/** 读内存缓存,lazy expire。命中后挪到 LRU 尾部。 */
|
|
202
|
-
function memGet(key) {
|
|
203
|
-
const cache = getMemCache();
|
|
204
|
-
const entry = cache.get(key);
|
|
205
|
-
if (!entry)
|
|
206
|
-
return { hit: false };
|
|
207
|
-
if (entry.expiresAt !== 0 && entry.expiresAt <= Date.now()) {
|
|
208
|
-
cache.delete(key);
|
|
209
|
-
memParentChild.unlinkChild(key);
|
|
210
|
-
return { hit: false };
|
|
211
|
-
}
|
|
212
|
-
cache.delete(key);
|
|
213
|
-
cache.set(key, entry);
|
|
214
|
-
return { hit: true, value: entry.value };
|
|
215
|
-
}
|
|
216
190
|
/** 设置方法缓存 */
|
|
217
191
|
async function setMethodCache(config, devid) {
|
|
218
192
|
const db = getRedisDB();
|
|
@@ -222,7 +196,7 @@ async function setMethodCache(config, devid) {
|
|
|
222
196
|
return;
|
|
223
197
|
}
|
|
224
198
|
// 统一序列化:undefined 不是合法 JSON,归一化成 null。
|
|
225
|
-
const
|
|
199
|
+
const dataStr = isNullish ? 'null' : JSON.stringify(config.result);
|
|
226
200
|
// 负缓存通常用更短 TTL,避免业务恢复后还长时间返回旧的 null。
|
|
227
201
|
const ttl = isNullish
|
|
228
202
|
? (config.nullCacheTime ?? config.autoClearTime)
|
|
@@ -230,100 +204,122 @@ async function setMethodCache(config, devid) {
|
|
|
230
204
|
// 映射关系存放(负缓存也参与关联清除,因为外部清缓存的语义不区分正负)
|
|
231
205
|
if (config.clearKey && config.clearKey.length > 0) {
|
|
232
206
|
for (const clear of config.clearKey) {
|
|
233
|
-
await db.sadd(
|
|
234
|
-
await db.sadd(
|
|
207
|
+
await db.sadd(CacheKey.parent(clear), config.key);
|
|
208
|
+
await db.sadd(CacheKey.child(config.key), clear);
|
|
235
209
|
}
|
|
236
210
|
}
|
|
211
|
+
// 写入缓存值(带时间戳 + staleAllowed 标志)。
|
|
212
|
+
// 保留旧 sa:只有显式 StaleWhileRevalidate 才设 1,否则沿用旧值(防止覆盖已有的 sa=1)。
|
|
213
|
+
const timestamp = Date.now();
|
|
214
|
+
const existingSa = await db.get(CacheKey.value(config.key)).then(r => r ? deserializeCacheValue(r).sa : undefined);
|
|
215
|
+
const staleAllowed = config.staleMode === CacheStaleMode.StaleWhileRevalidate ? true : (existingSa ?? false);
|
|
216
|
+
const payload = serializeCacheValue(dataStr, timestamp, false, staleAllowed);
|
|
237
217
|
if (ttl) { // 自动清空
|
|
238
|
-
await db.set(
|
|
239
|
-
|
|
240
|
-
// 订阅:清空 clear list —— 同一 key 在缓存生命周期内只注册一次监听器,
|
|
241
|
-
// 否则每次 miss 都会 .on 一次,clearMethodCache 触发时回调会被执行 N 遍,
|
|
242
|
-
// 长期运行造成监听器泄漏。
|
|
243
|
-
if (config.clearKey && config.clearKey.length > 0) {
|
|
244
|
-
const event = `[cache]${config.key}`;
|
|
245
|
-
if (globalThis[_EventBus].listenerCount(event) === 0) {
|
|
246
|
-
globalThis[_EventBus].on(event, async (key) => {
|
|
247
|
-
await clearChild(key, true);
|
|
248
|
-
globalThis[_LoggerService].debug?.(`cache ${key} clear by key!`);
|
|
249
|
-
});
|
|
250
|
-
}
|
|
251
|
-
}
|
|
218
|
+
await db.set(CacheKey.value(config.key), payload, 'EX', ttl * 60);
|
|
219
|
+
cacheLog(`cache ${config.key} seted t${timestamp}${isNullish ? ' (null)' : ''}!`);
|
|
252
220
|
}
|
|
253
221
|
else {
|
|
254
|
-
await db.set(
|
|
255
|
-
|
|
222
|
+
await db.set(CacheKey.value(config.key), payload);
|
|
223
|
+
cacheLog(`cache ${config.key} seted t${timestamp}${isNullish ? ' (null)' : ''}!`);
|
|
224
|
+
}
|
|
225
|
+
// staleAllowed 已存储在缓存值内部(CacheValue.sa),无需单独的 meta key
|
|
226
|
+
// 删除旧格式残留(兼容升级)
|
|
227
|
+
await db.del(CacheKey.meta(config.key));
|
|
228
|
+
// 订阅:清空 clear list —— 同一 key 在缓存生命周期内只注册一次监听器,
|
|
229
|
+
// 否则每次 miss 都会 .on 一次,clearMethodCache 触发时回调会被执行 N 遍,
|
|
230
|
+
// 长期运行造成监听器泄漏。
|
|
231
|
+
if (config.clearKey && config.clearKey.length > 0) {
|
|
232
|
+
const event = CacheKey.value(config.key);
|
|
233
|
+
if (globalThis[_EventBus].listenerCount(event) === 0) {
|
|
234
|
+
globalThis[_EventBus].on(event, async (key) => {
|
|
235
|
+
await clearCacheKey(key);
|
|
236
|
+
cacheLog(`cache ${key} clear by key!`);
|
|
237
|
+
});
|
|
238
|
+
}
|
|
256
239
|
}
|
|
257
240
|
if (devid) {
|
|
258
241
|
// 订阅:清空 clear list —— 同样去重,避免每次 miss 都重复注册。
|
|
259
242
|
const event = `user-${devid}`;
|
|
260
243
|
if (globalThis[_EventBus].listenerCount(event) === 0) {
|
|
261
|
-
globalThis[_EventBus].on(event, async
|
|
262
|
-
await
|
|
263
|
-
|
|
244
|
+
globalThis[_EventBus].on(event, async (key) => {
|
|
245
|
+
await clearCacheKey(key);
|
|
246
|
+
cacheLog(`cache ${key} clear by devid!`);
|
|
264
247
|
});
|
|
265
248
|
}
|
|
266
249
|
}
|
|
267
250
|
}
|
|
268
251
|
/**
|
|
269
|
-
*
|
|
270
|
-
* -
|
|
271
|
-
* -
|
|
252
|
+
* 单个缓存 key 的 stale-or-purge 决策与执行。
|
|
253
|
+
* - CacheValue.sa=1 → 标记 s=1(保留缓存值,StaleWhileRevalidate)
|
|
254
|
+
* - 否则 → 直接删缓存值(Purge)
|
|
255
|
+
*
|
|
256
|
+
* 无论哪种路径,parent↔child 关联关系都清理。
|
|
257
|
+
*
|
|
258
|
+
* @returns 受影响的 children 列表(parent 路径会返回所有被级联清理的 child key)
|
|
272
259
|
*/
|
|
273
|
-
|
|
274
|
-
// 内存侧
|
|
275
|
-
const cache = getMemCache();
|
|
276
|
-
// 1. 如果 key 是 parent,把它关联的所有 children 都清掉
|
|
277
|
-
const childrenFromParent = memParentChild.drainParent(key);
|
|
278
|
-
for (const child of childrenFromParent) {
|
|
279
|
-
cache.delete(child);
|
|
280
|
-
memParentChild.unlinkChild(child);
|
|
281
|
-
}
|
|
282
|
-
// 2. 如果 key 本身是个缓存条目,直接清,并清掉它的反向链接
|
|
283
|
-
if (cache.has(key)) {
|
|
284
|
-
cache.delete(key);
|
|
285
|
-
}
|
|
286
|
-
memParentChild.unlinkChild(key);
|
|
287
|
-
// redis 侧:没配 redis 时跳过
|
|
288
|
-
const redisDao = globalThis[_dao]?.[DBType.Redis]?.[_primaryDB];
|
|
289
|
-
if (!redisDao)
|
|
290
|
-
return;
|
|
260
|
+
async function clearCacheKey(key) {
|
|
291
261
|
const db = getRedisDB();
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
262
|
+
const affectedChildren = [];
|
|
263
|
+
// 1. 先处理 parent 级联:把关联的 children 全部清掉
|
|
264
|
+
const parentType = await db.type(CacheKey.parent(key));
|
|
265
|
+
if (parentType === 'set') {
|
|
266
|
+
const childKeys = await db.smembers(CacheKey.parent(key));
|
|
267
|
+
for (const child of childKeys) {
|
|
268
|
+
cacheLog(`cache ${child} cleared via parent ${key}!`);
|
|
269
|
+
await clearCacheKey(child);
|
|
270
|
+
affectedChildren.push(child);
|
|
271
|
+
}
|
|
272
|
+
await db.del(CacheKey.parent(key));
|
|
295
273
|
}
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
274
|
+
// 2. 判断 stale-allowed:查缓存值内部的 sa 标志
|
|
275
|
+
const valueRaw = await db.get(CacheKey.value(key));
|
|
276
|
+
const v = valueRaw ? deserializeCacheValue(valueRaw) : null;
|
|
277
|
+
if (v?.sa) {
|
|
278
|
+
// stale-allowed:保留缓存值,标记 s=1,同时保留 sa=1(防止下次 clear 变成 purge)
|
|
279
|
+
const newPayload = serializeCacheValue(v.d, v.t, true, true);
|
|
280
|
+
const ttlSec = await db.ttl(CacheKey.value(key));
|
|
281
|
+
if (ttlSec > 0) {
|
|
282
|
+
await db.set(CacheKey.value(key), newPayload, 'EX', ttlSec);
|
|
283
|
+
}
|
|
284
|
+
else {
|
|
285
|
+
await db.set(CacheKey.value(key), newPayload);
|
|
286
|
+
}
|
|
287
|
+
cacheLog(`cache ${key} marked stale (t${v.t})`);
|
|
299
288
|
}
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
await db.del(`[cache]${key}`);
|
|
289
|
+
else {
|
|
290
|
+
// Purge:直接删缓存值
|
|
291
|
+
await db.del(CacheKey.value(key));
|
|
292
|
+
cacheLog(`cache ${key} purged!`);
|
|
305
293
|
}
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
294
|
+
// 清理旧格式残留
|
|
295
|
+
await db.del(CacheKey.meta(key));
|
|
296
|
+
// 3. 清理 child→parent 反向关联
|
|
297
|
+
const childType = await db.type(CacheKey.child(key));
|
|
298
|
+
if (childType === 'set') {
|
|
299
|
+
const parentKeys = await db.smembers(CacheKey.child(key));
|
|
300
|
+
for (const pk of parentKeys) {
|
|
301
|
+
const t = await db.type(CacheKey.parent(pk));
|
|
302
|
+
if (t === 'set') {
|
|
303
|
+
await db.srem(CacheKey.parent(pk), key);
|
|
313
304
|
}
|
|
314
305
|
}
|
|
315
|
-
await db.del(
|
|
306
|
+
await db.del(CacheKey.child(key));
|
|
316
307
|
}
|
|
308
|
+
return affectedChildren;
|
|
317
309
|
}
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
310
|
+
/**
|
|
311
|
+
* 清空方法缓存。
|
|
312
|
+
* - redis 侧只在配置了 redis 时才操作;没配 redis 时安静跳过,不抛错。
|
|
313
|
+
* - 关联清除(clearKey)生效。
|
|
314
|
+
* - 被清理的 key 如果启用了 StaleWhileRevalidate(CacheValue.sa=1),会保留旧值并标记 stale。
|
|
315
|
+
*
|
|
316
|
+
* @param key 要清理的缓存 key 或 parent key
|
|
317
|
+
*/
|
|
318
|
+
export async function clearMethodCache(key) {
|
|
319
|
+
const redisDao = globalThis[_dao]?.[DBType.Redis]?.[_primaryDB];
|
|
320
|
+
if (!redisDao)
|
|
321
|
+
return;
|
|
322
|
+
await clearCacheKey(key);
|
|
327
323
|
}
|
|
328
324
|
/**
|
|
329
325
|
* Redis 缓存执行:先查缓存 → miss 后单飞抢锁执行 → 其它等待者只盯缓存、不抢锁。
|
|
@@ -336,30 +332,92 @@ async function clearParent(clearKey) {
|
|
|
336
332
|
* 命中判断用 `cached !== null`(ioredis 在 key 不存在时返回 `null`),
|
|
337
333
|
* 这样 `cacheNullValue=true` 时存进去的字面量 `'null'` 也会算作命中,达到防穿透效果。
|
|
338
334
|
*
|
|
335
|
+
* 命中时若发现 stale 标志(CacheValue.s=1),说明该值是"已被声明过期但保留的旧值",
|
|
336
|
+
* 此时仍返回旧值(零阻塞),同时触发后台 single-flight 异步刷新。
|
|
337
|
+
* 这样 clearMethodCache 后的首次读取不会被阻塞,后续读取将命中刷新后的新值。
|
|
338
|
+
*
|
|
339
339
|
* redis 不可用或 SETNX 异常时退化为无锁直跑,保留可用性。
|
|
340
340
|
*/
|
|
341
341
|
const SINGLEFLIGHT_LOCK_TTL_MS = 30000; // 首飞标记 TTL,兜底首飞挂掉的情况
|
|
342
342
|
const SINGLEFLIGHT_POLL_INTERVAL_MS = 50; // 等待者轮询缓存的间隔
|
|
343
343
|
const SINGLEFLIGHT_MAX_WAIT_MS = 30000; // 等待者最长等多久;超时就自己跑 fn(保底,不应该常发生)
|
|
344
|
+
/**
|
|
345
|
+
* 后台异步刷新:fire-and-forget,不阻塞调用方。
|
|
346
|
+
* 只有抢到首飞标记的实例才会执行 fn() 并写缓存;其它实例直接放弃。
|
|
347
|
+
* 刷新完成后删除 stale 标记,后续读取将命中新值。
|
|
348
|
+
* 如果配置了 onCacheUpdated 回调,调用时传入 ctx(方法的 this 上下文)。
|
|
349
|
+
*/
|
|
350
|
+
async function staleBackgroundRefresh(opts, setOpts) {
|
|
351
|
+
const db = getRedisDB();
|
|
352
|
+
const sfKey = CacheKey.sf(opts.key);
|
|
353
|
+
try {
|
|
354
|
+
const ok = await db.set(sfKey, '1', 'PX', SINGLEFLIGHT_LOCK_TTL_MS, 'NX');
|
|
355
|
+
if (ok !== 'OK')
|
|
356
|
+
return; // 别人已经在刷新了,自己放弃
|
|
357
|
+
}
|
|
358
|
+
catch {
|
|
359
|
+
return; // redis 出错,放弃刷新,下次读取再试
|
|
360
|
+
}
|
|
361
|
+
try {
|
|
362
|
+
const result = await opts.fn();
|
|
363
|
+
// 写新值(setMethodCache 内部会清除 stale 标志 s=1)
|
|
364
|
+
await setMethodCache({ key: opts.key, ...setOpts, result }, opts.devid);
|
|
365
|
+
// 调用缓存更新回调(带 ctx)
|
|
366
|
+
void opts.onCacheUpdated?.call(opts.ctx, result, ...(opts.args ?? []));
|
|
367
|
+
cacheLog(`cache ${opts.key} stale background refresh done!`);
|
|
368
|
+
}
|
|
369
|
+
catch (error) {
|
|
370
|
+
globalThis[_LoggerService].warn?.(`cache ${opts.key} stale background refresh failed: ${error?.message}`);
|
|
371
|
+
}
|
|
372
|
+
finally {
|
|
373
|
+
try {
|
|
374
|
+
await db.del(sfKey);
|
|
375
|
+
}
|
|
376
|
+
catch { /* TTL 兜底 */ }
|
|
377
|
+
}
|
|
378
|
+
}
|
|
344
379
|
async function redisCacheCore(opts) {
|
|
345
380
|
const db = getRedisDB();
|
|
346
|
-
const cacheKey =
|
|
347
|
-
const
|
|
348
|
-
if (
|
|
349
|
-
|
|
350
|
-
|
|
381
|
+
const cacheKey = CacheKey.value(opts.key);
|
|
382
|
+
const raw = await db.get(cacheKey);
|
|
383
|
+
if (raw !== null) {
|
|
384
|
+
// 一次读取拿到全部信息(值 + stale 状态)
|
|
385
|
+
const v = deserializeCacheValue(raw);
|
|
386
|
+
const isStale = v.s === 1;
|
|
387
|
+
if (isStale) {
|
|
388
|
+
// stale 窗口内:返回旧值 + 触发后台刷新(不阻塞)
|
|
389
|
+
cacheLog(`cache ${opts.key} hit (stale t${v.t})!`);
|
|
390
|
+
const setOpts = {
|
|
391
|
+
clearKey: opts.clearKey,
|
|
392
|
+
autoClearTime: opts.autoClearTime,
|
|
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);
|
|
400
|
+
});
|
|
401
|
+
globalStaleInflight.set(opts.key, refreshP);
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
else {
|
|
405
|
+
cacheLog(`cache ${opts.key} hit (t${v.t})!`);
|
|
406
|
+
}
|
|
407
|
+
return JSON.parse(v.d);
|
|
351
408
|
}
|
|
352
|
-
|
|
409
|
+
cacheLog(`cache ${opts.key} miss!`);
|
|
353
410
|
const setOpts = {
|
|
354
411
|
clearKey: opts.clearKey,
|
|
355
412
|
autoClearTime: opts.autoClearTime,
|
|
356
413
|
cacheNullValue: opts.cacheNullValue,
|
|
357
|
-
nullCacheTime: opts.nullCacheTime
|
|
414
|
+
nullCacheTime: opts.nullCacheTime,
|
|
415
|
+
staleMode: opts.staleMode
|
|
358
416
|
};
|
|
359
417
|
// 用一个独立的"首飞标记" key 选出谁去执行 fn——SET NX PX 是原子操作,
|
|
360
418
|
// 同一时刻只有一个调用者拿到 true,其它返回 null。这把"标记"不重入也不计数,
|
|
361
419
|
// 只是个 single-flight 指示器,和 redlock / GetRedisLock 的并发计数器不冲突。
|
|
362
|
-
const sfKey =
|
|
420
|
+
const sfKey = CacheKey.sf(opts.key);
|
|
363
421
|
let isLeader = false;
|
|
364
422
|
try {
|
|
365
423
|
const ok = await db.set(sfKey, '1', 'PX', SINGLEFLIGHT_LOCK_TTL_MS, 'NX');
|
|
@@ -370,6 +428,7 @@ async function redisCacheCore(opts) {
|
|
|
370
428
|
globalThis[_LoggerService].warn?.(`single-flight setnx ${opts.key} failed: ${error?.message}, fallback to no-lock`);
|
|
371
429
|
const result = await opts.fn();
|
|
372
430
|
await setMethodCache({ key: opts.key, ...setOpts, result }, opts.devid);
|
|
431
|
+
void opts.onCacheUpdated?.call(opts.ctx, result, ...(opts.args ?? []));
|
|
373
432
|
return result;
|
|
374
433
|
}
|
|
375
434
|
if (isLeader) {
|
|
@@ -377,6 +436,7 @@ async function redisCacheCore(opts) {
|
|
|
377
436
|
try {
|
|
378
437
|
const result = await opts.fn();
|
|
379
438
|
await setMethodCache({ key: opts.key, ...setOpts, result }, opts.devid);
|
|
439
|
+
void opts.onCacheUpdated?.call(opts.ctx, result, ...(opts.args ?? []));
|
|
380
440
|
return result;
|
|
381
441
|
}
|
|
382
442
|
finally {
|
|
@@ -394,18 +454,19 @@ async function redisCacheCore(opts) {
|
|
|
394
454
|
await sleep(SINGLEFLIGHT_POLL_INTERVAL_MS);
|
|
395
455
|
const recheck = await db.get(cacheKey);
|
|
396
456
|
if (recheck !== null) {
|
|
397
|
-
|
|
398
|
-
return JSON.parse(recheck);
|
|
457
|
+
cacheLog(`cache ${opts.key} hit after wait!`);
|
|
458
|
+
return JSON.parse(deserializeCacheValue(recheck).d);
|
|
399
459
|
}
|
|
400
460
|
// 首飞挂了(TTL 到期,sf 标记自动消失)但缓存仍然空——抢做下一任首飞
|
|
401
461
|
const leaderGone = await db.get(sfKey);
|
|
402
462
|
if (leaderGone === null) {
|
|
403
463
|
const taken = await db.set(sfKey, '1', 'PX', SINGLEFLIGHT_LOCK_TTL_MS, 'NX');
|
|
404
464
|
if (taken === 'OK') {
|
|
405
|
-
|
|
465
|
+
cacheLog(`cache ${opts.key}: previous leader gone, taking over`);
|
|
406
466
|
try {
|
|
407
467
|
const result = await opts.fn();
|
|
408
468
|
await setMethodCache({ key: opts.key, ...setOpts, result }, opts.devid);
|
|
469
|
+
void opts.onCacheUpdated?.call(opts.ctx, result, ...(opts.args ?? []));
|
|
409
470
|
return result;
|
|
410
471
|
}
|
|
411
472
|
finally {
|
|
@@ -422,56 +483,20 @@ async function redisCacheCore(opts) {
|
|
|
422
483
|
globalThis[_LoggerService].warn?.(`cache ${opts.key}: wait timed out, running fn without lock`);
|
|
423
484
|
const result = await opts.fn();
|
|
424
485
|
await setMethodCache({ key: opts.key, ...setOpts, result }, opts.devid);
|
|
486
|
+
void opts.onCacheUpdated?.call(opts.ctx, result, ...(opts.args ?? []));
|
|
425
487
|
return result;
|
|
426
488
|
}
|
|
427
489
|
}
|
|
428
490
|
}
|
|
429
|
-
/**
|
|
430
|
-
* 内存缓存执行:单进程内的 single-flight 不需要任何分布式协调——
|
|
431
|
-
* JS 单线程 + 同一份 in-flight Map 就够了。同一 key 的并发调用全部 await 同一个 Promise,
|
|
432
|
-
* Promise 解析后所有等待者下个事件循环 tick 并发返回,零序列化、零 redis 往返。
|
|
433
|
-
*/
|
|
434
|
-
async function memoryCacheCore(opts) {
|
|
435
|
-
const cacheKey = opts.key;
|
|
436
|
-
// 1. 看缓存
|
|
437
|
-
const hit = memGet(cacheKey);
|
|
438
|
-
if (hit.hit) {
|
|
439
|
-
globalThis[_LoggerService].debug?.(`memcache ${opts.key} hit!`);
|
|
440
|
-
return hit.value;
|
|
441
|
-
}
|
|
442
|
-
globalThis[_LoggerService].debug?.(`memcache ${opts.key} miss!`);
|
|
443
|
-
// 2. 已有等待中的 Promise → 直接 await,single-flight 自动达成
|
|
444
|
-
const inflight = getMemInflight();
|
|
445
|
-
const existing = inflight.get(cacheKey);
|
|
446
|
-
if (existing) {
|
|
447
|
-
return await existing;
|
|
448
|
-
}
|
|
449
|
-
// 3. 首飞:把 Promise 提前塞进 inflight,让后到者命中
|
|
450
|
-
const p = (async () => {
|
|
451
|
-
try {
|
|
452
|
-
const result = await opts.fn();
|
|
453
|
-
memSet(cacheKey, result, {
|
|
454
|
-
autoClearTime: opts.autoClearTime,
|
|
455
|
-
cacheNullValue: opts.cacheNullValue,
|
|
456
|
-
nullCacheTime: opts.nullCacheTime,
|
|
457
|
-
clearKey: opts.clearKey
|
|
458
|
-
});
|
|
459
|
-
return result;
|
|
460
|
-
}
|
|
461
|
-
finally {
|
|
462
|
-
inflight.delete(cacheKey);
|
|
463
|
-
}
|
|
464
|
-
})();
|
|
465
|
-
inflight.set(cacheKey, p);
|
|
466
|
-
return await p;
|
|
467
|
-
}
|
|
491
|
+
/** @internal 唯一的执行入口(Memory 后端已移除) */
|
|
468
492
|
async function excuteCacheCore(opts) {
|
|
469
|
-
return
|
|
493
|
+
return await redisCacheCore(opts);
|
|
470
494
|
}
|
|
471
495
|
/**
|
|
472
496
|
* 执行一个方法fn,
|
|
473
497
|
* 如果有缓存,则返回缓存,否则【加锁、二次检查】后执行方法并缓存,防止缓存击穿。
|
|
474
498
|
* 可选 `cacheNullValue=true` 开启负缓存,防穿透;负缓存默认沿用 autoClearTime,可单独用 nullCacheTime 设短。
|
|
499
|
+
* 可选 `staleMode: CacheStaleMode.StaleWhileRevalidate` 开启异步重新验证(清理时保留旧值,零阻塞)。
|
|
475
500
|
*/
|
|
476
501
|
export async function excuteWithCache(config, fn) {
|
|
477
502
|
const key = typeof config.key === 'function' ? config.key() : config.key;
|
|
@@ -481,7 +506,10 @@ export async function excuteWithCache(config, fn) {
|
|
|
481
506
|
autoClearTime: config.autoClearTime,
|
|
482
507
|
cacheNullValue: config.cacheNullValue,
|
|
483
508
|
nullCacheTime: config.nullCacheTime,
|
|
484
|
-
|
|
509
|
+
staleMode: config.staleMode,
|
|
510
|
+
onCacheUpdated: config.onCacheUpdated,
|
|
511
|
+
ctx: config.ctx,
|
|
512
|
+
args: config.args ?? [],
|
|
485
513
|
fn
|
|
486
514
|
});
|
|
487
515
|
}
|
|
@@ -489,7 +517,8 @@ export async function excuteWithCache(config, fn) {
|
|
|
489
517
|
* 缓存注解:先查缓存 → miss 才加锁 → 锁内再查一次 → 仍 miss 才执行原方法。
|
|
490
518
|
* 已内置 single-flight,不需要再叠 `@MethodLock`。
|
|
491
519
|
* 可选 `cacheNullValue=true` 开启负缓存,防穿透。
|
|
492
|
-
* 可选 `
|
|
520
|
+
* 可选 `staleMode: CacheStaleMode.StaleWhileRevalidate` 开启异步重新验证(清理时保留旧值,零阻塞)。
|
|
521
|
+
* 可选 `onCacheUpdated` 设置缓存更新回调(带 this 上下文)。
|
|
493
522
|
*/
|
|
494
523
|
export function MethodCache(config) {
|
|
495
524
|
return function (_target, _propertyKey, descriptor) {
|
|
@@ -506,10 +535,59 @@ export function MethodCache(config) {
|
|
|
506
535
|
autoClearTime: config.autoClearTime,
|
|
507
536
|
cacheNullValue: config.cacheNullValue,
|
|
508
537
|
nullCacheTime: config.nullCacheTime,
|
|
509
|
-
|
|
538
|
+
staleMode: config.staleMode,
|
|
539
|
+
onCacheUpdated: config.onCacheUpdated,
|
|
540
|
+
ctx: this, // 传递当前 this 上下文
|
|
541
|
+
args, // 传递方法输入参数
|
|
510
542
|
devid,
|
|
511
543
|
fn: async () => await fn.call(this, ...args)
|
|
512
544
|
});
|
|
513
545
|
};
|
|
514
546
|
};
|
|
515
547
|
}
|
|
548
|
+
/**
|
|
549
|
+
* 外部直接设置缓存值。用于绕过 @MethodCache 装饰器直接更新缓存的场景
|
|
550
|
+
* (如外部事件驱动的数据变更、手动刷新等)。
|
|
551
|
+
*
|
|
552
|
+
* 内部使用统一格式 { t, d, s, sa } 序列化,确保与 @MethodCache 的读取逻辑兼容。
|
|
553
|
+
*
|
|
554
|
+
* @param cacheKey 缓存 key(不含 [cache] 前缀,与 @MethodCache 的 key 一致)
|
|
555
|
+
* @param value 要缓存的值
|
|
556
|
+
* @param options 选项
|
|
557
|
+
*
|
|
558
|
+
* @example
|
|
559
|
+
* ```ts
|
|
560
|
+
* // 外部事件驱动更新缓存
|
|
561
|
+
* await setCache('match-timer-123', timer, { autoClearTime: 120 });
|
|
562
|
+
*
|
|
563
|
+
* // 同时触发 onCacheUpdated 回调(如果有)
|
|
564
|
+
* await setCache('match-timer-123', timer, {
|
|
565
|
+
* autoClearTime: 120,
|
|
566
|
+
* onCacheUpdated(this: MyService, value) {
|
|
567
|
+
* this.broadcast(value);
|
|
568
|
+
* }
|
|
569
|
+
* });
|
|
570
|
+
* ```
|
|
571
|
+
*/
|
|
572
|
+
export async function setCache(cacheKey, value, options = {}) {
|
|
573
|
+
const { autoClearTime, cacheNullValue = false, onCacheUpdated, ctx, args = [] } = options;
|
|
574
|
+
const isNullish = value === null || value === undefined;
|
|
575
|
+
if (isNullish && !cacheNullValue) {
|
|
576
|
+
return;
|
|
577
|
+
}
|
|
578
|
+
const dataStr = isNullish ? 'null' : JSON.stringify(value);
|
|
579
|
+
const timestamp = Date.now();
|
|
580
|
+
const db = getRedisDB();
|
|
581
|
+
// 保留旧 sa:沿用已有标志(防止外部直写清掉 sa=1,导致 clearMethodCache 变 purge)
|
|
582
|
+
const existingSa = await db.get(CacheKey.value(cacheKey)).then(r => r ? deserializeCacheValue(r).sa : undefined);
|
|
583
|
+
const payload = serializeCacheValue(dataStr, timestamp, false, !!existingSa);
|
|
584
|
+
if (autoClearTime) {
|
|
585
|
+
await db.set(CacheKey.value(cacheKey), payload, 'EX', autoClearTime * 60);
|
|
586
|
+
}
|
|
587
|
+
else {
|
|
588
|
+
await db.set(CacheKey.value(cacheKey), payload);
|
|
589
|
+
}
|
|
590
|
+
// 触发回调
|
|
591
|
+
void onCacheUpdated?.call(ctx, value, ...args);
|
|
592
|
+
cacheLog(`cache ${cacheKey} externally set (t${timestamp})`);
|
|
593
|
+
}
|