-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathredis-cache-adapter.js
More file actions
267 lines (239 loc) · 7.78 KB
/
redis-cache-adapter.js
File metadata and controls
267 lines (239 loc) · 7.78 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
/**
* Redis 缓存适配器:将 Redis 封装为 CacheLike 接口
* 支持直接传入 Redis URL 字符串或 ioredis 实例
*
* @example
* const { createRedisCacheAdapter } = require('monsqlize/lib/redis-cache-adapter');
*
* // 方式 1:传入 URL 字符串(推荐)
* const cache = createRedisCacheAdapter('redis://localhost:6379/0');
*
* // 方式 2:传入 ioredis 实例
* const Redis = require('ioredis');
* const redis = new Redis('redis://localhost:6379/0');
* const cache = createRedisCacheAdapter(redis);
*/
/**
* 创建 Redis 缓存适配器
* @param {string|Object} redisUrlOrInstance - Redis URL 字符串或 ioredis 实例
* @returns {Object} 实现了 CacheLike 接口的缓存对象
*/
function createRedisCacheAdapter(redisUrlOrInstance) {
let redis;
let shouldCloseOnDestroy = false;
// 如果传入的是字符串,自动创建 Redis 实例
if (typeof redisUrlOrInstance === 'string') {
try {
const IORedis = require('ioredis');
redis = new IORedis(redisUrlOrInstance);
shouldCloseOnDestroy = true;
} catch (error) {
throw new Error(
'ioredis 未安装。请运行: npm install ioredis\n' +
'或传入已创建的 ioredis 实例'
);
}
} else if (redisUrlOrInstance && typeof redisUrlOrInstance === 'object') {
// 传入的是 ioredis 实例
redis = redisUrlOrInstance;
shouldCloseOnDestroy = false;
} else {
throw new Error('redisUrlOrInstance 必须是 Redis URL 字符串或 ioredis 实例');
}
// 实现 CacheLike 接口的 10 个方法
return {
/**
* 获取单个缓存值
* @param {string} key
* @returns {Promise<any>}
*/
async get(key) {
try {
const val = await redis.get(key);
return val ? JSON.parse(val) : undefined;
} catch (error) {
// 解析失败返回 undefined
return undefined;
}
},
/**
* 获取单个缓存值及剩余 TTL(毫秒),供 MultiLevelCache L2→L1 回填时携带正确 TTL
* 使用 pipeline(GET + PTTL)单次 RTT,避免额外网络开销
* @param {string} key
* @returns {Promise<{value: any, remainingTTL: number}|undefined>}
* key 不存在返回 undefined;value 为 null 时表示缓存了空结果
* @since 1.1.9
*/
async getWithTTL(key) {
try {
const [[, rawVal], [, pttl]] = await redis.pipeline().get(key).pttl(key).exec();
// PTTL = -2 表示 key 不存在
if (pttl === -2) return undefined;
let value;
try {
value = JSON.parse(rawVal);
} catch (_) {
return undefined;
}
return {
value,
// PTTL = -1 表示永不过期,映射为 0(与 backfillLocalTTL=0 语义一致)
remainingTTL: pttl > 0 ? pttl : 0,
};
} catch (_) {
return undefined;
}
},
/**
* 设置单个缓存值
* @param {string} key
* @param {any} val
* @param {number} ttl - TTL(毫秒)
* @returns {Promise<void>}
*/
async set(key, val, ttl = 0) {
const str = JSON.stringify(val);
if (ttl > 0) {
await redis.psetex(key, ttl, str);
} else {
await redis.set(key, str);
}
},
/**
* 删除单个缓存项
* @param {string} key
* @returns {Promise<boolean>}
*/
async del(key) {
const result = await redis.del(key);
return result > 0;
},
/**
* 检查键是否存在
* @param {string} key
* @returns {Promise<boolean>}
*/
async exists(key) {
const result = await redis.exists(key);
return result > 0;
},
/**
* 批量获取
* @param {string[]} keys
* @returns {Promise<Object>}
*/
async getMany(keys) {
if (!keys || keys.length === 0) return {};
const values = await redis.mget(keys);
const result = {};
keys.forEach((key, i) => {
if (values[i]) {
try {
result[key] = JSON.parse(values[i]);
} catch {
// 解析失败跳过
}
}
});
return result;
},
/**
* 批量设置
* @param {Object} obj - 键值对对象
* @param {number} ttl - TTL(毫秒)
* @returns {Promise<boolean>}
*/
async setMany(obj, ttl = 0) {
if (!obj || Object.keys(obj).length === 0) return true;
const pipeline = redis.pipeline();
for (const [key, val] of Object.entries(obj)) {
const str = JSON.stringify(val);
if (ttl > 0) {
pipeline.psetex(key, ttl, str);
} else {
pipeline.set(key, str);
}
}
await pipeline.exec();
return true;
},
/**
* 批量删除
* @param {string[]} keys
* @returns {Promise<number>}
*/
async delMany(keys) {
if (!keys || keys.length === 0) return 0;
return await redis.del(...keys);
},
/**
* 按模式删除(支持通配符 *)
* @param {string} pattern
* @returns {Promise<number>}
*/
async delPattern(pattern) {
// 使用 SCAN 避免阻塞(生产环境推荐)
let cursor = '0';
let deletedCount = 0;
do {
const [nextCursor, keys] = await redis.scan(
cursor,
'MATCH', pattern,
'COUNT', 100
);
cursor = nextCursor;
if (keys.length > 0) {
deletedCount += await redis.del(...keys);
}
} while (cursor !== '0');
return deletedCount;
},
/**
* 清空所有缓存(谨慎使用)
* @returns {Promise<void>}
*/
async clear() {
await redis.flushdb();
},
/**
* 获取所有键(可选模式匹配)
* @param {string} pattern
* @returns {Promise<string[]>}
*/
async keys(pattern = '*') {
// 使用 SCAN 避免阻塞
const allKeys = [];
let cursor = '0';
do {
const [nextCursor, keys] = await redis.scan(
cursor,
'MATCH', pattern,
'COUNT', 100
);
cursor = nextCursor;
allKeys.push(...keys);
} while (cursor !== '0');
return allKeys;
},
/**
* 关闭 Redis 连接(仅当自动创建时才会关闭)
* @returns {Promise<void>}
*/
async close() {
if (shouldCloseOnDestroy && redis) {
try {
await redis.quit();
} catch {
// 忽略关闭错误
}
}
},
/**
* 获取底层 Redis 实例(用于高级操作)
*/
getRedisInstance() {
return redis;
}
};
}
module.exports = { createRedisCacheAdapter };