@pangza/redis 0.0.0-stage → 1.0.0
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/README.md +2 -2
- package/dist/Redis.d.ts +98 -0
- package/dist/Redis.d.ts.map +1 -0
- package/dist/Redis.js +678 -0
- package/dist/Redis.js.map +1 -0
- package/dist/cache.d.ts +16 -0
- package/dist/cache.d.ts.map +1 -0
- package/dist/cache.js +82 -0
- package/dist/cache.js.map +1 -0
- package/dist/decorators.d.ts +15 -0
- package/dist/decorators.d.ts.map +1 -0
- package/dist/decorators.js +30 -0
- package/dist/decorators.js.map +1 -0
- package/dist/enable.d.ts +12 -0
- package/dist/enable.d.ts.map +1 -0
- package/dist/enable.js +30 -0
- package/dist/enable.js.map +1 -0
- package/dist/errors.d.ts +11 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +22 -0
- package/dist/errors.js.map +1 -0
- package/dist/flag.d.ts +2 -0
- package/dist/flag.d.ts.map +1 -0
- package/dist/flag.js +8 -0
- package/dist/flag.js.map +1 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +43 -0
- package/dist/index.js.map +1 -0
- package/dist/rateLimit.d.ts +15 -0
- package/dist/rateLimit.d.ts.map +1 -0
- package/dist/rateLimit.js +90 -0
- package/dist/rateLimit.js.map +1 -0
- package/dist/redisson/RCountDownLatch.d.ts +12 -0
- package/dist/redisson/RCountDownLatch.d.ts.map +1 -0
- package/dist/redisson/RCountDownLatch.js +48 -0
- package/dist/redisson/RCountDownLatch.js.map +1 -0
- package/dist/redisson/RFairLock.d.ts +7 -0
- package/dist/redisson/RFairLock.d.ts.map +1 -0
- package/dist/redisson/RFairLock.js +32 -0
- package/dist/redisson/RFairLock.js.map +1 -0
- package/dist/redisson/RLock.d.ts +44 -0
- package/dist/redisson/RLock.d.ts.map +1 -0
- package/dist/redisson/RLock.js +156 -0
- package/dist/redisson/RLock.js.map +1 -0
- package/dist/redisson/RMultiLock.d.ts +18 -0
- package/dist/redisson/RMultiLock.d.ts.map +1 -0
- package/dist/redisson/RMultiLock.js +95 -0
- package/dist/redisson/RMultiLock.js.map +1 -0
- package/dist/redisson/RReadWriteLock.d.ts +9 -0
- package/dist/redisson/RReadWriteLock.d.ts.map +1 -0
- package/dist/redisson/RReadWriteLock.js +51 -0
- package/dist/redisson/RReadWriteLock.js.map +1 -0
- package/dist/redisson/RSemaphore.d.ts +13 -0
- package/dist/redisson/RSemaphore.d.ts.map +1 -0
- package/dist/redisson/RSemaphore.js +55 -0
- package/dist/redisson/RSemaphore.js.map +1 -0
- package/dist/redisson/Redisson.d.ts +45 -0
- package/dist/redisson/Redisson.d.ts.map +1 -0
- package/dist/redisson/Redisson.js +111 -0
- package/dist/redisson/Redisson.js.map +1 -0
- package/dist/redisson/index.d.ts +12 -0
- package/dist/redisson/index.d.ts.map +1 -0
- package/dist/redisson/index.js +32 -0
- package/dist/redisson/index.js.map +1 -0
- package/dist/redisson/objects.d.ts +116 -0
- package/dist/redisson/objects.d.ts.map +1 -0
- package/dist/redisson/objects.js +384 -0
- package/dist/redisson/objects.js.map +1 -0
- package/dist/redisson/scripts.d.ts +22 -0
- package/dist/redisson/scripts.d.ts.map +1 -0
- package/dist/redisson/scripts.js +196 -0
- package/dist/redisson/scripts.js.map +1 -0
- package/dist/redisson/types.d.ts +140 -0
- package/dist/redisson/types.d.ts.map +1 -0
- package/dist/redisson/types.js +8 -0
- package/dist/redisson/types.js.map +1 -0
- package/dist/redisson/utils.d.ts +12 -0
- package/dist/redisson/utils.d.ts.map +1 -0
- package/dist/redisson/utils.js +82 -0
- package/dist/redisson/utils.js.map +1 -0
- package/dist/types.d.ts +141 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +25 -0
- package/dist/types.js.map +1 -0
- package/package.json +36 -4
package/dist/Redis.js
ADDED
|
@@ -0,0 +1,678 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const fs_1 = __importDefault(require("fs"));
|
|
7
|
+
const crypto_1 = require("crypto");
|
|
8
|
+
const redis_1 = require("redis");
|
|
9
|
+
const config_1 = require("@pangza/core/config");
|
|
10
|
+
const types_1 = require("./types");
|
|
11
|
+
const errors_1 = require("./errors");
|
|
12
|
+
function readOptionalFile(path) {
|
|
13
|
+
const p = path?.trim();
|
|
14
|
+
if (!p)
|
|
15
|
+
return undefined;
|
|
16
|
+
try {
|
|
17
|
+
return fs_1.default.readFileSync(p, "utf8");
|
|
18
|
+
}
|
|
19
|
+
catch (err) {
|
|
20
|
+
console.warn(`[Redis] 无法读取 TLS 文件 ${p}:`, err instanceof Error ? err.message : err);
|
|
21
|
+
return undefined;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
function buildReconnectStrategy(cfg) {
|
|
25
|
+
const reconnect = cfg.reconnect !== false;
|
|
26
|
+
const maxDelay = cfg.maxReconnectDelay ?? types_1.defaultRedisConfig.maxReconnectDelay;
|
|
27
|
+
if (!reconnect)
|
|
28
|
+
return false;
|
|
29
|
+
return (retries) => {
|
|
30
|
+
if (retries > 20) {
|
|
31
|
+
console.error("[Redis] 重连次数过多,停止重连");
|
|
32
|
+
return new Error("Redis reconnect gave up");
|
|
33
|
+
}
|
|
34
|
+
return Math.min(maxDelay, 100 * Math.pow(2, retries));
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
function buildSocketBase(cfg, host, port) {
|
|
38
|
+
const socket = {
|
|
39
|
+
host,
|
|
40
|
+
port,
|
|
41
|
+
connectTimeout: cfg.connectTimeout ?? types_1.defaultRedisConfig.connectTimeout,
|
|
42
|
+
reconnectStrategy: buildReconnectStrategy(cfg),
|
|
43
|
+
};
|
|
44
|
+
if (cfg.tls?.enabled) {
|
|
45
|
+
socket.tls = true;
|
|
46
|
+
socket.rejectUnauthorized = cfg.tls.rejectUnauthorized !== false;
|
|
47
|
+
const ca = readOptionalFile(cfg.tls.ca);
|
|
48
|
+
const cert = readOptionalFile(cfg.tls.cert);
|
|
49
|
+
const key = readOptionalFile(cfg.tls.key);
|
|
50
|
+
if (ca)
|
|
51
|
+
socket.ca = ca;
|
|
52
|
+
if (cert)
|
|
53
|
+
socket.cert = cert;
|
|
54
|
+
if (key)
|
|
55
|
+
socket.key = key;
|
|
56
|
+
}
|
|
57
|
+
return socket;
|
|
58
|
+
}
|
|
59
|
+
function authFields(cfg) {
|
|
60
|
+
const username = cfg.username?.trim();
|
|
61
|
+
const password = cfg.password?.trim();
|
|
62
|
+
return {
|
|
63
|
+
username: username || undefined,
|
|
64
|
+
password: password || undefined,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
/** 创建 RESP2 客户端(standalone / cluster / sentinel) */
|
|
68
|
+
function createRedisClient(cfg) {
|
|
69
|
+
const mode = cfg.mode ?? "standalone";
|
|
70
|
+
const auth = authFields(cfg);
|
|
71
|
+
const RESP = 2;
|
|
72
|
+
if (mode === "cluster") {
|
|
73
|
+
const nodes = cfg.cluster?.nodes?.filter((n) => n?.host?.trim()) ?? [];
|
|
74
|
+
if (nodes.length === 0) {
|
|
75
|
+
throw new Error("Redis mode=cluster 时请配置 redis.cluster.nodes(至少一个种子节点)");
|
|
76
|
+
}
|
|
77
|
+
return (0, redis_1.createCluster)({
|
|
78
|
+
RESP,
|
|
79
|
+
rootNodes: nodes.map((n) => ({
|
|
80
|
+
socket: buildSocketBase(cfg, n.host, n.port ?? 6379),
|
|
81
|
+
})),
|
|
82
|
+
defaults: {
|
|
83
|
+
...auth,
|
|
84
|
+
socket: buildSocketBase(cfg, nodes[0].host, nodes[0].port ?? 6379),
|
|
85
|
+
},
|
|
86
|
+
useReplicas: cfg.cluster?.useReplicas === true,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
if (mode === "sentinel") {
|
|
90
|
+
const name = cfg.sentinel?.name?.trim();
|
|
91
|
+
const nodes = cfg.sentinel?.nodes?.filter((n) => n?.host?.trim()) ?? [];
|
|
92
|
+
if (!name || nodes.length === 0) {
|
|
93
|
+
throw new Error("Redis mode=sentinel 时请配置 redis.sentinel.name 与 redis.sentinel.nodes");
|
|
94
|
+
}
|
|
95
|
+
return (0, redis_1.createSentinel)({
|
|
96
|
+
RESP,
|
|
97
|
+
name,
|
|
98
|
+
sentinelRootNodes: nodes.map((n) => ({
|
|
99
|
+
host: n.host,
|
|
100
|
+
port: n.port ?? 26379,
|
|
101
|
+
})),
|
|
102
|
+
nodeClientOptions: {
|
|
103
|
+
...auth,
|
|
104
|
+
socket: buildSocketBase(cfg, cfg.host ?? types_1.defaultRedisConfig.host, cfg.port ?? types_1.defaultRedisConfig.port),
|
|
105
|
+
},
|
|
106
|
+
sentinelClientOptions: {
|
|
107
|
+
...auth,
|
|
108
|
+
},
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
const host = cfg.host ?? types_1.defaultRedisConfig.host;
|
|
112
|
+
const port = cfg.port ?? types_1.defaultRedisConfig.port;
|
|
113
|
+
return (0, redis_1.createClient)({
|
|
114
|
+
RESP,
|
|
115
|
+
socket: buildSocketBase(cfg, host, port),
|
|
116
|
+
...auth,
|
|
117
|
+
database: cfg.db ?? types_1.defaultRedisConfig.db,
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* 固定窗口限流:INCR 后若尚无 TTL 则设置 expire。
|
|
122
|
+
*/
|
|
123
|
+
const RATE_LIMIT_LUA = `
|
|
124
|
+
local key=KEYS[1]
|
|
125
|
+
local time=tonumber(ARGV[1])
|
|
126
|
+
local count=tonumber(ARGV[2])
|
|
127
|
+
local current=redis.call('get', key)
|
|
128
|
+
|
|
129
|
+
if current and tonumber(current)>count then
|
|
130
|
+
return tonumber(current)
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
current = redis.call('incr', key)
|
|
134
|
+
local ttl = redis.call('pttl', key)
|
|
135
|
+
if ttl < 0 then
|
|
136
|
+
redis.call('expire', key, time)
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
return tonumber(current)
|
|
140
|
+
`.trim();
|
|
141
|
+
/**
|
|
142
|
+
* 滑动窗口限流(ZSET):清理窗口外成员后计数,未超限则 ZADD。
|
|
143
|
+
* 返回当前窗口内数量(含本次);调用方判断 > count 则拒绝。
|
|
144
|
+
*/
|
|
145
|
+
const SLIDING_RATE_LIMIT_LUA = `
|
|
146
|
+
local key = KEYS[1]
|
|
147
|
+
local now = tonumber(ARGV[1])
|
|
148
|
+
local window = tonumber(ARGV[2])
|
|
149
|
+
local limit = tonumber(ARGV[3])
|
|
150
|
+
redis.call('ZREMRANGEBYSCORE', key, 0, now - window)
|
|
151
|
+
local n = redis.call('ZCARD', key)
|
|
152
|
+
if n >= limit then
|
|
153
|
+
return n + 1
|
|
154
|
+
end
|
|
155
|
+
redis.call('ZADD', key, now, now .. '-' .. ARGV[4])
|
|
156
|
+
redis.call('PEXPIRE', key, window)
|
|
157
|
+
return n + 1
|
|
158
|
+
`.trim();
|
|
159
|
+
/** 仅当 value 匹配时删除(安全解锁) */
|
|
160
|
+
const UNLOCK_IF_MATCH_LUA = `
|
|
161
|
+
if redis.call('get', KEYS[1]) == ARGV[1] then
|
|
162
|
+
return redis.call('del', KEYS[1])
|
|
163
|
+
end
|
|
164
|
+
return 0
|
|
165
|
+
`.trim();
|
|
166
|
+
/** 仅当 value 匹配时续期 */
|
|
167
|
+
const RENEW_IF_MATCH_LUA = `
|
|
168
|
+
if redis.call('get', KEYS[1]) == ARGV[1] then
|
|
169
|
+
return redis.call('pexpire', KEYS[1], tonumber(ARGV[2]))
|
|
170
|
+
end
|
|
171
|
+
return 0
|
|
172
|
+
`.trim();
|
|
173
|
+
function randomToken() {
|
|
174
|
+
return (0, crypto_1.randomBytes)(16).toString("hex");
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Redis 操作封装(基于 node-redis v6)。
|
|
178
|
+
* 配置来自项目根目录 application*.yaml,模块加载及首次调用时自动连接。
|
|
179
|
+
*/
|
|
180
|
+
class Redis {
|
|
181
|
+
static async ensureReady() {
|
|
182
|
+
if (!this.client?.isReady) {
|
|
183
|
+
await this.connect();
|
|
184
|
+
}
|
|
185
|
+
if (!this.client?.isReady) {
|
|
186
|
+
throw new Error("Redis 连接不可用,请检查 YAML 中的 redis 配置");
|
|
187
|
+
}
|
|
188
|
+
return this.client;
|
|
189
|
+
}
|
|
190
|
+
/** 覆盖配置;已连接时需 disconnect 后再 connect 才生效 */
|
|
191
|
+
static configure(config = {}) {
|
|
192
|
+
this.config = {
|
|
193
|
+
...types_1.defaultRedisConfig,
|
|
194
|
+
...this.config,
|
|
195
|
+
...config,
|
|
196
|
+
tls: {
|
|
197
|
+
...this.config.tls,
|
|
198
|
+
...config.tls,
|
|
199
|
+
},
|
|
200
|
+
cluster: {
|
|
201
|
+
...this.config.cluster,
|
|
202
|
+
...config.cluster,
|
|
203
|
+
},
|
|
204
|
+
sentinel: {
|
|
205
|
+
...this.config.sentinel,
|
|
206
|
+
...config.sentinel,
|
|
207
|
+
},
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
static isReady() {
|
|
211
|
+
return !!this.client?.isReady;
|
|
212
|
+
}
|
|
213
|
+
/** PING;成功返回 true,失败抛错或返回 false(ping 自身异常时) */
|
|
214
|
+
static async ping() {
|
|
215
|
+
const client = await this.ensureReady();
|
|
216
|
+
const res = await client.ping();
|
|
217
|
+
return res === "PONG" || res === true || res === "OK";
|
|
218
|
+
}
|
|
219
|
+
static async connect(config) {
|
|
220
|
+
if (config)
|
|
221
|
+
this.configure(config);
|
|
222
|
+
if (this.client?.isReady)
|
|
223
|
+
return;
|
|
224
|
+
if (this.connecting)
|
|
225
|
+
return this.connecting;
|
|
226
|
+
this.connecting = (async () => {
|
|
227
|
+
const cfg = {
|
|
228
|
+
...types_1.defaultRedisConfig,
|
|
229
|
+
...this.config,
|
|
230
|
+
};
|
|
231
|
+
const pwd = String(cfg.password ?? "").trim();
|
|
232
|
+
if (!pwd) {
|
|
233
|
+
const production = config_1.configProfile === "production";
|
|
234
|
+
const msg = "[Redis] 未配置 password:仅建议本机开发使用;生产请设置 redis.password,并限制网络访问";
|
|
235
|
+
if (production) {
|
|
236
|
+
console.error(msg);
|
|
237
|
+
}
|
|
238
|
+
else {
|
|
239
|
+
console.warn(msg);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
if (this.client) {
|
|
243
|
+
try {
|
|
244
|
+
await this.client.quit();
|
|
245
|
+
}
|
|
246
|
+
catch {
|
|
247
|
+
try {
|
|
248
|
+
this.client.destroy();
|
|
249
|
+
}
|
|
250
|
+
catch {
|
|
251
|
+
// ignore
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
this.client = null;
|
|
255
|
+
}
|
|
256
|
+
if (this.subClient) {
|
|
257
|
+
try {
|
|
258
|
+
await this.subClient.quit();
|
|
259
|
+
}
|
|
260
|
+
catch {
|
|
261
|
+
try {
|
|
262
|
+
this.subClient.destroy();
|
|
263
|
+
}
|
|
264
|
+
catch {
|
|
265
|
+
// ignore
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
this.subClient = null;
|
|
269
|
+
}
|
|
270
|
+
const client = createRedisClient(cfg);
|
|
271
|
+
client.on("error", (err) => {
|
|
272
|
+
console.error("[Redis] error:", err.message);
|
|
273
|
+
});
|
|
274
|
+
client.on("reconnecting", () => {
|
|
275
|
+
console.warn("[Redis] reconnecting...");
|
|
276
|
+
});
|
|
277
|
+
await client.connect();
|
|
278
|
+
this.client = client;
|
|
279
|
+
const mode = cfg.mode ?? "standalone";
|
|
280
|
+
const tlsTag = cfg.tls?.enabled ? " tls" : "";
|
|
281
|
+
if (mode === "cluster") {
|
|
282
|
+
console.log(`[Redis] 已连接 cluster${tlsTag}`);
|
|
283
|
+
}
|
|
284
|
+
else if (mode === "sentinel") {
|
|
285
|
+
console.log(`[Redis] 已连接 sentinel name=${cfg.sentinel?.name ?? ""}${tlsTag}`);
|
|
286
|
+
}
|
|
287
|
+
else {
|
|
288
|
+
console.log(`[Redis] 已连接 ${cfg.host}:${cfg.port} db=${cfg.db ?? 0}${tlsTag}`);
|
|
289
|
+
}
|
|
290
|
+
})();
|
|
291
|
+
try {
|
|
292
|
+
await this.connecting;
|
|
293
|
+
}
|
|
294
|
+
finally {
|
|
295
|
+
this.connecting = null;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
static async disconnect() {
|
|
299
|
+
const close = async (c) => {
|
|
300
|
+
if (!c)
|
|
301
|
+
return;
|
|
302
|
+
try {
|
|
303
|
+
await c.quit();
|
|
304
|
+
}
|
|
305
|
+
catch {
|
|
306
|
+
try {
|
|
307
|
+
c.destroy();
|
|
308
|
+
}
|
|
309
|
+
catch {
|
|
310
|
+
// ignore
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
};
|
|
314
|
+
const main = this.client;
|
|
315
|
+
const sub = this.subClient;
|
|
316
|
+
this.client = null;
|
|
317
|
+
this.subClient = null;
|
|
318
|
+
await close(main);
|
|
319
|
+
await close(sub);
|
|
320
|
+
}
|
|
321
|
+
static async getClient() {
|
|
322
|
+
return this.ensureReady();
|
|
323
|
+
}
|
|
324
|
+
static key(raw) {
|
|
325
|
+
const prefix = this.config.keyPrefix ?? types_1.defaultRedisConfig.keyPrefix;
|
|
326
|
+
return raw.startsWith(prefix) ? raw : `${prefix}${raw}`;
|
|
327
|
+
}
|
|
328
|
+
// ─── String / JSON ───────────────────────────────────────────
|
|
329
|
+
static async set(key, value, ttlSeconds) {
|
|
330
|
+
const client = await this.ensureReady();
|
|
331
|
+
const k = this.key(key);
|
|
332
|
+
if (ttlSeconds && ttlSeconds > 0) {
|
|
333
|
+
await client.set(k, value, { EX: ttlSeconds });
|
|
334
|
+
}
|
|
335
|
+
else {
|
|
336
|
+
await client.set(k, value);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
static async setJSON(key, value, ttlSeconds) {
|
|
340
|
+
await this.set(key, JSON.stringify(value), ttlSeconds);
|
|
341
|
+
}
|
|
342
|
+
static async get(key) {
|
|
343
|
+
const client = await this.ensureReady();
|
|
344
|
+
return client.get(this.key(key));
|
|
345
|
+
}
|
|
346
|
+
static async getJSON(key) {
|
|
347
|
+
const raw = await this.get(key);
|
|
348
|
+
if (raw == null)
|
|
349
|
+
return null;
|
|
350
|
+
try {
|
|
351
|
+
return JSON.parse(raw);
|
|
352
|
+
}
|
|
353
|
+
catch {
|
|
354
|
+
return null;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
static async del(...keys) {
|
|
358
|
+
if (keys.length === 0)
|
|
359
|
+
return 0;
|
|
360
|
+
const client = await this.ensureReady();
|
|
361
|
+
return client.del(keys.map((k) => this.key(k)));
|
|
362
|
+
}
|
|
363
|
+
static async exists(key) {
|
|
364
|
+
const client = await this.ensureReady();
|
|
365
|
+
const n = await client.exists(this.key(key));
|
|
366
|
+
return n > 0;
|
|
367
|
+
}
|
|
368
|
+
static async expire(key, ttlSeconds) {
|
|
369
|
+
const client = await this.ensureReady();
|
|
370
|
+
const result = await client.expire(this.key(key), ttlSeconds);
|
|
371
|
+
return Number(result) === 1;
|
|
372
|
+
}
|
|
373
|
+
static async ttl(key) {
|
|
374
|
+
const client = await this.ensureReady();
|
|
375
|
+
return client.ttl(this.key(key));
|
|
376
|
+
}
|
|
377
|
+
static async setNX(key, value, ttlSeconds) {
|
|
378
|
+
const client = await this.ensureReady();
|
|
379
|
+
const k = this.key(key);
|
|
380
|
+
const result = await client.set(k, value, {
|
|
381
|
+
NX: true,
|
|
382
|
+
...(ttlSeconds && ttlSeconds > 0 ? { EX: ttlSeconds } : {}),
|
|
383
|
+
});
|
|
384
|
+
return result === "OK";
|
|
385
|
+
}
|
|
386
|
+
static async setNxPx(key, value, ttlMs) {
|
|
387
|
+
const client = await this.ensureReady();
|
|
388
|
+
const result = await client.set(this.key(key), value, {
|
|
389
|
+
NX: true,
|
|
390
|
+
PX: Math.max(1, ttlMs),
|
|
391
|
+
});
|
|
392
|
+
return result === "OK";
|
|
393
|
+
}
|
|
394
|
+
static async unlockIfMatch(key, expectValue) {
|
|
395
|
+
const client = await this.ensureReady();
|
|
396
|
+
const result = (await client.eval(UNLOCK_IF_MATCH_LUA, {
|
|
397
|
+
keys: [this.key(key)],
|
|
398
|
+
arguments: [expectValue],
|
|
399
|
+
}));
|
|
400
|
+
return Number(result) === 1;
|
|
401
|
+
}
|
|
402
|
+
/** 值匹配时续期(毫秒) */
|
|
403
|
+
static async renewIfMatch(key, expectValue, ttlMs) {
|
|
404
|
+
const client = await this.ensureReady();
|
|
405
|
+
const result = (await client.eval(RENEW_IF_MATCH_LUA, {
|
|
406
|
+
keys: [this.key(key)],
|
|
407
|
+
arguments: [expectValue, String(Math.max(1, ttlMs))],
|
|
408
|
+
}));
|
|
409
|
+
return Number(result) === 1;
|
|
410
|
+
}
|
|
411
|
+
/**
|
|
412
|
+
* 尝试获取锁。
|
|
413
|
+
* @returns 句柄;失败返回 null
|
|
414
|
+
*/
|
|
415
|
+
static async tryLock(key, option = {}) {
|
|
416
|
+
const ttlMs = option.ttlMs ?? 30000;
|
|
417
|
+
const token = option.token ?? randomToken();
|
|
418
|
+
const lockKey = key.startsWith(types_1.LOCK_KEY) ? key : `${types_1.LOCK_KEY}${key}`;
|
|
419
|
+
const ok = await this.setNxPx(lockKey, token, ttlMs);
|
|
420
|
+
if (!ok)
|
|
421
|
+
return null;
|
|
422
|
+
return {
|
|
423
|
+
key: lockKey,
|
|
424
|
+
token,
|
|
425
|
+
release: () => this.unlockIfMatch(lockKey, token),
|
|
426
|
+
renew: (ms) => this.renewIfMatch(lockKey, token, ms),
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
/**
|
|
430
|
+
* 持锁执行;可选自动续租。拿不到锁抛 LockError。
|
|
431
|
+
*/
|
|
432
|
+
static async withLock(key, fn, option = {}) {
|
|
433
|
+
const ttlMs = option.ttlMs ?? 30000;
|
|
434
|
+
const handle = await this.tryLock(key, { ttlMs, token: option.token });
|
|
435
|
+
if (!handle) {
|
|
436
|
+
throw new errors_1.LockError(`获取锁失败: ${key}`);
|
|
437
|
+
}
|
|
438
|
+
const renewEvery = option.renewIntervalMs === undefined
|
|
439
|
+
? Math.max(100, Math.floor(ttlMs / 3))
|
|
440
|
+
: option.renewIntervalMs;
|
|
441
|
+
let timer = null;
|
|
442
|
+
if (renewEvery > 0) {
|
|
443
|
+
timer = setInterval(() => {
|
|
444
|
+
void handle.renew(ttlMs).catch(() => {
|
|
445
|
+
// 续租失败忽略;业务侧应尽量短持锁
|
|
446
|
+
});
|
|
447
|
+
}, renewEvery);
|
|
448
|
+
if (typeof timer === "object" && "unref" in timer) {
|
|
449
|
+
timer.unref?.();
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
try {
|
|
453
|
+
return await fn();
|
|
454
|
+
}
|
|
455
|
+
finally {
|
|
456
|
+
if (timer)
|
|
457
|
+
clearInterval(timer);
|
|
458
|
+
await handle.release();
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
// ─── Hash ────────────────────────────────────────────────────
|
|
462
|
+
static async hSet(key, field, value) {
|
|
463
|
+
const client = await this.ensureReady();
|
|
464
|
+
return client.hSet(this.key(key), field, value);
|
|
465
|
+
}
|
|
466
|
+
static async hGet(key, field) {
|
|
467
|
+
const client = await this.ensureReady();
|
|
468
|
+
const v = await client.hGet(this.key(key), field);
|
|
469
|
+
return v ?? undefined;
|
|
470
|
+
}
|
|
471
|
+
static async hGetAll(key) {
|
|
472
|
+
const client = await this.ensureReady();
|
|
473
|
+
return client.hGetAll(this.key(key));
|
|
474
|
+
}
|
|
475
|
+
static async hDel(key, ...fields) {
|
|
476
|
+
if (fields.length === 0)
|
|
477
|
+
return 0;
|
|
478
|
+
const client = await this.ensureReady();
|
|
479
|
+
return client.hDel(this.key(key), fields);
|
|
480
|
+
}
|
|
481
|
+
// ─── Set ─────────────────────────────────────────────────────
|
|
482
|
+
static async sAdd(key, ...members) {
|
|
483
|
+
if (members.length === 0)
|
|
484
|
+
return 0;
|
|
485
|
+
const client = await this.ensureReady();
|
|
486
|
+
return client.sAdd(this.key(key), members);
|
|
487
|
+
}
|
|
488
|
+
static async sMembers(key) {
|
|
489
|
+
const client = await this.ensureReady();
|
|
490
|
+
return client.sMembers(this.key(key));
|
|
491
|
+
}
|
|
492
|
+
static async sRem(key, ...members) {
|
|
493
|
+
if (members.length === 0)
|
|
494
|
+
return 0;
|
|
495
|
+
const client = await this.ensureReady();
|
|
496
|
+
return client.sRem(this.key(key), members);
|
|
497
|
+
}
|
|
498
|
+
// ─── List ────────────────────────────────────────────────────
|
|
499
|
+
static async lPush(key, ...values) {
|
|
500
|
+
if (values.length === 0)
|
|
501
|
+
return 0;
|
|
502
|
+
const client = await this.ensureReady();
|
|
503
|
+
return client.lPush(this.key(key), values);
|
|
504
|
+
}
|
|
505
|
+
static async rPush(key, ...values) {
|
|
506
|
+
if (values.length === 0)
|
|
507
|
+
return 0;
|
|
508
|
+
const client = await this.ensureReady();
|
|
509
|
+
return client.rPush(this.key(key), values);
|
|
510
|
+
}
|
|
511
|
+
static async lPop(key) {
|
|
512
|
+
const client = await this.ensureReady();
|
|
513
|
+
return client.lPop(this.key(key));
|
|
514
|
+
}
|
|
515
|
+
static async rPop(key) {
|
|
516
|
+
const client = await this.ensureReady();
|
|
517
|
+
return client.rPop(this.key(key));
|
|
518
|
+
}
|
|
519
|
+
static async lRange(key, start, stop) {
|
|
520
|
+
const client = await this.ensureReady();
|
|
521
|
+
return client.lRange(this.key(key), start, stop);
|
|
522
|
+
}
|
|
523
|
+
static async lLen(key) {
|
|
524
|
+
const client = await this.ensureReady();
|
|
525
|
+
return client.lLen(this.key(key));
|
|
526
|
+
}
|
|
527
|
+
// ─── ZSet ────────────────────────────────────────────────────
|
|
528
|
+
static async zAdd(key, members) {
|
|
529
|
+
if (members.length === 0)
|
|
530
|
+
return 0;
|
|
531
|
+
const client = await this.ensureReady();
|
|
532
|
+
return client.zAdd(this.key(key), members.map((m) => ({ score: m.score, value: m.value })));
|
|
533
|
+
}
|
|
534
|
+
static async zRange(key, min, max) {
|
|
535
|
+
const client = await this.ensureReady();
|
|
536
|
+
return client.zRange(this.key(key), min, max);
|
|
537
|
+
}
|
|
538
|
+
static async zRangeByScore(key, min, max) {
|
|
539
|
+
const client = await this.ensureReady();
|
|
540
|
+
return client.zRangeByScore(this.key(key), min, max);
|
|
541
|
+
}
|
|
542
|
+
static async zRem(key, ...members) {
|
|
543
|
+
if (members.length === 0)
|
|
544
|
+
return 0;
|
|
545
|
+
const client = await this.ensureReady();
|
|
546
|
+
return client.zRem(this.key(key), members);
|
|
547
|
+
}
|
|
548
|
+
static async zCard(key) {
|
|
549
|
+
const client = await this.ensureReady();
|
|
550
|
+
return client.zCard(this.key(key));
|
|
551
|
+
}
|
|
552
|
+
static async zScore(key, member) {
|
|
553
|
+
const client = await this.ensureReady();
|
|
554
|
+
const s = await client.zScore(this.key(key), member);
|
|
555
|
+
return s == null ? null : Number(s);
|
|
556
|
+
}
|
|
557
|
+
// ─── Pipeline ────────────────────────────────────────────────
|
|
558
|
+
/**
|
|
559
|
+
* 批量命令。fn 内对 multi 链式调用,结束后自动 exec。
|
|
560
|
+
* key 需自行 `Redis.key(...)` 加前缀。
|
|
561
|
+
*/
|
|
562
|
+
static async pipeline(fn) {
|
|
563
|
+
const client = await this.ensureReady();
|
|
564
|
+
const multi = client.multi();
|
|
565
|
+
fn(multi);
|
|
566
|
+
return (await multi.exec());
|
|
567
|
+
}
|
|
568
|
+
// ─── Pub/Sub ─────────────────────────────────────────────────
|
|
569
|
+
static async publish(channel, message) {
|
|
570
|
+
const client = await this.ensureReady();
|
|
571
|
+
return client.publish(channel, message);
|
|
572
|
+
}
|
|
573
|
+
/**
|
|
574
|
+
* 订阅频道。返回取消订阅函数。
|
|
575
|
+
* 内部使用独立连接,避免阻塞主 client。
|
|
576
|
+
*/
|
|
577
|
+
static async subscribe(channel, listener) {
|
|
578
|
+
await this.ensureReady();
|
|
579
|
+
if (!this.subClient?.isReady) {
|
|
580
|
+
const cfg = { ...types_1.defaultRedisConfig, ...this.config };
|
|
581
|
+
const sub = createRedisClient(cfg);
|
|
582
|
+
sub.on("error", (err) => {
|
|
583
|
+
console.error("[Redis] sub error:", err.message);
|
|
584
|
+
});
|
|
585
|
+
await sub.connect();
|
|
586
|
+
this.subClient = sub;
|
|
587
|
+
}
|
|
588
|
+
const sub = this.subClient;
|
|
589
|
+
await sub.subscribe(channel, (message, ch) => {
|
|
590
|
+
listener(message, ch);
|
|
591
|
+
});
|
|
592
|
+
return async () => {
|
|
593
|
+
try {
|
|
594
|
+
await sub.unsubscribe(channel);
|
|
595
|
+
}
|
|
596
|
+
catch {
|
|
597
|
+
// ignore
|
|
598
|
+
}
|
|
599
|
+
};
|
|
600
|
+
}
|
|
601
|
+
// ─── Counter / PX ────────────────────────────────────────────
|
|
602
|
+
static async incr(key) {
|
|
603
|
+
const client = await this.ensureReady();
|
|
604
|
+
return client.incr(this.key(key));
|
|
605
|
+
}
|
|
606
|
+
static async decr(key) {
|
|
607
|
+
const client = await this.ensureReady();
|
|
608
|
+
return client.decr(this.key(key));
|
|
609
|
+
}
|
|
610
|
+
static async setPx(key, value, ttlMs) {
|
|
611
|
+
const client = await this.ensureReady();
|
|
612
|
+
await client.set(this.key(key), value, { PX: Math.max(1, ttlMs) });
|
|
613
|
+
}
|
|
614
|
+
static async setJSONPx(key, value, ttlMs) {
|
|
615
|
+
await this.setPx(key, JSON.stringify(value), ttlMs);
|
|
616
|
+
}
|
|
617
|
+
/**
|
|
618
|
+
* 固定窗口限流。
|
|
619
|
+
* @returns 当前窗口内计数
|
|
620
|
+
*/
|
|
621
|
+
static async rateLimit(key, timeSeconds, count) {
|
|
622
|
+
const client = await this.ensureReady();
|
|
623
|
+
const result = (await client.eval(RATE_LIMIT_LUA, {
|
|
624
|
+
keys: [this.key(key)],
|
|
625
|
+
arguments: [String(timeSeconds), String(count)],
|
|
626
|
+
}));
|
|
627
|
+
return Number(result ?? 0);
|
|
628
|
+
}
|
|
629
|
+
/**
|
|
630
|
+
* 滑动窗口限流(ZSET)。
|
|
631
|
+
* @returns 当前窗口内数量(含本次尝试)
|
|
632
|
+
*/
|
|
633
|
+
static async rateLimitSliding(key, timeSeconds, count) {
|
|
634
|
+
const client = await this.ensureReady();
|
|
635
|
+
const now = Date.now();
|
|
636
|
+
const windowMs = Math.max(1, Math.floor(timeSeconds * 1000));
|
|
637
|
+
const result = (await client.eval(SLIDING_RATE_LIMIT_LUA, {
|
|
638
|
+
keys: [this.key(key)],
|
|
639
|
+
arguments: [
|
|
640
|
+
String(now),
|
|
641
|
+
String(windowMs),
|
|
642
|
+
String(count),
|
|
643
|
+
randomToken().slice(0, 8),
|
|
644
|
+
],
|
|
645
|
+
}));
|
|
646
|
+
return Number(result ?? 0);
|
|
647
|
+
}
|
|
648
|
+
/** 按前缀扫描删除(@CacheEvict allEntries);小规模缓存用 */
|
|
649
|
+
static async deleteByPrefix(prefix) {
|
|
650
|
+
const client = await this.ensureReady();
|
|
651
|
+
const full = this.key(prefix);
|
|
652
|
+
let cursor = "0";
|
|
653
|
+
let removed = 0;
|
|
654
|
+
do {
|
|
655
|
+
const reply = await client.scan(cursor, {
|
|
656
|
+
MATCH: `${full}*`,
|
|
657
|
+
COUNT: 100,
|
|
658
|
+
});
|
|
659
|
+
cursor = String(reply.cursor);
|
|
660
|
+
const keys = reply.keys;
|
|
661
|
+
if (keys.length) {
|
|
662
|
+
removed += await client.del(keys);
|
|
663
|
+
}
|
|
664
|
+
} while (cursor !== "0");
|
|
665
|
+
return removed;
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
Redis.client = null;
|
|
669
|
+
Redis.config = {
|
|
670
|
+
...types_1.defaultRedisConfig,
|
|
671
|
+
...config_1.redisConfig,
|
|
672
|
+
};
|
|
673
|
+
Redis.connecting = null;
|
|
674
|
+
/** Pub/Sub 专用连接(subscribe 会阻塞普通命令) */
|
|
675
|
+
Redis.subClient = null;
|
|
676
|
+
// 不在 import 时连 Redis:避免启动被连接超时拖慢;首次 API 调用走 ensureReady()
|
|
677
|
+
exports.default = Redis;
|
|
678
|
+
//# sourceMappingURL=Redis.js.map
|