@taicode/common-server 3.7.11 → 3.7.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/output/index.d.ts +1 -0
- package/output/index.d.ts.map +1 -1
- package/output/index.js +1 -0
- package/output/rate-limiter/index.d.ts +1 -0
- package/output/rate-limiter/index.d.ts.map +1 -1
- package/output/rate-limiter/index.js +1 -0
- package/output/rate-limiter/rate-limiter.d.ts.map +1 -1
- package/output/rate-limiter/redis-store.d.ts +45 -0
- package/output/rate-limiter/redis-store.d.ts.map +1 -0
- package/output/rate-limiter/redis-store.js +114 -0
- package/output/rate-limiter/redis-store.test.d.ts +2 -0
- package/output/rate-limiter/redis-store.test.d.ts.map +1 -0
- package/output/rate-limiter/redis-store.test.js +70 -0
- package/output/redis-cache/index.d.ts +4 -0
- package/output/redis-cache/index.d.ts.map +1 -0
- package/output/redis-cache/index.js +1 -0
- package/output/redis-cache/redis-cache.d.ts +139 -0
- package/output/redis-cache/redis-cache.d.ts.map +1 -0
- package/output/redis-cache/redis-cache.js +282 -0
- package/output/redis-cache/redis-cache.test.d.ts +2 -0
- package/output/redis-cache/redis-cache.test.d.ts.map +1 -0
- package/output/redis-cache/redis-cache.test.js +156 -0
- package/output/redis-queue/redis-queue-batch-consumer.d.ts +2 -2
- package/output/redis-queue/redis-queue-batch-consumer.d.ts.map +1 -1
- package/output/redis-queue/redis-queue-batch-consumer.js +5 -6
- package/output/redis-queue/redis-queue-batch-consumer.test.d.ts +1 -1
- package/output/redis-queue/redis-queue-batch-consumer.test.d.ts.map +1 -1
- package/output/redis-queue/redis-queue-batch-consumer.test.js +1 -3
- package/output/redis-queue/redis-queue-common.d.ts.map +1 -1
- package/output/redis-queue/redis-queue-common.test.d.ts +17 -13
- package/output/redis-queue/redis-queue-common.test.d.ts.map +1 -1
- package/output/redis-queue/redis-queue-consumer.d.ts +2 -2
- package/output/redis-queue/redis-queue-consumer.d.ts.map +1 -1
- package/output/redis-queue/redis-queue-consumer.js +2 -3
- package/output/redis-queue/redis-queue-consumer.test.d.ts +1 -1
- package/output/redis-queue/redis-queue-consumer.test.d.ts.map +1 -1
- package/output/redis-queue/redis-queue-consumer.test.js +1 -1
- package/output/redis-queue/redis-queue-provider.d.ts +2 -2
- package/output/redis-queue/redis-queue-provider.d.ts.map +1 -1
- package/output/redis-queue/redis-queue-provider.js +1 -1
- package/output/redis-queue/redis-queue-provider.test.d.ts +2 -2
- package/output/redis-queue/redis-queue-provider.test.d.ts.map +1 -1
- package/output/redis-queue/redis-queue-provider.test.js +42 -42
- package/output/redis-queue/test-helpers.d.ts +11 -14
- package/output/redis-queue/test-helpers.d.ts.map +1 -1
- package/output/redis-queue/test-helpers.js +9 -23
- package/output/redis-queue/test-helpers.test.d.ts +1 -1
- package/output/redis-queue/test-helpers.test.d.ts.map +1 -1
- package/output/redis-queue/test-helpers.test.js +7 -13
- package/output/redis-queue/types.d.ts +7 -0
- package/output/redis-queue/types.d.ts.map +1 -1
- package/output/schema/index.d.ts +1 -1
- package/output/schema/index.d.ts.map +1 -1
- package/package.json +11 -3
package/output/index.d.ts
CHANGED
package/output/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../source/index.ts"],"names":[],"mappings":"AACA,cAAc,mBAAmB,CAAA;AACjC,cAAc,wBAAwB,CAAA;AACtC,cAAc,yBAAyB,CAAA;AACvC,cAAc,oBAAoB,CAAA"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../source/index.ts"],"names":[],"mappings":"AACA,cAAc,mBAAmB,CAAA;AACjC,cAAc,wBAAwB,CAAA;AACtC,cAAc,wBAAwB,CAAA;AACtC,cAAc,yBAAyB,CAAA;AACvC,cAAc,oBAAoB,CAAA"}
|
package/output/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../source/rate-limiter/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAA;AAC1B,cAAc,mBAAmB,CAAA;AACjC,cAAc,mBAAmB,CAAA"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../source/rate-limiter/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAA;AAC1B,cAAc,mBAAmB,CAAA;AACjC,cAAc,mBAAmB,CAAA;AACjC,cAAc,kBAAkB,CAAA"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rate-limiter.d.ts","sourceRoot":"","sources":["../../source/rate-limiter/rate-limiter.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"rate-limiter.d.ts","sourceRoot":"","sources":["../../source/rate-limiter/rate-limiter.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,eAAe,EAAE,aAAa,EAAE,kBAAkB,EAAE,eAAe,EAAkB,MAAM,YAAY,CAAA;AAErH,qBAAa,WAAW;IACtB,OAAO,CAAC,KAAK,CAAgB;gBAEjB,OAAO,GAAE,kBAAuB;IAItC,UAAU,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,eAAe,GAAG,OAAO,CAAC,eAAe,CAAC;IAoC1E,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAItC,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,GAAG,IAAI,CAAC;IAI9D;;OAEG;IACG,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,MAAM,EAAE,eAAe,GAAG,OAAO,CAAC,eAAe,CAAC;CAezF;AAED;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,KAAK,EAAE,MAAM,EAAE,GAAG,MAAM,CAE7E"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { type RedisClientType } from 'redis';
|
|
2
|
+
import type { RateLimitInfo, RateLimitStore } from './types.js';
|
|
3
|
+
export interface RedisRateLimitStoreOptions {
|
|
4
|
+
/** 键前缀,避免与其他用途的键混在一起 */
|
|
5
|
+
prefix?: string;
|
|
6
|
+
/** Redis 连接串;与 redisClient 二选一 */
|
|
7
|
+
redisUrl?: string;
|
|
8
|
+
/** 外部 Redis 客户端;与 redisUrl 二选一 */
|
|
9
|
+
redisClient?: RedisClientType;
|
|
10
|
+
/**
|
|
11
|
+
* Redis 不可用时的降级存储,默认使用进程内 MemoryRateLimitStore。
|
|
12
|
+
* 刻意不降级为「不限流」:多实例之间不再共享计数已经是能力损失,
|
|
13
|
+
* 但单实例内仍然能挡住突发,比彻底放行安全得多。
|
|
14
|
+
*/
|
|
15
|
+
fallback?: RateLimitStore;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* 基于 Redis 的限流存储。
|
|
19
|
+
*
|
|
20
|
+
* 与 MemoryRateLimitStore 保持完全相同的「固定窗口」语义:
|
|
21
|
+
* 窗口在第一次请求时开启,窗口结束时间写入存储并随窗口一起过期。
|
|
22
|
+
* 区别只在于计数放在 Redis 里,因此多副本部署下 limit 是全局生效的
|
|
23
|
+
* (进程内计数在 N 个副本下等于把 limit 放大了 N 倍)。
|
|
24
|
+
*
|
|
25
|
+
* 读改写之间存在极小的竞态窗口(并发请求可能少计一次),
|
|
26
|
+
* 对限流来说这种近似完全可接受,换来的是无需 Lua 脚本的简单实现。
|
|
27
|
+
*/
|
|
28
|
+
export declare class RedisRateLimitStore implements RateLimitStore {
|
|
29
|
+
private readonly prefix;
|
|
30
|
+
private readonly redis;
|
|
31
|
+
private readonly isExternalRedis;
|
|
32
|
+
private readonly fallback;
|
|
33
|
+
private lastErrorLogTime;
|
|
34
|
+
constructor(options?: RedisRateLimitStoreOptions);
|
|
35
|
+
connect(): Promise<void>;
|
|
36
|
+
disconnect(): void;
|
|
37
|
+
get(key: string): Promise<RateLimitInfo | null>;
|
|
38
|
+
set(key: string, info: RateLimitInfo): Promise<void>;
|
|
39
|
+
delete(key: string): Promise<void>;
|
|
40
|
+
/** Redis 的键自带过期时间,无需额外清理 */
|
|
41
|
+
cleanup(): Promise<void>;
|
|
42
|
+
private buildKey;
|
|
43
|
+
private logError;
|
|
44
|
+
}
|
|
45
|
+
//# sourceMappingURL=redis-store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"redis-store.d.ts","sourceRoot":"","sources":["../../source/rate-limiter/redis-store.ts"],"names":[],"mappings":"AAAA,OAAO,EAAgB,KAAK,eAAe,EAAE,MAAM,OAAO,CAAA;AAG1D,OAAO,KAAK,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,YAAY,CAAA;AAE/D,MAAM,WAAW,0BAA0B;IACzC,wBAAwB;IACxB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,kCAAkC;IAClC,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,kCAAkC;IAClC,WAAW,CAAC,EAAE,eAAe,CAAA;IAC7B;;;;OAIG;IACH,QAAQ,CAAC,EAAE,cAAc,CAAA;CAC1B;AAED;;;;;;;;;;GAUG;AACH,qBAAa,mBAAoB,YAAW,cAAc;IACxD,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAQ;IAC/B,OAAO,CAAC,QAAQ,CAAC,KAAK,CAA+B;IACrD,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAiB;IACjD,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAgB;IAEzC,OAAO,CAAC,gBAAgB,CAAI;gBAEhB,OAAO,GAAE,0BAA+B;IAyBvC,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAK9B,UAAU,IAAI,IAAI;IAKZ,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,GAAG,IAAI,CAAC;IAqB/C,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC;IAgBpD,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAY/C,4BAA4B;IACf,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAIrC,OAAO,CAAC,QAAQ;IAIhB,OAAO,CAAC,QAAQ;CAOjB"}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { createClient } from 'redis';
|
|
2
|
+
import { logger } from '@taicode/common-base';
|
|
3
|
+
import { MemoryRateLimitStore } from './memory-store.js';
|
|
4
|
+
/**
|
|
5
|
+
* 基于 Redis 的限流存储。
|
|
6
|
+
*
|
|
7
|
+
* 与 MemoryRateLimitStore 保持完全相同的「固定窗口」语义:
|
|
8
|
+
* 窗口在第一次请求时开启,窗口结束时间写入存储并随窗口一起过期。
|
|
9
|
+
* 区别只在于计数放在 Redis 里,因此多副本部署下 limit 是全局生效的
|
|
10
|
+
* (进程内计数在 N 个副本下等于把 limit 放大了 N 倍)。
|
|
11
|
+
*
|
|
12
|
+
* 读改写之间存在极小的竞态窗口(并发请求可能少计一次),
|
|
13
|
+
* 对限流来说这种近似完全可接受,换来的是无需 Lua 脚本的简单实现。
|
|
14
|
+
*/
|
|
15
|
+
export class RedisRateLimitStore {
|
|
16
|
+
prefix;
|
|
17
|
+
redis = null;
|
|
18
|
+
isExternalRedis = false;
|
|
19
|
+
fallback;
|
|
20
|
+
lastErrorLogTime = 0;
|
|
21
|
+
constructor(options = {}) {
|
|
22
|
+
if (options.redisUrl && options.redisClient) {
|
|
23
|
+
throw new Error('[RedisRateLimitStore] Cannot specify both redisUrl and redisClient');
|
|
24
|
+
}
|
|
25
|
+
this.prefix = options.prefix ?? 'rate-limit:';
|
|
26
|
+
this.fallback = options.fallback ?? new MemoryRateLimitStore();
|
|
27
|
+
if (options.redisClient) {
|
|
28
|
+
this.redis = options.redisClient;
|
|
29
|
+
this.isExternalRedis = true;
|
|
30
|
+
}
|
|
31
|
+
else if (options.redisUrl) {
|
|
32
|
+
this.redis = createClient({
|
|
33
|
+
url: options.redisUrl,
|
|
34
|
+
socket: {
|
|
35
|
+
connectTimeout: 5_000,
|
|
36
|
+
reconnectStrategy: (retries) => Math.min(retries * 200, 3_000),
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
this.redis.on('error', (error) => {
|
|
40
|
+
logger.error('[RedisRateLimitStore] Redis Client Error:', error);
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
async connect() {
|
|
45
|
+
if (this.redis === null || this.isExternalRedis || this.redis.isOpen)
|
|
46
|
+
return;
|
|
47
|
+
await this.redis.connect();
|
|
48
|
+
}
|
|
49
|
+
disconnect() {
|
|
50
|
+
if (this.redis === null || this.isExternalRedis || !this.redis.isOpen)
|
|
51
|
+
return;
|
|
52
|
+
this.redis.disconnect();
|
|
53
|
+
}
|
|
54
|
+
async get(key) {
|
|
55
|
+
if (!this.redis?.isReady) {
|
|
56
|
+
return this.fallback.get(key);
|
|
57
|
+
}
|
|
58
|
+
try {
|
|
59
|
+
const raw = await this.redis.get(this.buildKey(key));
|
|
60
|
+
if (raw === null)
|
|
61
|
+
return null;
|
|
62
|
+
const info = JSON.parse(raw);
|
|
63
|
+
return {
|
|
64
|
+
count: info.count,
|
|
65
|
+
windowStart: new Date(info.windowStart),
|
|
66
|
+
windowEnd: new Date(info.windowEnd),
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
catch (error) {
|
|
70
|
+
this.logError('get', error);
|
|
71
|
+
return this.fallback.get(key);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
async set(key, info) {
|
|
75
|
+
if (!this.redis?.isReady) {
|
|
76
|
+
return this.fallback.set(key, info);
|
|
77
|
+
}
|
|
78
|
+
const ttlSeconds = Math.ceil((info.windowEnd.getTime() - Date.now()) / 1000);
|
|
79
|
+
if (ttlSeconds <= 0)
|
|
80
|
+
return;
|
|
81
|
+
try {
|
|
82
|
+
await this.redis.set(this.buildKey(key), JSON.stringify(info), { EX: ttlSeconds });
|
|
83
|
+
}
|
|
84
|
+
catch (error) {
|
|
85
|
+
this.logError('set', error);
|
|
86
|
+
await this.fallback.set(key, info);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
async delete(key) {
|
|
90
|
+
await this.fallback.delete(key);
|
|
91
|
+
if (!this.redis?.isReady)
|
|
92
|
+
return;
|
|
93
|
+
try {
|
|
94
|
+
await this.redis.del(this.buildKey(key));
|
|
95
|
+
}
|
|
96
|
+
catch (error) {
|
|
97
|
+
this.logError('delete', error);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
/** Redis 的键自带过期时间,无需额外清理 */
|
|
101
|
+
async cleanup() {
|
|
102
|
+
await this.fallback.cleanup?.();
|
|
103
|
+
}
|
|
104
|
+
buildKey(key) {
|
|
105
|
+
return `${this.prefix}${key}`;
|
|
106
|
+
}
|
|
107
|
+
logError(operation, error) {
|
|
108
|
+
const now = Date.now();
|
|
109
|
+
if (now - this.lastErrorLogTime < 30_000)
|
|
110
|
+
return;
|
|
111
|
+
this.lastErrorLogTime = now;
|
|
112
|
+
logger.error(`[RedisRateLimitStore] ${operation} failed, fallback to memory store:`, error);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"redis-store.test.d.ts","sourceRoot":"","sources":["../../source/rate-limiter/redis-store.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
2
|
+
import { createRedisClient } from '../redis-cache/redis-cache.js';
|
|
3
|
+
import { RateLimiter } from './rate-limiter.js';
|
|
4
|
+
import { RedisRateLimitStore } from './redis-store.js';
|
|
5
|
+
const REDIS_URL = process.env.REDIS_URL || 'redis://localhost:6379';
|
|
6
|
+
describe('RedisRateLimitStore', () => {
|
|
7
|
+
let redis;
|
|
8
|
+
let store;
|
|
9
|
+
beforeEach(async () => {
|
|
10
|
+
redis = createRedisClient({ url: REDIS_URL });
|
|
11
|
+
await redis.connect();
|
|
12
|
+
store = new RedisRateLimitStore({ prefix: 'test:rate-limit:', redisClient: redis });
|
|
13
|
+
await store.delete('ip:1.2.3.4');
|
|
14
|
+
});
|
|
15
|
+
afterEach(async () => {
|
|
16
|
+
await store.delete('ip:1.2.3.4');
|
|
17
|
+
if (redis.isOpen)
|
|
18
|
+
redis.disconnect();
|
|
19
|
+
});
|
|
20
|
+
it('应该在窗口内累计计数', async () => {
|
|
21
|
+
await store.set('ip:1.2.3.4', {
|
|
22
|
+
count: 1,
|
|
23
|
+
windowStart: new Date(),
|
|
24
|
+
windowEnd: new Date(Date.now() + 60_000),
|
|
25
|
+
});
|
|
26
|
+
const info = await store.get('ip:1.2.3.4');
|
|
27
|
+
expect(info?.count).toBe(1);
|
|
28
|
+
});
|
|
29
|
+
it('应该让 RateLimiter 在多实例间共享计数', async () => {
|
|
30
|
+
const anotherStore = new RedisRateLimitStore({ prefix: 'test:rate-limit:', redisClient: redis });
|
|
31
|
+
const limiterA = new RateLimiter({ store });
|
|
32
|
+
const limiterB = new RateLimiter({ store: anotherStore });
|
|
33
|
+
const config = { limit: 2, window: 60 };
|
|
34
|
+
expect((await limiterA.checkLimit('ip:1.2.3.4', config)).allowed).toBe(true);
|
|
35
|
+
// 另一个「实例」能看到前一个实例的计数,因此第三次就被拦下
|
|
36
|
+
expect((await limiterB.checkLimit('ip:1.2.3.4', config)).allowed).toBe(true);
|
|
37
|
+
const third = await limiterA.checkLimit('ip:1.2.3.4', config);
|
|
38
|
+
expect(third.allowed).toBe(false);
|
|
39
|
+
expect(third.remaining).toBe(0);
|
|
40
|
+
});
|
|
41
|
+
it('应该为限流键设置与窗口一致的过期时间', async () => {
|
|
42
|
+
await store.set('ip:1.2.3.4', {
|
|
43
|
+
count: 1,
|
|
44
|
+
windowStart: new Date(),
|
|
45
|
+
windowEnd: new Date(Date.now() + 30_000),
|
|
46
|
+
});
|
|
47
|
+
const ttl = await redis.ttl('test:rate-limit:ip:1.2.3.4');
|
|
48
|
+
expect(ttl).toBeGreaterThan(0);
|
|
49
|
+
expect(ttl).toBeLessThanOrEqual(30);
|
|
50
|
+
});
|
|
51
|
+
it('窗口已过期时不应写入', async () => {
|
|
52
|
+
await store.set('ip:1.2.3.4', {
|
|
53
|
+
count: 1,
|
|
54
|
+
windowStart: new Date(Date.now() - 120_000),
|
|
55
|
+
windowEnd: new Date(Date.now() - 60_000),
|
|
56
|
+
});
|
|
57
|
+
expect(await redis.get('test:rate-limit:ip:1.2.3.4')).toBeNull();
|
|
58
|
+
});
|
|
59
|
+
it('Redis 不可用时应降级到进程内存储而不是放行', async () => {
|
|
60
|
+
redis.disconnect();
|
|
61
|
+
const result = await store.get('ip:1.2.3.4');
|
|
62
|
+
expect(result).toBeNull();
|
|
63
|
+
await store.set('ip:1.2.3.4', {
|
|
64
|
+
count: 3,
|
|
65
|
+
windowStart: new Date(),
|
|
66
|
+
windowEnd: new Date(Date.now() + 60_000),
|
|
67
|
+
});
|
|
68
|
+
expect((await store.get('ip:1.2.3.4'))?.count).toBe(3);
|
|
69
|
+
});
|
|
70
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../source/redis-cache/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAA;AAChE,YAAY,EAAE,mBAAmB,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAA;AAE9E,YAAY,EAAE,eAAe,EAAE,MAAM,OAAO,CAAA"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { RedisCache, createRedisClient } from './redis-cache.js';
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { type RedisClientType } from 'redis';
|
|
2
|
+
/**
|
|
3
|
+
* 进程内一级缓存配置。
|
|
4
|
+
*
|
|
5
|
+
* 一级缓存的意义是把「同一个进程内的高频重复读取」压缩到零网络开销:
|
|
6
|
+
* Redis 的一次 GET 大约 0.2~1ms,而进程内查表是纳秒级。
|
|
7
|
+
* 鉴权这类每请求都要走的路径,一级缓存带来的收益非常可观。
|
|
8
|
+
*/
|
|
9
|
+
export interface RedisCacheL1Options {
|
|
10
|
+
/**
|
|
11
|
+
* 一级缓存容量(条目数),默认 500。
|
|
12
|
+
* 达到容量后按 LRU 淘汰,因此只需保证「热点」能装下即可。
|
|
13
|
+
*/
|
|
14
|
+
maxSize?: number;
|
|
15
|
+
/**
|
|
16
|
+
* 一级缓存存活时间(毫秒),默认 5000。
|
|
17
|
+
*
|
|
18
|
+
* 刻意比二级缓存短一个数量级:跨实例的失效通知依赖 Redis 的键删除,
|
|
19
|
+
* 一级缓存无法被其他实例感知,只能靠短 TTL 自然过期来收敛。
|
|
20
|
+
* 这也意味着一级缓存越短,权限变更生效越快。
|
|
21
|
+
*/
|
|
22
|
+
ttlMs?: number;
|
|
23
|
+
}
|
|
24
|
+
export interface RedisCacheOptions {
|
|
25
|
+
/**
|
|
26
|
+
* 键前缀,通常形如 `account:`。
|
|
27
|
+
* 服务名 + 环境一起放进来可以避免不同服务、不同环境互相污染。
|
|
28
|
+
*/
|
|
29
|
+
prefix: string;
|
|
30
|
+
/** Redis 连接串;与 redisClient 二选一 */
|
|
31
|
+
redisUrl?: string;
|
|
32
|
+
/** 外部 Redis 客户端(多个缓存共享同一条连接时使用);与 redisUrl 二选一 */
|
|
33
|
+
redisClient?: RedisClientType;
|
|
34
|
+
/** 默认过期时间(秒),默认 300 */
|
|
35
|
+
defaultTtlSeconds?: number;
|
|
36
|
+
/**
|
|
37
|
+
* 空值(null / undefined)的过期时间(秒),默认 10。
|
|
38
|
+
* 空值必须缓存但必须短,否则「删除后立刻重建」的语义会被缓存彻底挡住(缓存穿透)。
|
|
39
|
+
*/
|
|
40
|
+
nullTtlSeconds?: number;
|
|
41
|
+
/** 一级缓存配置;传 false 关闭一级缓存,默认为开启 */
|
|
42
|
+
l1?: RedisCacheL1Options | false;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Redis 缓存(可选带进程内一级缓存)。
|
|
46
|
+
*
|
|
47
|
+
* 设计原则:
|
|
48
|
+
* 1. **缓存故障绝不影响业务**:所有方法都不会抛出异常,Redis 不可用时
|
|
49
|
+
* 自动退化为「不带缓存的直读」或「仅一级缓存」,调用方无需 try/catch。
|
|
50
|
+
* 2. **一级缓存是可选加速层**:`l1` 关闭时退化为纯 Redis 缓存;
|
|
51
|
+
* 不传 `redisUrl`/`redisClient` 时退化为纯一级缓存(单测里就这么用)。
|
|
52
|
+
* 3. **空值也缓存**:防止不存在的 key 每次穿透到数据库。
|
|
53
|
+
* 4. **TTL 带抖动**:避免同一时刻写入的大量键同时过期,形成瞬时数据库尖峰。
|
|
54
|
+
*/
|
|
55
|
+
export declare class RedisCache {
|
|
56
|
+
private readonly prefix;
|
|
57
|
+
private readonly redis;
|
|
58
|
+
private readonly isExternalRedis;
|
|
59
|
+
private readonly defaultTtlSeconds;
|
|
60
|
+
private readonly nullTtlSeconds;
|
|
61
|
+
private readonly l1;
|
|
62
|
+
private readonly l1TtlMs;
|
|
63
|
+
/** 请求合并:同一个 key 的并发 miss 只触发一次 loader,防止缓存击穿 */
|
|
64
|
+
private readonly inFlight;
|
|
65
|
+
private lastErrorLogTime;
|
|
66
|
+
constructor(options: RedisCacheOptions);
|
|
67
|
+
/** 一级缓存条目数(未开启时为 0) */
|
|
68
|
+
get size(): number;
|
|
69
|
+
/** 一级缓存容量(未开启时为 0) */
|
|
70
|
+
get capacity(): number;
|
|
71
|
+
/** 二级缓存是否可用。不可用时所有操作都会安静地退化为直读,不会抛错。 */
|
|
72
|
+
get remoteReady(): boolean;
|
|
73
|
+
/**
|
|
74
|
+
* 建立连接。
|
|
75
|
+
* 外部传入的客户端由持有者负责连接,这里直接返回。
|
|
76
|
+
*/
|
|
77
|
+
connect(): Promise<void>;
|
|
78
|
+
/** 断开连接(外部分享的客户端不会被断开) */
|
|
79
|
+
disconnect(): void;
|
|
80
|
+
/**
|
|
81
|
+
* 释放实例资源。
|
|
82
|
+
*
|
|
83
|
+
* 只清理进程内的东西,**不会**动 Redis:二级缓存是多实例共享的,
|
|
84
|
+
* 某个进程退出不应该把别人的缓存一起清掉。
|
|
85
|
+
*/
|
|
86
|
+
dispose(): Promise<void>;
|
|
87
|
+
/**
|
|
88
|
+
* 清空一级缓存(同步),并尽力异步清空二级缓存里该前缀下的全部键。
|
|
89
|
+
* 二级缓存的清理不阻塞调用方,失败也不影响结果。
|
|
90
|
+
*/
|
|
91
|
+
clear(): void;
|
|
92
|
+
get<T>(key: string): Promise<T | null>;
|
|
93
|
+
set<T>(key: string, value: T, ttlSeconds?: number): Promise<void>;
|
|
94
|
+
delete(key: string): Promise<void>;
|
|
95
|
+
/**
|
|
96
|
+
* 按前缀批量删除。传空字符串表示清空当前缓存实例的全部键。
|
|
97
|
+
*
|
|
98
|
+
* 用于「改了一批数据,相关的缓存全部作废」的场景:
|
|
99
|
+
* 与其维护精细的依赖关系,不如按命名空间整体作废——
|
|
100
|
+
* 这类写操作本身频率很低,一次 SCAN 的代价可以接受。
|
|
101
|
+
*/
|
|
102
|
+
deleteByPrefix(prefix: string): Promise<number>;
|
|
103
|
+
/**
|
|
104
|
+
* 读缓存,未命中时执行 loader 并回填。
|
|
105
|
+
*
|
|
106
|
+
* 并发调用同一个 key 时只会执行一次 loader(请求合并),
|
|
107
|
+
* 这是鉴权链路上最容易被打穿的场景:热点用户缓存刚失效,
|
|
108
|
+
* 同一瞬间几十个请求同时涌入。
|
|
109
|
+
*/
|
|
110
|
+
getOrSet<T>(key: string, loader: () => Promise<T>, ttlSeconds?: number): Promise<T>;
|
|
111
|
+
/**
|
|
112
|
+
* 生成最终键。
|
|
113
|
+
* 注意:调用方传入的 key 里已经带了业务前缀(如 `user:`),
|
|
114
|
+
* 因此这里只负责拼接实例前缀。
|
|
115
|
+
*/
|
|
116
|
+
private buildKey;
|
|
117
|
+
/** 以下两个方法接收「已经拼好前缀的最终键」,供 getOrSet 避免重复拼前缀 */
|
|
118
|
+
private getByCacheKey;
|
|
119
|
+
private setByCacheKey;
|
|
120
|
+
private getLocal;
|
|
121
|
+
private setLocal;
|
|
122
|
+
private resolveTtl;
|
|
123
|
+
private getLogPrefix;
|
|
124
|
+
private logError;
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* 创建 Redis 客户端。
|
|
128
|
+
*
|
|
129
|
+
* 与 RedisQueueCommon 保持同样的连接策略,但补了两点:
|
|
130
|
+
* - 连接超时受限,避免 Redis 不可达时启动流程被拖住;
|
|
131
|
+
* - 重连采用固定上限的退避,避免无意义的密集重试。
|
|
132
|
+
*/
|
|
133
|
+
export interface CreateRedisClientOptions {
|
|
134
|
+
url: string;
|
|
135
|
+
logPrefix?: string;
|
|
136
|
+
connectTimeoutMs?: number;
|
|
137
|
+
}
|
|
138
|
+
export declare function createRedisClient(options: CreateRedisClientOptions): RedisClientType;
|
|
139
|
+
//# sourceMappingURL=redis-cache.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"redis-cache.d.ts","sourceRoot":"","sources":["../../source/redis-cache/redis-cache.ts"],"names":[],"mappings":"AACA,OAAO,EAAgB,KAAK,eAAe,EAAE,MAAM,OAAO,CAAA;AAE1D;;;;;;GAMG;AACH,MAAM,WAAW,mBAAmB;IAClC;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB;;;;;;OAMG;IACH,KAAK,CAAC,EAAE,MAAM,CAAA;CACf;AAED,MAAM,WAAW,iBAAiB;IAChC;;;OAGG;IACH,MAAM,EAAE,MAAM,CAAA;IACd,kCAAkC;IAClC,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,kDAAkD;IAClD,WAAW,CAAC,EAAE,eAAe,CAAA;IAC7B,uBAAuB;IACvB,iBAAiB,CAAC,EAAE,MAAM,CAAA;IAC1B;;;OAGG;IACH,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,kCAAkC;IAClC,EAAE,CAAC,EAAE,mBAAmB,GAAG,KAAK,CAAA;CACjC;AAaD;;;;;;;;;;GAUG;AACH,qBAAa,UAAU;IACrB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAQ;IAC/B,OAAO,CAAC,QAAQ,CAAC,KAAK,CAA+B;IACrD,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAiB;IACjD,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAAQ;IAC1C,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAQ;IAEvC,OAAO,CAAC,QAAQ,CAAC,EAAE,CAA0B;IAC7C,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAQ;IAEhC,gDAAgD;IAChD,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAsC;IAE/D,OAAO,CAAC,gBAAgB,CAAI;gBAEhB,OAAO,EAAE,iBAAiB;IA6BtC,uBAAuB;IACvB,IAAW,IAAI,IAAI,MAAM,CAExB;IAED,sBAAsB;IACtB,IAAW,QAAQ,IAAI,MAAM,CAE5B;IAED,wCAAwC;IACxC,IAAW,WAAW,IAAI,OAAO,CAEhC;IAED;;;OAGG;IACU,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAOrC,0BAA0B;IACnB,UAAU,IAAI,IAAI;IAOzB;;;;;OAKG;IACU,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAMrC;;;OAGG;IACI,KAAK,IAAI,IAAI;IAQP,GAAG,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC,GAAG,IAAI,CAAC;IAItC,GAAG,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,EAAE,UAAU,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAIjE,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAa/C;;;;;;OAMG;IACU,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAwB5D;;;;;;OAMG;IACU,QAAQ,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,EAAE,UAAU,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC,CAAC;IA6BhG;;;;OAIG;IACH,OAAO,CAAC,QAAQ;IAIhB,8CAA8C;YAChC,aAAa;YAmBb,aAAa;IAY3B,OAAO,CAAC,QAAQ;IAahB,OAAO,CAAC,QAAQ;IAUhB,OAAO,CAAC,UAAU;IAalB,OAAO,CAAC,YAAY;IAIpB,OAAO,CAAC,QAAQ;CAOjB;AAED;;;;;;GAMG;AACH,MAAM,WAAW,wBAAwB;IACvC,GAAG,EAAE,MAAM,CAAA;IACX,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,gBAAgB,CAAC,EAAE,MAAM,CAAA;CAC1B;AAED,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,wBAAwB,GAAG,eAAe,CAkBpF"}
|