mm_redis 2.1.8 → 2.3.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/index.js CHANGED
@@ -1,16 +1,16 @@
1
1
  const Redis = require('./lib/redis');
2
- const Admin = require('mm_corejs').Admin;
2
+ const { reg } = require('mm_admin');
3
3
 
4
- let admin = new Admin();
4
+ const admin = reg('mm_redis');
5
5
  admin.setClass(Redis);
6
6
 
7
7
  module.exports = {
8
8
  Redis,
9
9
  admin,
10
10
  redisAdmin(scope, config) {
11
- if (!scope) {
12
- scope = 'os';
13
- }
14
- return admin.get(scope, config);
11
+ return admin.get(scope || 'os', config);
15
12
  }
16
- };
13
+ };
14
+ /* mm-tool-logic-export 工具能力层:其它模块可 require("<pkg>").tools.<name> 直接引用 */
15
+ Object.defineProperty(module.exports, 'tools', { enumerable: true, configurable: true,
16
+ get: function () { return require('./lib/tools'); } });
@@ -0,0 +1,16 @@
1
+ 'use strict';
2
+ const fs = require('fs');
3
+ const path = require('path');
4
+ /** 模块能力层:懒加载工具实现,避免模块入口被 require 时加载全部实现 */
5
+ module.exports = new Proxy({}, {
6
+ get(_t, key) {
7
+ if (typeof key !== 'string') return undefined;
8
+ if (!fs.existsSync(path.join(__dirname, key, 'index.js'))) return undefined;
9
+ return require('./' + key);
10
+ },
11
+ has(_t, key) {
12
+ return typeof key === 'string' && fs.existsSync(path.join(__dirname, key, 'index.js'));
13
+ },
14
+ ownKeys() { return fs.readdirSync(__dirname, { withFileTypes: true }).filter(e => e.isDirectory()).map(e => e.name); },
15
+ getOwnPropertyDescriptor(_t, key) { return this.has(_t, key) ? { enumerable: true, configurable: true } : undefined; }
16
+ });
@@ -0,0 +1,115 @@
1
+ /**
2
+ * 队列发信器
3
+ * 发送消息到消息队列(redis v4 官方包,与 mm_redis 共用底层库)
4
+ */
5
+
6
+ /**
7
+ * 获取 Redis 客户端(复用 mm_redis 连接池,不可用时自建独立连接)
8
+ * @param {object} opts 配置选项
9
+ * @returns {object} { client, release } 或 null
10
+ */
11
+ async function _getClient(opts) {
12
+ var opts2 = { ...opts };
13
+ // 统一 db/database 字段,mm_redis 读取的是 database 字段
14
+ if (opts2.db !== undefined && opts2.database === undefined) {
15
+ opts2.database = opts2.db;
16
+ }
17
+
18
+ var redis_admin = null;
19
+ try {
20
+ var { redisAdmin } = require('mm_redis');
21
+ if (redisAdmin) {
22
+ // 与 queue 管理器共用 queue 作用域,保证发布/消费配置一致
23
+ redis_admin = redisAdmin('queue', opts2);
24
+ }
25
+ } catch (e) {
26
+ // mm_redis 不可用时使用传入的 opts
27
+ }
28
+
29
+ // 优先复用 mm_redis 连接池,保证配置一致且不新建独立连接
30
+ if (redis_admin && typeof redis_admin._getConnection === 'function') {
31
+ var conn = await redis_admin._getConnection();
32
+ return {
33
+ client: conn,
34
+ release: function () {
35
+ try {
36
+ redis_admin._releaseConnection(conn);
37
+ } catch (e) {
38
+ void e;
39
+ }
40
+ }
41
+ };
42
+ }
43
+
44
+ // 回退: mm_redis 不可用,自建独立连接
45
+ var redis_mod = require('redis');
46
+ if (!redis_mod || !redis_mod.createClient) {
47
+ return null;
48
+ }
49
+ var createClient = redis_mod.createClient;
50
+ var client = createClient({
51
+ socket: {
52
+ host: opts2.host || 'localhost',
53
+ port: opts2.port || 6379,
54
+ connectTimeout: opts2.connect_timeout || 5000,
55
+ reconnectStrategy: false
56
+ },
57
+ password: opts2.password || '',
58
+ database: opts2.database || 0
59
+ });
60
+ await client.connect();
61
+ return {
62
+ client: client,
63
+ release: function () {
64
+ try {
65
+ client.quit();
66
+ } catch (e) {
67
+ void e;
68
+ }
69
+ }
70
+ };
71
+ }
72
+
73
+ /**
74
+ * 主方法
75
+ * @param {object} ctx 上下文对象
76
+ * @param {object} params 步骤参数
77
+ * @returns {object} 发送结果
78
+ */
79
+ async function main(params, ctx, options) {
80
+ var opts = (this && typeof this.getOptions === 'function') ? this.getOptions() : {};
81
+ var driver = opts.driver || 'redis';
82
+
83
+ if (driver !== 'redis') {
84
+ throw new Error('不支持的队列驱动: ' + driver);
85
+ }
86
+
87
+ var queue_name = params.queue_name || '';
88
+ var data = params.data;
89
+
90
+ if (!queue_name) {
91
+ ctx.output = { error: { code: 'QUEUE_NO_NAME', message: '队列名称不能为空' } };
92
+ return ctx.output;
93
+ }
94
+
95
+ var holder = await _getClient(opts);
96
+ if (!holder || !holder.client) {
97
+ throw new Error('队列发信需要 redis 库,请手动执行: npm install redis');
98
+ }
99
+ var client = holder.client;
100
+ try {
101
+ await client.lPush(queue_name, JSON.stringify(data));
102
+ } finally {
103
+ await holder.release();
104
+ }
105
+
106
+ ctx.output = {
107
+ sent: true,
108
+ tip: '消息已入队',
109
+ queue_name: queue_name,
110
+ message: data
111
+ };
112
+ return ctx.output;
113
+ }
114
+
115
+ module.exports = { main };
@@ -0,0 +1,395 @@
1
+ /**
2
+ * 消息队列管理器
3
+ * 管理消息队列的通用操作,支持发布、消费、确认、重试、死信、延迟、批量消费
4
+ * 驱动: Redis (redis v4 官方包,与 mm_redis 共用底层库)
5
+ */
6
+
7
+ /**
8
+ * 带超时的 Promise 竞争,超时后抛出错误
9
+ * @param {Promise} promise 原始 Promise
10
+ * @param {number} timeout_ms 超时毫秒数
11
+ * @returns {Promise} 竞争结果
12
+ * @throws {Error} 超时时抛出
13
+ */
14
+ function _withTimeout(promise, timeout_ms) {
15
+ var timer = null;
16
+ var timeout_promise = new Promise(function (resolve, reject) {
17
+ timer = setTimeout(function () {
18
+ reject(new Error('Redis 连接超时'));
19
+ }, timeout_ms);
20
+ });
21
+ return Promise.race([promise, timeout_promise]).finally(function () {
22
+ clearTimeout(timer);
23
+ });
24
+ }
25
+
26
+ /**
27
+ * 获取 Redis 客户端(通过 mm_redis 的 redisAdmin 获取配置,创建独立连接)
28
+ * @param {object} opts 配置选项
29
+ * @returns {object} Redis 客户端
30
+ * @throws {Error} 连接失败或超时时抛出
31
+ */
32
+ async function _getClient(opts) {
33
+ var opts2 = { ...opts };
34
+ // 统一 db/database 字段,mm_redis 读取的是 database 字段
35
+ if (opts2.db !== undefined && opts2.database === undefined) {
36
+ opts2.database = opts2.db;
37
+ }
38
+
39
+ var redis_admin = null;
40
+ try {
41
+ var { redisAdmin } = require('mm_redis');
42
+ if (redisAdmin) {
43
+ // 使用独立作用域,避免与 redis 管理器共用 sys 池导致本模块配置被覆盖
44
+ redis_admin = redisAdmin('queue', opts2);
45
+ }
46
+ } catch (e) {
47
+ // mm_redis 不可用时使用传入的 opts
48
+ }
49
+
50
+ // 优先复用 mm_redis 连接池,保证配置一致且不新建独立连接
51
+ // 使用超时限制,避免 Redis 不可用时连接无限挂起
52
+ if (redis_admin && typeof redis_admin._getConnection === 'function') {
53
+ var conn_promise = redis_admin._getConnection();
54
+ var conn;
55
+ try {
56
+ conn = await _withTimeout(conn_promise, opts2.connect_timeout || 3000);
57
+ } catch (e) {
58
+ // 超时后底层连接可能仍在创建,创建完成后强制回收,避免连接泄漏
59
+ conn_promise.then(function (c) {
60
+ try {
61
+ redis_admin._releaseConnection(c);
62
+ } catch (e2) {
63
+ void e2;
64
+ }
65
+ }).catch(function (e2) {
66
+ void e2;
67
+ });
68
+ throw e;
69
+ }
70
+ return {
71
+ client: conn,
72
+ release: function () {
73
+ try {
74
+ redis_admin._releaseConnection(conn);
75
+ } catch (e2) {
76
+ void e2;
77
+ }
78
+ }
79
+ };
80
+ }
81
+
82
+ // 回退: mm_redis 不可用,自建独立连接
83
+ // 禁用自动重连,连接失败时立即抛出,避免无限重连挂起
84
+ var redis_mod = require('redis');
85
+ if (!redis_mod || !redis_mod.createClient) {
86
+ return null;
87
+ }
88
+ var createClient = redis_mod.createClient;
89
+ var client = createClient({
90
+ socket: {
91
+ host: opts2.host || 'localhost',
92
+ port: opts2.port || 6379,
93
+ connectTimeout: opts2.connect_timeout || 5000,
94
+ reconnectStrategy: false
95
+ },
96
+ password: opts2.password || '',
97
+ database: opts2.database || 0
98
+ });
99
+ try {
100
+ await client.connect();
101
+ } catch (e) {
102
+ try {
103
+ await client.quit();
104
+ } catch (e2) {
105
+ void e2;
106
+ }
107
+ throw new Error('Redis 连接失败: ' + (e && e.message ? e.message : String(e)));
108
+ }
109
+ return {
110
+ client: client,
111
+ release: function () {
112
+ try {
113
+ client.quit();
114
+ } catch (e2) {
115
+ void e2;
116
+ }
117
+ }
118
+ };
119
+ }
120
+
121
+ /**
122
+ * 发布消息到队列
123
+ * @param {object} client Redis 客户端
124
+ * @param {string} queue_name 队列名称
125
+ * @param {*} message 消息内容
126
+ * @returns {object} 发布结果
127
+ */
128
+ async function _publish(client, queue_name, message) {
129
+ // lPush 返回的是列表长度而非消息ID,这里生成唯一消息ID
130
+ var message_id = Date.now().toString(36) + '-' + Math.random().toString(36).substr(2, 9);
131
+ await client.lPush(queue_name, JSON.stringify(message));
132
+ return { msg: '消息发布成功', message_id: message_id, queue: queue_name };
133
+ }
134
+
135
+ /**
136
+ * 消费队列消息
137
+ * @param {object} client Redis 客户端
138
+ * @param {string} queue_name 队列名称
139
+ * @returns {object} 消费结果
140
+ */
141
+ async function _consume(client, queue_name) {
142
+ var raw = await client.rPopLPush(queue_name, queue_name + ':processing');
143
+ if (!raw) {
144
+ return { obj: null, queue: queue_name };
145
+ }
146
+ var parsed = JSON.parse(raw);
147
+ return { obj: parsed, queue: queue_name, _raw: raw };
148
+ }
149
+
150
+ /**
151
+ * 批量消费队列消息
152
+ * @param {object} client Redis 客户端
153
+ * @param {string} queue_name 队列名称
154
+ * @param {number} count 消费数量
155
+ * @returns {object} 批量消费结果
156
+ */
157
+ async function _batchConsume(client, queue_name, count) {
158
+ var list = [];
159
+ for (var i = 0; i < count; i++) {
160
+ var raw = await client.rPopLPush(queue_name, queue_name + ':processing');
161
+ if (!raw) break;
162
+ list.push(JSON.parse(raw));
163
+ }
164
+ return { list: list, count: list.length, queue: queue_name };
165
+ }
166
+
167
+ /**
168
+ * 确认消费(删除处理中消息)
169
+ * @param {object} client Redis 客户端
170
+ * @param {string} queue_name 队列名称
171
+ * @param {string} message_raw 原始消息字符串
172
+ * @returns {object} 确认结果
173
+ */
174
+ async function _ack(client, queue_name, message_raw) {
175
+ if (!message_raw) {
176
+ return { error: { code: 'QUEUE_ACK_NO_MSG', message: '确认消费需要传入 _raw 消息原文' } };
177
+ }
178
+ var removed = await client.lRem(queue_name + ':processing', 1, message_raw);
179
+ return { msg: removed > 0 ? '消息已确认' : '消息未找到', queue: queue_name };
180
+ }
181
+
182
+ /**
183
+ * 消费失败重试(移回主队列)
184
+ * @param {object} client Redis 客户端
185
+ * @param {string} queue_name 队列名称
186
+ * @param {string} message_raw 原始消息字符串
187
+ * @param {number} max_retries 最大重试次数
188
+ * @returns {object} 重试结果
189
+ */
190
+ async function _retry(client, queue_name, message_raw, max_retries) {
191
+ if (!message_raw) {
192
+ return { error: { code: 'QUEUE_RETRY_NO_MSG', message: '重试需要传入 _raw 消息原文' } };
193
+ }
194
+ var max_retry = max_retries || 3;
195
+ var parsed = JSON.parse(message_raw);
196
+ var retry_count = (parsed._retry_count || 0) + 1;
197
+ if (retry_count > max_retry) {
198
+ await client.lRem(queue_name + ':processing', 1, message_raw);
199
+ parsed._retry_count = retry_count;
200
+ parsed._dlq_at = new Date().toISOString();
201
+ await client.lPush(queue_name + ':dlq', JSON.stringify(parsed));
202
+ return { msg: '已达最大重试次数,移入死信队列', retry_count: retry_count, queue: queue_name };
203
+ }
204
+ parsed._retry_count = retry_count;
205
+ await client.lRem(queue_name + ':processing', 1, message_raw);
206
+ await client.lPush(queue_name, JSON.stringify(parsed));
207
+ return { msg: '消息已重新入队', retry_count: retry_count, queue: queue_name };
208
+ }
209
+
210
+ /**
211
+ * 死信队列操作
212
+ * @param {object} client Redis 客户端
213
+ * @param {string} queue_name 队列名称
214
+ * @param {string} dlq_action 死信操作: view/replay/clear
215
+ * @returns {object} 死信队列结果
216
+ */
217
+ async function _dlq(client, queue_name, dlq_action) {
218
+ var dlq_key = queue_name + ':dlq';
219
+ switch (dlq_action) {
220
+ case 'view': {
221
+ var all = await client.lRange(dlq_key, 0, -1);
222
+ var list = (all || []).map(function (r) { return JSON.parse(r); });
223
+ return { list: list, count: list.length, queue: queue_name };
224
+ }
225
+ case 'replay': {
226
+ var moved = 0;
227
+ var msg;
228
+ while ((msg = await client.rPopLPush(dlq_key, queue_name)) !== null) {
229
+ moved++;
230
+ }
231
+ return { msg: '已重放 ' + moved + ' 条死信消息到主队列', count: moved, queue: queue_name };
232
+ }
233
+ case 'clear': {
234
+ var deleted = await client.del(dlq_key);
235
+ return { msg: '已清空死信队列', queue: queue_name };
236
+ }
237
+ default:
238
+ return { error: { code: 'QUEUE_DLQ_UNKNOWN', message: '不支持的死信操作: ' + dlq_action } };
239
+ }
240
+ }
241
+
242
+ /**
243
+ * 延迟消息
244
+ * @param {object} client Redis 客户端
245
+ * @param {string} queue_name 队列名称
246
+ * @param {*} message 消息内容
247
+ * @param {number} delay_seconds 延迟秒数
248
+ * @returns {object} 延迟消息结果
249
+ */
250
+ async function _delay(client, queue_name, message, delay_seconds) {
251
+ var delay_sec = delay_seconds || 0;
252
+ if (delay_sec <= 0) {
253
+ return { error: { code: 'QUEUE_DELAY_INVALID', message: '延迟时间必须大于0秒' } };
254
+ }
255
+ var exec_at = Date.now() + delay_sec * 1000;
256
+ await client.zAdd(queue_name + ':delay', { score: exec_at, value: JSON.stringify(message) });
257
+ return { msg: '延迟消息已添加', exec_at: new Date(exec_at).toISOString(), delay_seconds: delay_sec, queue: queue_name };
258
+ }
259
+
260
+ /**
261
+ * 将到期延迟消息移入主队列
262
+ * @param {object} client Redis 客户端
263
+ * @param {string} queue_name 队列名称
264
+ * @returns {object} 迁移结果
265
+ */
266
+ async function _delayPoll(client, queue_name) {
267
+ var now = Date.now();
268
+ var due_msgs = await client.zRangeByScore(queue_name + ':delay', 0, now);
269
+ if (!due_msgs || due_msgs.length === 0) {
270
+ return { msg: '无到期延迟消息', count: 0, queue: queue_name };
271
+ }
272
+ for (var i = 0; i < due_msgs.length; i++) {
273
+ await client.lPush(queue_name, due_msgs[i]);
274
+ }
275
+ await client.zRemRangeByScore(queue_name + ':delay', 0, now);
276
+ return { msg: '已迁移到期消息', count: due_msgs.length, queue: queue_name };
277
+ }
278
+
279
+ /**
280
+ * 查看队列状态
281
+ * @param {object} client Redis 客户端
282
+ * @param {string} queue_name 队列名称
283
+ * @returns {object} 队列状态
284
+ */
285
+ async function _status(client, queue_name) {
286
+ var main_len = await client.lLen(queue_name);
287
+ var proc_len = await client.lLen(queue_name + ':processing');
288
+ var dlq_len = await client.lLen(queue_name + ':dlq');
289
+ var delay_len = await client.zCard(queue_name + ':delay');
290
+ return {
291
+ obj: {
292
+ queue: queue_name,
293
+ pending: main_len,
294
+ processing: proc_len,
295
+ dead_letter: dlq_len,
296
+ delayed: delay_len
297
+ }
298
+ };
299
+ }
300
+
301
+ /**
302
+ * 主函数
303
+ * @param {object} ctx 上下文
304
+ * @param {object} params 参数
305
+ * @returns {object} 执行结果
306
+ */
307
+ async function main(params, ctx, options) {
308
+ var opts = (this && typeof this.getOptions === 'function') ? this.getOptions() : {};
309
+ var driver = opts.driver || 'redis';
310
+
311
+ if (driver !== 'redis') {
312
+ ctx.output = {
313
+ error: {
314
+ code: 'MGR_UNSUPPORTED_DRIVER',
315
+ message: '不支持的队列驱动: ' + driver + ',当前支持: redis'
316
+ }
317
+ };
318
+ return ctx.output;
319
+ }
320
+
321
+ var holder;
322
+ try {
323
+ holder = await _getClient(opts);
324
+ } catch (e) {
325
+ ctx.output = {
326
+ error: {
327
+ code: 'MGR_CONNECT_FAIL',
328
+ message: 'Redis 连接失败: ' + e.message
329
+ }
330
+ };
331
+ return ctx.output;
332
+ }
333
+
334
+ if (!holder || !holder.client) {
335
+ ctx.output = {
336
+ error: {
337
+ code: 'MGR_MISSING_DEP',
338
+ message: '消息队列需要 redis 库,请手动执行: npm install redis'
339
+ }
340
+ };
341
+ return ctx.output;
342
+ }
343
+ var client = holder.client;
344
+
345
+ var action = params.action || '';
346
+ var queue_name = params.queue_name || '';
347
+
348
+ if (!queue_name) {
349
+ ctx.output = { error: { code: 'QUEUE_NO_NAME', message: '队列名称不能为空' } };
350
+ await holder.release();
351
+ return ctx.output;
352
+ }
353
+
354
+ try {
355
+ switch (action) {
356
+ case 'publish':
357
+ ctx.output = await _publish(client, queue_name, params.message);
358
+ break;
359
+ case 'consume':
360
+ ctx.output = await _consume(client, queue_name);
361
+ break;
362
+ case 'batch_consume':
363
+ ctx.output = await _batchConsume(client, queue_name, params.count || 10);
364
+ break;
365
+ case 'ack':
366
+ ctx.output = await _ack(client, queue_name, params._raw);
367
+ break;
368
+ case 'retry':
369
+ ctx.output = await _retry(client, queue_name, params._raw, params.max_retries);
370
+ break;
371
+ case 'dlq':
372
+ ctx.output = await _dlq(client, queue_name, params.dlq_action);
373
+ break;
374
+ case 'delay':
375
+ ctx.output = await _delay(client, queue_name, params.message, params.delay_seconds);
376
+ break;
377
+ case 'delay_poll':
378
+ ctx.output = await _delayPoll(client, queue_name);
379
+ break;
380
+ case 'status':
381
+ ctx.output = await _status(client, queue_name);
382
+ break;
383
+ default:
384
+ ctx.output = {
385
+ error: { code: 'MGR_UNKNOWN_ACTION', message: '不支持的操作: ' + action }
386
+ };
387
+ }
388
+ } finally {
389
+ await holder.release();
390
+ }
391
+
392
+ return ctx.output;
393
+ }
394
+
395
+ module.exports = { main };
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Redis管理器
3
+ * 管理 Redis 连接和缓存操作(基于 mm_redis)
4
+ */
5
+ async function main(params, ctx, options) {
6
+ const { redisAdmin } = require('mm_redis');
7
+ var action = params.action || '';
8
+ var key = params.key || '';
9
+ var value = params.value;
10
+ var ttl = params.ttl || 0;
11
+ var field = params.field;
12
+ var body = params.body || {};
13
+
14
+ var opts = this.getOptions();
15
+
16
+ let config = { ...opts };
17
+
18
+ var redis = redisAdmin('os', config);
19
+ await redis.open();
20
+
21
+ const SIMPLE = [
22
+ 'get', 'set', 'del', 'has', 'exists', 'add', 'getset',
23
+ 'expire', 'pexpire', 'persist', 'ttl',
24
+ 'incr', 'decr', 'incrby', 'decrby', 'incrbyfloat',
25
+ 'keys', 'clear', 'clearExpire', 'dbsize', 'flushdb', 'isConnected',
26
+ 'mget', 'mset',
27
+ 'strlen', 'append', 'setrange',
28
+ 'hgetall', 'hkeys', 'hvals', 'hlen', 'hdel', 'hmset', 'hmget',
29
+ 'setForList', 'addForList', 'getForList', 'clearForList', 'hasForList',
30
+ 'sadd', 'srem', 'sismember', 'smembers', 'scard', 'sinter', 'sunion', 'sdiff',
31
+ 'zcard', 'zrem', 'zcount', 'zrevrange',
32
+ 'publish', 'unsubscribe'
33
+ ];
34
+
35
+ const FIELD_METHODS = [
36
+ 'hget', 'hset', 'hexists',
37
+ 'zadd', 'zscore', 'zrank', 'zrevrank', 'zincrby'
38
+ ];
39
+
40
+ if (SIMPLE.indexOf(action) !== -1) {
41
+ ctx.output = await redis[action](key, value, ttl);
42
+ } else if (FIELD_METHODS.indexOf(action) !== -1) {
43
+ ctx.output = await redis[action](key, field, value, ttl);
44
+ } else if (action === 'hsetObj') {
45
+ ctx.output = await redis.hmset(key, body);
46
+ } else if (action === 'msetObj') {
47
+ ctx.output = await redis.mset(body, ttl);
48
+ } else {
49
+ ctx.output = {
50
+ error: { code: 'MGR_UNKNOWN_ACTION', message: '不支持的操作: ' + action }
51
+ };
52
+ }
53
+
54
+ return ctx.output;
55
+ }
56
+
57
+ module.exports = { main };
@@ -0,0 +1,210 @@
1
+ /**
2
+ * Redis 分布式锁管理器
3
+ * 基于 Redis SET NX + TTL,使用 WATCH/MULTI/EXEC 实现原子释放和续期
4
+ * 防止锁过期后被误删:每次获取锁生成唯一 uuid,释放时验证 uuid 匹配
5
+ * 依赖: redis (v4+,与 mm_redis 共用同一个底层库)
6
+ */
7
+
8
+ /**
9
+ * 生成唯一锁标识
10
+ * @returns {string} 唯一锁值
11
+ */
12
+ function _genLockValue() {
13
+ return Date.now().toString(36) + '-' + Math.random().toString(36).substr(2, 9);
14
+ }
15
+
16
+ /**
17
+ * 获取 Redis 客户端(复用 mm_redis 连接池,不可用时自建独立连接)
18
+ * @param {object} opts 配置选项
19
+ * @returns {object} { client, release } 或 null
20
+ */
21
+ async function _getClient(opts) {
22
+ var redis_admin = null;
23
+ try {
24
+ var { redisAdmin } = require('mm_redis');
25
+ if (redisAdmin) {
26
+ redis_admin = redisAdmin('os', opts);
27
+ }
28
+ } catch (e) {
29
+ // mm_redis 不可用时使用传入的 opts
30
+ }
31
+
32
+ // 优先复用 mm_redis 连接池,保证配置一致且不新建独立连接
33
+ if (redis_admin && typeof redis_admin._getConnection === 'function') {
34
+ var conn = await redis_admin._getConnection();
35
+ return {
36
+ client: conn,
37
+ release: function () {
38
+ try {
39
+ redis_admin._releaseConnection(conn);
40
+ } catch (e) {
41
+ void e;
42
+ }
43
+ }
44
+ };
45
+ }
46
+
47
+ // 回退: mm_redis 不可用,自建独立连接
48
+ var redis_mod = require('redis');
49
+ if (!redis_mod || !redis_mod.createClient) {
50
+ return null;
51
+ }
52
+ var createClient = redis_mod.createClient;
53
+ var client = createClient({
54
+ socket: {
55
+ host: opts.host || 'localhost',
56
+ port: opts.port || 6379,
57
+ connectTimeout: 5000
58
+ },
59
+ password: opts.password || '',
60
+ database: opts.db || 0
61
+ });
62
+ await client.connect();
63
+ return {
64
+ client: client,
65
+ release: function () {
66
+ try {
67
+ client.quit();
68
+ } catch (e) {
69
+ void e;
70
+ }
71
+ }
72
+ };
73
+ }
74
+
75
+ /**
76
+ * 获取分布式锁
77
+ * @param {object} client Redis 客户端
78
+ * @param {string} lock_key 锁键名
79
+ * @param {number} ttl 锁过期时间(秒)
80
+ * @returns {object} 获取结果
81
+ */
82
+ async function _acquire(client, lock_key, ttl) {
83
+ var lock_value = _genLockValue();
84
+ var ttl_sec = ttl || 30;
85
+ var result = await client.set(lock_key, lock_value, { EX: ttl_sec, NX: true });
86
+ if (result === 'OK') {
87
+ return { acquired: true, lock_key: lock_key, lock_value: lock_value, ttl: ttl_sec };
88
+ }
89
+ return { acquired: false, lock_key: lock_key, msg: '锁已被占用' };
90
+ }
91
+
92
+ /**
93
+ * 释放分布式锁(WATCH/MULTI/EXEC 原子操作)
94
+ * 原理: WATCH 监控键 → 验证持有者 → MULTI 事务删除 → EXEC 提交
95
+ * 如果 EXEC 返回 null 说明 WATCH 期间键被修改(锁已被他人持有或过期后重新获取),放弃删除
96
+ * @param {object} client Redis 客户端
97
+ * @param {string} lock_key 锁键名
98
+ * @param {string} lock_value 锁值(需与获取时一致)
99
+ * @returns {object} 释放结果
100
+ */
101
+ async function _release(client, lock_key, lock_value) {
102
+ if (!lock_value) {
103
+ return { released: false, msg: '缺少 lock_value,无法安全释放锁' };
104
+ }
105
+ await client.watch(lock_key);
106
+ var current = await client.get(lock_key);
107
+ if (current !== lock_value) {
108
+ await client.unwatch();
109
+ return { released: false, lock_key: lock_key, msg: '锁已被他人持有或已过期' };
110
+ }
111
+ var exec_result = await client.multi().del(lock_key).exec();
112
+ if (exec_result === null) {
113
+ return { released: false, lock_key: lock_key, msg: '锁在释放过程中被修改,放弃删除' };
114
+ }
115
+ return { released: true, lock_key: lock_key };
116
+ }
117
+
118
+ /**
119
+ * 锁续期(WATCH/MULTI/EXEC 原子操作)
120
+ * 原理: 同释放,将 DEL 替换为 EXPIRE
121
+ * @param {object} client Redis 客户端
122
+ * @param {string} lock_key 锁键名
123
+ * @param {string} lock_value 锁值(需与获取时一致)
124
+ * @param {number} ttl 续期时间(秒)
125
+ * @returns {object} 续期结果
126
+ */
127
+ async function _renew(client, lock_key, lock_value, ttl) {
128
+ if (!lock_value) {
129
+ return { renewed: false, msg: '缺少 lock_value,无法续期' };
130
+ }
131
+ var ttl_sec = ttl || 30;
132
+ await client.watch(lock_key);
133
+ var current = await client.get(lock_key);
134
+ if (current !== lock_value) {
135
+ await client.unwatch();
136
+ return { renewed: false, lock_key: lock_key, msg: '锁已被他人持有或已过期' };
137
+ }
138
+ var exec_result = await client.multi().expire(lock_key, ttl_sec).exec();
139
+ if (exec_result === null) {
140
+ return { renewed: false, lock_key: lock_key, msg: '锁在续期过程中被修改' };
141
+ }
142
+ return { renewed: true, lock_key: lock_key, ttl: ttl_sec };
143
+ }
144
+
145
+ /**
146
+ * 主函数
147
+ * @param {object} ctx 上下文
148
+ * @param {object} params 参数
149
+ * @returns {object} 执行结果
150
+ */
151
+ async function main(params, ctx, options) {
152
+ var opts = (this && typeof this.getOptions === 'function') ? this.getOptions() : {};
153
+
154
+ var holder;
155
+ try {
156
+ holder = await _getClient(opts);
157
+ } catch (e) {
158
+ ctx.output = {
159
+ error: {
160
+ code: 'LOCK_CONNECT_FAIL',
161
+ message: 'Redis 连接失败: ' + e.message
162
+ }
163
+ };
164
+ return ctx.output;
165
+ }
166
+
167
+ if (!holder || !holder.client) {
168
+ ctx.output = {
169
+ error: {
170
+ code: 'LOCK_MISSING_DEP',
171
+ message: '分布式锁需要 redis 库,请手动执行: npm install redis'
172
+ }
173
+ };
174
+ return ctx.output;
175
+ }
176
+ var client = holder.client;
177
+
178
+ var action = params.action || '';
179
+ var lock_key = params.lock_key || '';
180
+
181
+ if (!lock_key) {
182
+ ctx.output = { error: { code: 'LOCK_NO_KEY', message: '锁键名 lock_key 不能为空' } };
183
+ await holder.release();
184
+ return ctx.output;
185
+ }
186
+
187
+ try {
188
+ switch (action) {
189
+ case 'acquire':
190
+ ctx.output = await _acquire(client, lock_key, params.ttl);
191
+ break;
192
+ case 'release':
193
+ ctx.output = await _release(client, lock_key, params.lock_value);
194
+ break;
195
+ case 'renew':
196
+ ctx.output = await _renew(client, lock_key, params.lock_value, params.ttl);
197
+ break;
198
+ default:
199
+ ctx.output = {
200
+ error: { code: 'LOCK_UNKNOWN_ACTION', message: '不支持的操作: ' + action + ',支持: acquire/release/renew' }
201
+ };
202
+ }
203
+ } finally {
204
+ await holder.release();
205
+ }
206
+
207
+ return ctx.output;
208
+ }
209
+
210
+ module.exports = { main };
package/package.json CHANGED
@@ -1,50 +1,51 @@
1
1
  {
2
- "name": "mm_redis",
3
- "version": "2.1.8",
4
- "description": "高效的Redis客户端封装库,提供连接池管理、数据类型操作、错误处理和批量操作优化",
5
- "main": "index.js",
6
- "scripts": {
7
- "test": "node test.js",
8
- "dev": "nodemon test.js"
9
- },
10
- "repository": {
11
- "type": "git",
12
- "url": "git+https://gitee.com/qiuwenwu91/mm_redis.git"
13
- },
14
- "keywords": [
15
- "mm_mod",
16
- "mm_adapter",
17
- "mm_redis",
18
- "redis",
19
- "cache",
20
- "client",
21
- "connection-pool",
22
- "pipeline",
23
- "hash",
24
- "set",
25
- "sorted-set",
26
- "list",
27
- "redis-client",
28
- "nodejs"
29
- ],
30
- "layer": "adapter",
31
- "group": "redis",
32
- "author": "qww",
33
- "license": "ISC",
34
- "bugs": {
35
- "url": "https://gitee.com/qiuwenwu91/mm_redis/issues"
36
- },
37
- "homepage": "https://www.npmjs.com/package/mm_redis",
38
- "dependencies": {
39
- "mm_corejs": "^1.1.4",
40
- "mm_mod": "^1.0.1",
41
- "mm_helper": "^1.0.0",
42
- "redis": "^6.2.1"
43
- },
44
- "engines": {
45
- "node": ">=14.0.0"
46
- },
47
- "devDependencies": {
48
- "mm_eslint": "^1.7.7"
49
- }
2
+ "name": "mm_redis",
3
+ "version": "2.3.0",
4
+ "description": "高效的Redis客户端封装库,提供连接池管理、数据类型操作、错误处理和批量操作优化",
5
+ "main": "index.js",
6
+ "scripts": {
7
+ "test": "node test.js",
8
+ "dev": "nodemon test.js"
9
+ },
10
+ "repository": {
11
+ "type": "git",
12
+ "url": "git+https://gitee.com/qiuwenwu91/mm_redis.git"
13
+ },
14
+ "keywords": [
15
+ "mm_mod",
16
+ "mm_adapter",
17
+ "mm_redis",
18
+ "redis",
19
+ "cache",
20
+ "client",
21
+ "connection-pool",
22
+ "pipeline",
23
+ "hash",
24
+ "set",
25
+ "sorted-set",
26
+ "list",
27
+ "redis-client",
28
+ "nodejs"
29
+ ],
30
+ "layer": "infrastructure",
31
+ "group": "cache",
32
+ "author": "qww",
33
+ "license": "ISC",
34
+ "bugs": {
35
+ "url": "https://gitee.com/qiuwenwu91/mm_redis/issues"
36
+ },
37
+ "homepage": "https://www.npmjs.com/package/mm_redis",
38
+ "dependencies": {
39
+ "mm_admin": "^1.1.0",
40
+ "mm_eslint": "^1.0.0",
41
+ "mm_helper": "^1.0.0",
42
+ "mm_mod": "^1.0.1",
43
+ "redis": "^6.2.1"
44
+ },
45
+ "engines": {
46
+ "node": ">=14.0.0"
47
+ },
48
+ "devDependencies": {
49
+ "mm_eslint": "^1.7.7"
50
+ }
50
51
  }
@@ -0,0 +1,7 @@
1
+ 'use strict';
2
+ /**
3
+ * push_queue —— 工具契约适配层
4
+ * 业务实现归位于模块 lib/tools/push_queue/,本文件只把模块能力暴露给加载器。
5
+ * 其它模块可直接 require('mm_redis').tools.push_queue 复用。
6
+ */
7
+ module.exports = require('../../lib/tools/push_queue');
@@ -0,0 +1,56 @@
1
+ {
2
+ "name": "push_queue",
3
+ "handler": "sender",
4
+ "title": "队列发信器",
5
+ "description": "将消息写入 Redis 消息队列(lPush)。适用场景:需要异步解耦、削峰填谷或与消费端对接时。工作流提示:queue_name 必填队列名称,data 填消息数据(会 JSON 序列化),返回 sent 与 message 确认入队结果。",
6
+ "type": "gateway",
7
+ "params": {
8
+ "type": "object",
9
+ "properties": {
10
+ "queue_name": {
11
+ "type": "string",
12
+ "description": "队列名称"
13
+ },
14
+ "data": {
15
+ "description": "消息数据"
16
+ }
17
+ },
18
+ "required": [
19
+ "queue_name",
20
+ "data"
21
+ ]
22
+ },
23
+ "returns": {
24
+ "description": "返回队列发信结果,正常时包含队列名称与消息数据,异常时包含错误码与错误信息",
25
+ "type": "object",
26
+ "properties": {
27
+ "sent": {
28
+ "type": "boolean",
29
+ "description": "是否入队成功"
30
+ },
31
+ "tip": {
32
+ "type": "string",
33
+ "description": "提示信息"
34
+ },
35
+ "message": {
36
+ "description": "消息数据"
37
+ },
38
+ "error": {
39
+ "type": "object",
40
+ "description": "错误信息,仅调用出错时返回",
41
+ "properties": {
42
+ "code": {
43
+ "type": "string",
44
+ "description": "错误码"
45
+ },
46
+ "message": {
47
+ "type": "string",
48
+ "description": "错误描述"
49
+ }
50
+ }
51
+ }
52
+ }
53
+ },
54
+ "options": {},
55
+ "group": "transport"
56
+ }
@@ -0,0 +1,7 @@
1
+ 'use strict';
2
+ /**
3
+ * queue —— 工具契约适配层
4
+ * 业务实现归位于模块 lib/tools/queue/,本文件只把模块能力暴露给加载器。
5
+ * 其它模块可直接 require('mm_redis').tools.queue 复用。
6
+ */
7
+ module.exports = require('../../lib/tools/queue');
@@ -0,0 +1,129 @@
1
+ {
2
+ "name": "queue",
3
+ "handler": "manager",
4
+ "title": "消息队列管理器",
5
+ "description": "消息队列发布、消费、确认、重试、死信。适用场景:异步解耦、可靠传递。工作流提示:action 与 queue_name 必填,publish/delay 配 message,消费后 _raw 做 ack/retry,死信 dlq 配 dlq_action,结果在 obj/list、ID 在 message_id。",
6
+ "type": "data",
7
+ "params": {
8
+ "type": "object",
9
+ "properties": {
10
+ "action": {
11
+ "type": "string",
12
+ "description": "操作类型: publish/consume/batch_consume/ack/retry/dlq/delay/delay_poll/status",
13
+ "enum": [
14
+ "publish",
15
+ "consume",
16
+ "batch_consume",
17
+ "ack",
18
+ "retry",
19
+ "dlq",
20
+ "delay",
21
+ "delay_poll",
22
+ "status"
23
+ ]
24
+ },
25
+ "queue_name": {
26
+ "type": "string",
27
+ "description": "队列名称"
28
+ },
29
+ "message": {
30
+ "description": "消息内容(publish/delay时必填)"
31
+ },
32
+ "count": {
33
+ "type": "number",
34
+ "description": "批量消费数量(batch_consume时使用,默认10)"
35
+ },
36
+ "_raw": {
37
+ "type": "string",
38
+ "description": "消费时返回的原始消息字符串(ack/retry时必填)"
39
+ },
40
+ "max_retries": {
41
+ "type": "number",
42
+ "description": "最大重试次数(retry时使用,默认3)"
43
+ },
44
+ "dlq_action": {
45
+ "type": "string",
46
+ "description": "死信操作: view/replay/clear (dlq时使用)",
47
+ "enum": [
48
+ "view",
49
+ "replay",
50
+ "clear"
51
+ ]
52
+ },
53
+ "delay_seconds": {
54
+ "type": "number",
55
+ "description": "延迟秒数(delay时使用,必须>0)"
56
+ }
57
+ },
58
+ "required": [
59
+ "action",
60
+ "queue_name"
61
+ ]
62
+ },
63
+ "returns": {
64
+ "description": "管理操作结果,读操作返回 obj/list,写操作返回 msg/message_id/queue,异常时返回 error",
65
+ "type": "object",
66
+ "properties": {
67
+ "obj": {
68
+ "description": "消费的消息内容或状态"
69
+ },
70
+ "list": {
71
+ "type": "array",
72
+ "description": "批量消费的消息列表"
73
+ },
74
+ "msg": {
75
+ "type": "string",
76
+ "description": "操作结果描述"
77
+ },
78
+ "message_id": {
79
+ "type": "string",
80
+ "description": "消息ID"
81
+ },
82
+ "queue": {
83
+ "type": "string",
84
+ "description": "队列名称"
85
+ },
86
+ "count": {
87
+ "type": "number",
88
+ "description": "操作影响的消息数量"
89
+ },
90
+ "retry_count": {
91
+ "type": "number",
92
+ "description": "当前重试次数"
93
+ },
94
+ "delay_seconds": {
95
+ "type": "number",
96
+ "description": "延迟秒数"
97
+ },
98
+ "exec_at": {
99
+ "type": "string",
100
+ "description": "延迟消息执行时间"
101
+ }
102
+ }
103
+ },
104
+ "options": {
105
+ "driver": {
106
+ "type": "string",
107
+ "description": "队列驱动: redis",
108
+ "default": "redis"
109
+ },
110
+ "host": {
111
+ "type": "string",
112
+ "description": "队列服务主机",
113
+ "default": "localhost"
114
+ },
115
+ "port": {
116
+ "type": "number",
117
+ "description": "队列服务端口"
118
+ },
119
+ "password": {
120
+ "type": "string",
121
+ "description": "队列服务密码"
122
+ },
123
+ "db": {
124
+ "type": "number",
125
+ "description": "Redis 数据库编号"
126
+ }
127
+ },
128
+ "group": "system"
129
+ }
@@ -0,0 +1,7 @@
1
+ 'use strict';
2
+ /**
3
+ * redis —— 工具契约适配层
4
+ * 业务实现归位于模块 lib/tools/redis/,本文件只把模块能力暴露给加载器。
5
+ * 其它模块可直接 require('mm_redis').tools.redis 复用。
6
+ */
7
+ module.exports = require('../../lib/tools/redis');
@@ -0,0 +1,133 @@
1
+ {
2
+ "name": "redis",
3
+ "handler": "manager",
4
+ "title": "Redis管理器",
5
+ "description": "管理 Redis 缓存,覆盖键值、哈希、列表、集合、有序集合、发布订阅。适用场景:缓存数据、计数器、分布式协作或消息订阅时。工作流提示:action 与 key 必填,字符串配 value/ttl,哈希配 field/body,集合/有序集合/pub/sub 有专属 action,返回随操作而定。",
6
+ "type": "data",
7
+ "params": {
8
+ "type": "object",
9
+ "properties": {
10
+ "action": {
11
+ "type": "string",
12
+ "description": "操作类型",
13
+ "enum": [
14
+ "get",
15
+ "set",
16
+ "del",
17
+ "has",
18
+ "exists",
19
+ "add",
20
+ "getset",
21
+ "expire",
22
+ "pexpire",
23
+ "persist",
24
+ "ttl",
25
+ "incr",
26
+ "decr",
27
+ "incrby",
28
+ "decrby",
29
+ "incrbyfloat",
30
+ "keys",
31
+ "clear",
32
+ "clearExpire",
33
+ "dbsize",
34
+ "flushdb",
35
+ "isConnected",
36
+ "mget",
37
+ "mset",
38
+ "msetObj",
39
+ "strlen",
40
+ "append",
41
+ "setrange",
42
+ "hget",
43
+ "hset",
44
+ "hgetall",
45
+ "hkeys",
46
+ "hvals",
47
+ "hlen",
48
+ "hdel",
49
+ "hmset",
50
+ "hmget",
51
+ "hexists",
52
+ "hsetObj",
53
+ "setForList",
54
+ "addForList",
55
+ "getForList",
56
+ "clearForList",
57
+ "hasForList",
58
+ "sadd",
59
+ "srem",
60
+ "sismember",
61
+ "smembers",
62
+ "scard",
63
+ "sinter",
64
+ "sunion",
65
+ "sdiff",
66
+ "zadd",
67
+ "zrem",
68
+ "zscore",
69
+ "zrank",
70
+ "zrevrank",
71
+ "zincrby",
72
+ "zcard",
73
+ "zcount",
74
+ "zrevrange",
75
+ "publish",
76
+ "unsubscribe"
77
+ ]
78
+ },
79
+ "key": {
80
+ "type": "string",
81
+ "description": "缓存键"
82
+ },
83
+ "value": {
84
+ "description": "缓存值/增量值/成员等"
85
+ },
86
+ "ttl": {
87
+ "type": "number",
88
+ "description": "过期时间(秒)",
89
+ "min": 0,
90
+ "max": 31536000
91
+ },
92
+ "field": {
93
+ "type": "string",
94
+ "description": "哈希字段名/有序集合成员名(哈希和有序集合操作)"
95
+ },
96
+ "body": {
97
+ "type": "object",
98
+ "description": "批量数据对象(hsetObj/msetObj 操作)"
99
+ }
100
+ },
101
+ "required": [
102
+ "action",
103
+ "key"
104
+ ]
105
+ },
106
+ "returns": {
107
+ "description": "管理操作结果,返回值类型根据操作而定",
108
+ "type": "object"
109
+ },
110
+ "options": {
111
+ "host": {
112
+ "type": "string",
113
+ "description": "Redis主机地址",
114
+ "default": "127.0.0.1"
115
+ },
116
+ "port": {
117
+ "type": "number",
118
+ "description": "Redis端口",
119
+ "default": 6379
120
+ },
121
+ "password": {
122
+ "type": "string",
123
+ "description": "Redis密码",
124
+ "default": "asd159357"
125
+ },
126
+ "db": {
127
+ "type": "number",
128
+ "description": "数据库编号",
129
+ "default": 0
130
+ }
131
+ },
132
+ "group": "storage"
133
+ }
@@ -0,0 +1,7 @@
1
+ 'use strict';
2
+ /**
3
+ * redis_lock —— 工具契约适配层
4
+ * 业务实现归位于模块 lib/tools/redis_lock/,本文件只把模块能力暴露给加载器。
5
+ * 其它模块可直接 require('mm_redis').tools.redis_lock 复用。
6
+ */
7
+ module.exports = require('../../lib/tools/redis_lock');
@@ -0,0 +1,91 @@
1
+ {
2
+ "name": "redis_lock",
3
+ "handler": "manager",
4
+ "title": "Redis分布式锁",
5
+ "description": "基于 Redis 的分布式锁,支持获取、释放、续期,防过期误删。适用场景:多实例并发互斥访问共享资源时。工作流提示:action 选 acquire/release/renew,lock_key 必填,release/renew 回传 acquire 的 lock_value,ttl 设秒数,获取在 acquired。",
6
+ "type": "data",
7
+ "params": {
8
+ "type": "object",
9
+ "properties": {
10
+ "action": {
11
+ "type": "string",
12
+ "description": "操作类型: acquire/release/renew",
13
+ "enum": [
14
+ "acquire",
15
+ "release",
16
+ "renew"
17
+ ]
18
+ },
19
+ "lock_key": {
20
+ "type": "string",
21
+ "description": "锁键名,建议格式: lock:业务:资源ID"
22
+ },
23
+ "lock_value": {
24
+ "type": "string",
25
+ "description": "锁值(release/renew时必填,需与acquire返回的lock_value一致)"
26
+ },
27
+ "ttl": {
28
+ "type": "number",
29
+ "description": "锁过期时间(秒),默认30秒"
30
+ }
31
+ },
32
+ "required": [
33
+ "action",
34
+ "lock_key"
35
+ ]
36
+ },
37
+ "returns": {
38
+ "description": "锁操作结果,acquire返回lock_value用于后续释放/续期",
39
+ "type": "object",
40
+ "properties": {
41
+ "acquired": {
42
+ "type": "boolean",
43
+ "description": "是否获取锁成功(acquire操作)"
44
+ },
45
+ "released": {
46
+ "type": "boolean",
47
+ "description": "是否释放锁成功(release操作)"
48
+ },
49
+ "renewed": {
50
+ "type": "boolean",
51
+ "description": "是否续期成功(renew操作)"
52
+ },
53
+ "lock_key": {
54
+ "type": "string",
55
+ "description": "锁键名"
56
+ },
57
+ "lock_value": {
58
+ "type": "string",
59
+ "description": "锁唯一标识(acquire时返回,用于后续释放/续期)"
60
+ },
61
+ "ttl": {
62
+ "type": "number",
63
+ "description": "锁过期时间(秒)"
64
+ },
65
+ "msg": {
66
+ "type": "string",
67
+ "description": "操作结果描述"
68
+ }
69
+ }
70
+ },
71
+ "options": {
72
+ "host": {
73
+ "type": "string",
74
+ "description": "Redis 主机",
75
+ "default": "localhost"
76
+ },
77
+ "port": {
78
+ "type": "number",
79
+ "description": "Redis 端口"
80
+ },
81
+ "password": {
82
+ "type": "string",
83
+ "description": "Redis 密码"
84
+ },
85
+ "db": {
86
+ "type": "number",
87
+ "description": "Redis 数据库编号"
88
+ }
89
+ },
90
+ "group": "system"
91
+ }