UNPKG

mm_redis

Version:

高效的Redis客户端封装库,提供连接池管理、数据类型操作、错误处理和批量操作优化

2,166 lines (1,993 loc) 111 kB
/** * @fileOverview redis缓存帮助类函数 * @author <a href="http://qww.elins.cn">邱文武</a> * @version 1.5.0 */ const { BaseService } = require('mm_base_service'); const { createClient } = require('redis'); // Redis缓存帮助类函数 /* 创建Redis帮助类函数 */ /// 构造函数 class Redis extends BaseService { /** * @description 创建Redis帮助类函数 (构造函数) * @param {Object} config 配置参数 * @constructor */ constructor(config) { super(); // 配置参数 this.config = { // 服务器地址 "host": "localhost", // 端口号 "port": 6379, // 密码 "password": "asd159357", // 数据库 "database": 0, // 前缀 "prefix": "", // 连接超时时间(ms) "connect_timeout": 10000, // 重试次数 "retry_count": 3, // 重试间隔(ms) "retry_delay": 1000, // 连接池配置 "pool": { "max_clients": 100, // 最大连接数 "min_idle": 5, // 最小空闲连接数 "max_idle": 20, // 最大空闲连接数 "idle_timeout": 30000 // 空闲超时时间(ms) } }; this.setConfig(config); // 初始化连接池 this._initPool(); // 唯一标识符 this.identifier = this.config.host + "/" + this.config.database; // 数据库连接器 this.conn = null; // 连接状态 this.connected = false; // 重试次数计数器 this.retry_count = 0; // 订阅回调函数 this.func_list = {}; } } /** * 应用键前缀 * @param {String} key 键 * @return {String} 处理后的键 */ Redis.prototype.applyPrefix = function(key) { return this.config.prefix ? this.config.prefix + key : key; }; /** * 移除键前缀 * @param {String} key 带前缀的键 * @return {String} 处理后的键 */ Redis.prototype.removePrefix = function(key) { if (this.config.prefix && key.startsWith(this.config.prefix)) { return key.slice(this.config.prefix.length); } return key; }; /** * 消息订阅 * @param {String} channel 订阅的频道 * @param {Function} func 操作回调函数,可判断成功与否 * @return {Promise<Boolean>} 订阅结果 */ Redis.prototype.subscribe = async function(channel, func) { try { await this.conn.subscribe(this.applyPrefix(channel)); this.func_list[channel] = func; return true; } catch (err) { $.log.error(`[Redis] 订阅频道 ${channel} 失败:`, err); return false; } }; /** * 取消订阅 * @param {String} channel 订阅的频道 * @return {Promise<Boolean>} 取消订阅结果 */ Redis.prototype.unsubscribe = async function(channel) { try { await this.conn.unsubscribe(this.applyPrefix(channel)); delete this.func_list[channel]; return true; } catch (err) { $.log.error(`[Redis] 取消订阅频道 ${channel} 失败:`, err); return false; } }; /** * 发布消息 * @param {String} channel 频道 * @param {String} message 订阅的消息 * @return {Promise<Number>} 接收消息的客户端数量 */ Redis.prototype.publish = async function(channel, message) { try { return await this.conn.publish(this.applyPrefix(channel), message); } catch (err) { $.log.error(`[Redis] 发布消息到频道 ${channel} 失败:`, err); $.log.error('[Redis] 操作失败:', err); return false; } }; /** * 订阅消息处理, 如果没有订阅函数,则其他的订阅消息会进入该函数 * @param {String} channel * @param {String} message */ Redis.prototype.main = async function(channel, message) { // 默认实现,可由子类重写 $.log.debug(`[Redis] 收到未处理的消息 [${channel}]:`, message); }; /** * 消息通知 * @param {String} channel 频道 * @param {String} message 消息 */ Redis.prototype.message = function(channel, message) { // 移除前缀 channel = this.removePrefix(channel); var func = this.func_list[channel]; if (func) { try { func(message); } catch (err) { $.log.error(`[Redis] 处理频道 ${channel} 的消息时出错:`, err); } } else { this.main(channel, message); } }; /** * @description 设置配置 * @param {Object} config 配置对象或配置路径 */ Redis.prototype.setConfig = function(config) { let obj = {}; if (typeof config === 'string') { obj = config.loadJson(__dirname); } else if (config) { // 合并配置参数 if (config.pool) { // 确保连接池配置的合并不会覆盖所有默认值 if (!this.config.pool) { this.config.pool = {}; } Object.assign(this.config.pool, config.pool); // 删除config中的pool属性,避免后续Object.assign重复处理 const configCopy = { ...config }; delete configCopy.pool; Object.assign(this.config, configCopy); } else { Object.assign(this.config, config); } } else { return; } // 确保连接池配置存在且包含所有必要字段 if (!this.config.pool) { this.config.pool = {}; } // 设置默认连接池配置,只覆盖未定义的字段 const defaultPoolConfig = { max_clients: 100, min_idle: 5, max_idle: 20, idle_timeout: 30000 }; for (const key in defaultPoolConfig) { if (this.config.pool[key] === undefined) { this.config.pool[key] = defaultPoolConfig[key]; } } // 如果连接池已经初始化,更新其配置 if (this.connection_pool) { this.connection_pool.config = this.config.pool; } // 初始化连接池 this._initPool(); $.log.debug('[Redis] 配置已更新'); this.identifier = this.config.host + "/" + this.config.database; return this; }; // 添加连接池初始化方法 Redis.prototype._initPool = function() { // 确保连接池配置存在 if (!this.config.pool) { this.config.pool = { max_clients: 100, min_idle: 5, max_idle: 20, idle_timeout: 30000 }; } // 初始化连接池相关属性 this.connection_pool = { available: [], in_use: new Set(), config: this.config.pool // 直接引用配置对象,确保同步更新 }; // 记录连接池初始化 // $.log.debug('[Redis] 连接池已初始化,配置:', this.config.pool); }; /** * @description 连接Redis数据库 * @return {Promise<Boolean>} 连接结果 */ Redis.prototype.open = async function() { if (this.conn && this.connected) { return true; } try { const cg = this.config; this.retry_count = 0; // 创建客户端 this.conn = createClient({ socket: { host: cg.host, port: cg.port, timeout: cg.connect_timeout }, password: cg.password, database: cg.database }); // 监听错误事件 this.conn.on('error', (err) => { $.log.error('[Redis] Redis连接错误:', err); this.connected = false; // 尝试重连 this._reconnect(); }); // 监听连接成功事件 this.conn.on('connect', () => { // $.log.debug('[Redis] Redis已连接'); this.connected = true; this.retry_count = 0; }); // 监听断开连接事件 this.conn.on('end', () => { // $.log.debug('[Redis] Redis连接已断开'); this.connected = false; }); // 监听订阅成功事件 this.conn.on('subscribe', (channel, count) => { $.log.debug(`[Redis] 频道${this.removePrefix(channel)}订阅成功, 当前总计订阅数:${count}`); }); // 监听取消订阅事件 this.conn.on('unsubscribe', (channel, count) => { $.log.debug(`[Redis] 频道${this.removePrefix(channel)}取消订阅, 当前总计订阅数:${count}`); }); // 收到消息后执行回调 this.conn.on('message', (channel, message) => { this.message(channel, message); }); // 连接Redis await this.conn.connect(); this.connected = true; return true; } catch (err) { $.log.error('[Redis] Redis连接失败:', err); // 尝试重连 this._reconnect(); return false; } }; /** * 内部重连方法 * @private */ Redis.prototype._reconnect = function() { if (this.retry_count >= this.config.retry_count) { $.log.error(`[Redis] 达到最大重连次数 ${this.config.retry_count},停止重连`); return; } this.retry_count++; $.log.debug(`[Redis] 尝试第 ${this.retry_count} 次重连...`); setTimeout(async () => { try { await this.open(); } catch (err) { $.log.error(`[Redis] 重连失败:`, err); } }, this.config.retry_delay); }; /** * @description 关闭连接 * @return {Promise<Boolean>} 关闭结果 */ // 修复close方法,正确处理连接池 Redis.prototype.close = function() { try { // $.log.debug('[Redis] 关闭所有连接...'); // 关闭所有使用中的连接 if (this.connection_pool && this.connection_pool.in_use) { for (const connection of this.connection_pool.in_use) { try { connection.close(); } catch (e) { $.log.error('[Redis] 关闭连接失败:', e); } } this.connection_pool.in_use.clear(); } // 关闭所有可用连接 if (this.connection_pool && this.connection_pool.available) { for (const connection of this.connection_pool.available) { try { // 检查连接对象是否有close方法 if (typeof connection.close === 'function') { connection.close(); } else if (typeof connection.end === 'function') { // 某些Redis客户端可能使用end方法 connection.end(); } else { $.log.debug('[Redis] 连接对象没有可用的关闭方法'); } } catch (e) { $.log.error('[Redis] 关闭连接失败:', e); } } this.connection_pool.available = []; } this.connected = false; $.log.debug('[Redis] Redis连接已断开'); return Promise.resolve(true); } catch (error) { $.log.error('[Redis] 关闭连接时发生错误:', error); return Promise.resolve(false); } }; // 添加Pipeline支持 Redis.prototype.pipeline = function() { const redisInstance = this; const commands = []; return { // 设置值 set: function(key, value, seconds) { commands.push(['set', key, value, seconds]); return this; }, // 获取值 get: function(key) { commands.push(['get', key]); return this; }, // 删除 del: function(key) { commands.push(['del', key]); return this; }, // 判断存在 has: function(key) { commands.push(['has', key]); return this; }, // 设置哈希字段 hset: function(key, field, value) { commands.push(['hset', key, field, value]); return this; }, // 过期时间设置 expire: function(key, seconds) { commands.push(['expire', key, seconds]); return this; }, // 执行所有命令 exec: async function() { try { // 使用Redis实例的方法获取连接 const connection = await redisInstance._getConnection(); const results = []; // 依次执行所有命令 for (const cmd of commands) { try { let result; const [command, ...args] = cmd; // 根据命令类型直接调用对应的方法 switch (command) { case 'set': { // 键前缀处理和值序列化 const [key, value, seconds] = args; const prefixedKey = redisInstance.applyPrefix(key); let processedValue = value; if (typeof value === 'object' && value !== null) { processedValue = JSON.stringify(value); } result = await connection.set(prefixedKey, processedValue); // 设置过期时间 if (seconds && seconds > 0) { await connection.expire(prefixedKey, seconds); } break; } case 'get': { const [key] = args; const prefixedKey = redisInstance.applyPrefix(key); result = await connection.get(prefixedKey); // 尝试解析JSON if (typeof result === 'string') { try { if (result.startsWith('{') && result.endsWith('}') || result.startsWith('[') && result.endsWith(']')) { result = JSON.parse(result); } } catch (e) { // 解析失败,保持原样 } } break; } case 'del': { const [key] = args; const prefixedKey = redisInstance.applyPrefix(key); result = await connection.del(prefixedKey); break; } case 'has': { const [key] = args; const prefixedKey = redisInstance.applyPrefix(key); result = await connection.exists(prefixedKey) > 0; break; } case 'hset': { const [key, field, value] = args; const prefixedKey = redisInstance.applyPrefix(key); result = await connection.hSet(prefixedKey, field, value); break; } case 'expire': { const [key, seconds] = args; const prefixedKey = redisInstance.applyPrefix(key); result = await connection.expire(prefixedKey, seconds); break; } default: // 对于其他命令,直接返回null result = null; break; } // 直接返回结果值,符合测试期望的格式 results.push(result); } catch (err) { $.log.error(`[Redis] Pipeline命令执行失败 [${cmd[0]}]:`, err); results.push(null); } } // 释放连接回连接池 redisInstance._recycleConnection(connection); return results; } catch (error) { $.log.error('[Redis] Pipeline执行失败:', error); return []; } } }; }; /** * @description 检查连接状态 * @return {Boolean} 连接状态 */ Redis.prototype.isConnected = function() { return this.connected && this.conn && this.conn.isReady; }; /** * @description 查询或是设置缓存过期时间 * @param {String} key 键 * @param {Number} seconds 秒, 为空则查询有效时长 * @return {Promise<Number|Boolean>} 查询时长或设置结果, 时长为-1表示永不过期 */ /** * 获取或设置键的过期时间 * @param {String} key 键名 * @param {Number} [seconds] 过期时间(秒),不传此参数则获取剩余过期时间 * @return {Promise<Number|Boolean>} 不传seconds时返回剩余过期时间(-1表示无过期设置,-2表示已过期),传seconds时返回是否设置成功 */ Redis.prototype.ttl = async function(key, seconds) { try { const connection = await this._getConnection(); try { key = this.applyPrefix(key); // 如果传入了seconds参数,则设置过期时间 if (seconds !== undefined) { if (typeof seconds !== 'number' || isNaN(seconds) || seconds < 0) { return false; } // 设置过期时间 if (typeof connection.expire === 'function') { await connection.expire(key, seconds); return true; } else if (typeof connection.execute === 'function') { await connection.execute('EXPIRE', [key, seconds]); return true; } else { $.log.error('[Redis] 连接对象不支持expire操作'); return false; } } // 否则获取剩余过期时间 if (typeof connection.ttl === 'function') { return await connection.ttl(key); } else if (typeof connection.execute === 'function') { return await connection.execute('TTL', [key]); } else { $.log.error('[Redis] 连接对象不支持ttl操作'); return -2; // 默认返回键不存在 } } finally { this._recycleConnection(connection); } } catch (err) { $.log.error(`[Redis] ${seconds !== undefined ? '设置' : '获取'}过期时间失败 [${key}]:`, err); return seconds !== undefined ? false : -2; } }; /** * @description 增加整数值(负数为减)(兼容方法) * @param {String} key 键 * @param {Number} num 数值 * @param {Number} [seconds=0] 过期时间(秒) * @return {Promise<Number>} 计算后的结果 */ Redis.prototype.addInt = async function(key, num, seconds = 0) { try { // 参数验证 if (!key || typeof key !== 'string') { $.log.error('[Redis] 参数错误: 键必须是非空字符串'); return false; } if (typeof num !== 'number' || isNaN(num) || !Number.isInteger(num)) { $.log.error('[Redis] 参数错误: 数值必须是有效整数'); return false; } // 触发前置事件,允许修改或阻止操作 const beforeEventData = { key, value: num, seconds, cancel: false }; this.emit('addInt_before', beforeEventData); // 如果事件处理器标记为取消,则返回false if (beforeEventData.cancel) { $.log.debug(`[Redis] addInt操作被事件处理器取消 [${key}]`); return false; } // 使用可能被修改的值 let { key: finalKey, value: finalNum, seconds: finalSeconds } = beforeEventData; const connection = await this._getConnection(); try { const prefixedKey = this.applyPrefix(finalKey); let result; if (finalNum > 0) { // 增加整数值 if (typeof connection.incrBy === 'function') { result = await connection.incrBy(prefixedKey, finalNum); } else if (typeof connection.execute === 'function') { result = await connection.execute('INCRBY', [prefixedKey, finalNum]); } else { // 如果出错,回退到先获取再设置的方式 $.log.warn(`[Redis] 使用原子操作增加整数值失败,使用回退方案`); let value = await this.get(prefixedKey); let n = 0; if (value) { n = Number(value); } n += finalNum; await this.set(prefixedKey, n, finalSeconds); result = n; } } else { // 减少整数值 if (typeof connection.decrBy === 'function') { result = await connection.decrBy(prefixedKey, -finalNum); } else if (typeof connection.execute === 'function') { result = await connection.execute('DECRBY', [prefixedKey, -finalNum]); } else { // 如果出错,回退到先获取再设置的方式 $.log.warn(`[Redis] 使用原子操作减少整数值失败,使用回退方案`); let value = await this.get(prefixedKey); let n = 0; if (value) { n = Number(value); } n += finalNum; await this.set(prefixedKey, n, finalSeconds); result = n; } } // 如果设置了过期时间 if (finalSeconds > 0) { if (typeof connection.expire === 'function') { await connection.expire(prefixedKey, finalSeconds); } else if (typeof connection.execute === 'function') { await connection.execute('EXPIRE', [prefixedKey, finalSeconds]); } } // 触发后置事件 this.emit('addInt_after', { key: finalKey, value: finalNum, seconds: finalSeconds, result, success: true }); return result; } finally { this._recycleConnection(connection); } } catch (err) { // 触发错误事件 this.emit('addInt_error', { key, value: num, seconds, error: err }); $.log.error(`[Redis] 增加整数值失败 [${key}]:`, err); // 与CacheBase保持一致,错误时返回默认值而不是抛出异常 return 0; } }; /** * @description 增加浮点数值(负数为减)(兼容方法) * @param {String} key 键 * @param {Number} num 数值 * @param {Number} seconds 过期时间,0表示永不过期 * @return {Promise<Number>} 计算后的结果 */ Redis.prototype.addFloat = async function(key, num, seconds = 0) { try { // 参数验证 if (!key || typeof key !== 'string') { $.log.warn(`[Redis] addFloat: key参数必须是非空字符串`); return 0; } // 验证num参数 num = parseFloat(num); if (isNaN(num)) { $.log.warn(`[Redis] addFloat: num参数必须是有效的数值`); return 0; } // 触发前置事件,允许修改或阻止操作 const beforeEventData = { key, value: num, seconds, cancel: false }; this.emit('addFloat_before', beforeEventData); // 如果事件处理器标记为取消,则返回默认值 if (beforeEventData.cancel) { $.log.debug(`[Redis] addFloat操作被事件处理器取消 [${key}]`); return 0; } // 使用可能被修改的值 let { key: finalKey, value: finalNum, seconds: finalSeconds } = beforeEventData; let result; const connection = await this._getConnection(); try { const prefixedKey = this.applyPrefix(finalKey); // 优先使用原生方法 if (typeof connection.incrByFloat === 'function') { result = parseFloat(await connection.incrByFloat(prefixedKey, finalNum)); } // 尝试使用execute通用方法 else if (typeof connection.execute === 'function') { result = parseFloat(await connection.execute('INCRBYFLOAT', [prefixedKey, finalNum])); } // 回退方案 else { $.log.warn(`[Redis] 连接对象不支持incrByFloat操作,使用回退方案`); let value = await this.get(prefixedKey); let n = 0; if (value) { n = Number(value); } n += finalNum; await this.set(prefixedKey, n, finalSeconds); result = n; } // 设置过期时间 if (finalSeconds > 0) { if (typeof connection.expire === 'function') { await connection.expire(prefixedKey, finalSeconds); } else if (typeof connection.execute === 'function') { await connection.execute('EXPIRE', [prefixedKey, finalSeconds]); } } // 触发后置事件 this.emit('addFloat_after', { key: finalKey, value: finalNum, seconds: finalSeconds, result, success: true }); return result; } catch (err) { // 如果出错,回退到先获取再设置的方式 $.log.warn(`[Redis] 使用原子操作增加浮点数值失败,使用回退方案:`, err); let value = await this.get(prefixedKey); let n = 0; if (value) { n = Number(value); } n += finalNum; await this.set(prefixedKey, n, finalSeconds); // 触发后置事件 this.emit('addFloat_after', { key: finalKey, value: finalNum, seconds: finalSeconds, result: n, success: true }); return n; } finally { this._recycleConnection(connection); } } catch (err) { // 触发错误事件 this.emit('addFloat_error', { key, value: num, seconds, error: err }); $.log.error(`[Redis] 增加浮点数值失败 [${key}]:`, err); // 与CacheBase保持一致,错误时返回默认值而不是抛出异常 return 0; } }; /** * @description 增加字符串值到指定缓存(兼容方法) * @param {String} key 键 * @param {String} str 添加的字符串 * @param {Number} seconds 过期时间,0表示永不过期 * @return {Promise<Number>} 添加后的字符串长度 */ Redis.prototype.addStr = async function(key, str, seconds = 0) { try { // 参数验证 if (!key || typeof key !== 'string') { $.log.warn(`[Redis] addStr: key参数必须是非空字符串`); return 0; } // 验证str参数 if (str === undefined || str === null) { $.log.warn(`[Redis] addStr: str参数不能为undefined或null`); return 0; } str = String(str); // 触发前置事件,允许修改或阻止操作 const beforeEventData = { key, value: str, seconds, cancel: false }; this.emit('addStr_before', beforeEventData); // 如果事件处理器标记为取消,则返回0 if (beforeEventData.cancel) { $.log.debug(`[Redis] addStr操作被事件处理器取消 [${key}]`); return 0; } // 使用可能被修改的值 let { key: finalKey, value: finalStr, seconds: finalSeconds } = beforeEventData; let result; const connection = await this._getConnection(); try { const prefixedKey = this.applyPrefix(finalKey); // 优先使用原生方法 if (typeof connection.append === 'function') { result = await connection.append(prefixedKey, finalStr); } // 尝试使用execute通用方法 else if (typeof connection.execute === 'function') { result = await connection.execute('APPEND', [prefixedKey, finalStr]); } // 回退方案 else { $.log.warn(`[Redis] 连接对象不支持append操作,使用回退方案`); let value = await this.get(prefixedKey) || ''; value += finalStr; await this.set(prefixedKey, value, finalSeconds); result = value.length; } // 设置过期时间 if (finalSeconds > 0) { if (typeof connection.expire === 'function') { await connection.expire(prefixedKey, finalSeconds); } else if (typeof connection.execute === 'function') { await connection.execute('EXPIRE', [prefixedKey, finalSeconds]); } } // 触发后置事件 this.emit('addStr_after', { key: finalKey, value: finalStr, seconds: finalSeconds, result, success: true }); return result; } catch (err) { $.log.error('[Redis] 追加字符串失败:', err); // 回退方案:先获取再设置 let value = await this.get(prefixedKey) || ''; value += finalStr; await this.set(prefixedKey, value, finalSeconds); // 触发后置事件 this.emit('addStr_after', { key: finalKey, value: finalStr, seconds: finalSeconds, result: value.length, success: true }); return value.length; } finally { this._recycleConnection(connection); } } catch (err) { // 触发错误事件 this.emit('addStr_error', { key, value: str, seconds, error: err }); $.log.error(`[Redis] 追加字符串失败 [${key}]:`, err); return 0; } }; /** * @description 删除缓存(兼容方法) * @param {String|Array<String>} key 键或键数组 * @return {Promise<Boolean>} 成功返回true,失败返回false */ Redis.prototype.del = async function(key) { try { // 键名必须是非空字符串或非空字符串数组 if (!key) { $.log.warn('[Redis] del: key必须是非空字符串或非空字符串数组'); return false; } // 如果是单个键,验证是否为非空字符串 if (typeof key === 'string') { if (!key) { $.log.warn('[Redis] del: key必须是非空字符串'); return false; } } // 如果是数组,验证每个元素是否为非空字符串 else if (Array.isArray(key)) { for (const k of key) { if (!k || typeof k !== 'string') { $.log.warn('[Redis] del: key必须是非空字符串或非空字符串数组'); return false; } } } else { $.log.warn('[Redis] del: key必须是非空字符串或非空字符串数组'); return false; } // 触发前置事件,允许修改或阻止操作 const beforeEventData = { key, cancel: false }; this.emit('del_before', beforeEventData); // 如果事件处理器标记为取消,则返回false if (beforeEventData.cancel) { $.log.debug(`[Redis] del操作被事件处理器取消 [${key}]`); return false; } // 使用可能被修改的值 let { key: finalKey } = beforeEventData; const connection = await this._getConnection(); try { let success; if (Array.isArray(finalKey)) { // 批量删除 - 分别删除每个键以确保正确处理 success = true; const deleted = []; for (const k of finalKey) { const prefixedKey = this.applyPrefix(k); const result = await connection.del(prefixedKey); if (result !== 1) { success = false; } else { deleted.push(k); } } // 触发后置事件 this.emit('del_after', { key: finalKey, success, deleted }); } else { // 单个删除 const prefixedKey = this.applyPrefix(finalKey); const n = await connection.del(prefixedKey); success = n === 1; // 触发后置事件 this.emit('del_after', { key: finalKey, success, deleted: success ? [finalKey] : [] }); } return success; } finally { this._recycleConnection(connection); } } catch (err) { // 触发错误事件 this.emit('del_error', { key, error: err }); $.log.error(`[Redis] 删除缓存失败 [${key}]:`, err); return false; } }; /** * @description 修改缓存 * @param {String} key 键 * @param {Object} value 值 * @param {Number} seconds 秒 * @return {Promise<Boolean>} 操作结果 */ /** * @description 设置缓存(兼容方法) * @param {String} key 键 * @param {*} value 值 * @param {Number} seconds 过期时间,0表示永不过期 * @return {Promise<Boolean>} 成功返回true,失败返回false */ Redis.prototype.set = async function(key, value, seconds = 0) { try { // 键名必须是非空字符串 if (!key || typeof key !== 'string') { $.log.error('[Redis] 参数错误: key必须是非空字符串'); return false; } // 触发前置事件,允许修改或阻止操作 const beforeEventData = { key, value, seconds, cancel: false }; this.emit('set_before', beforeEventData); // 如果事件处理器标记为取消,则返回false if (beforeEventData.cancel) { $.log.debug(`[Redis] set操作被事件处理器取消 [${key}]`); return false; } // 使用可能被修改的值 let { key: finalKey, value: finalValue, seconds: finalSeconds } = beforeEventData; const connection = await this._getConnection(); try { const prefixedKey = this.applyPrefix(finalKey); if (typeof(finalValue) === "object") { finalValue = JSON.stringify(finalValue); } if (finalSeconds > 0) { await connection.set(prefixedKey, finalValue, { EX: finalSeconds }); } else { await connection.set(prefixedKey, finalValue); } // 触发后置事件 this.emit('set_after', { key, value, seconds, success: true }); return true; } finally { this._recycleConnection(connection); } } catch (err) { $.log.error(`[Redis] 设置缓存失败 [${key}]:`, err); // 触发错误事件 this.emit('set_error', { key, value, seconds, error: err }); return false; } }; /** * @description 仅当键不存在时设置缓存 * @param {String} key 键 * @param {Object} value 值 * @param {Number} seconds 过期时间(秒) * @return {Promise<Boolean>} 操作结果,键不存在时设置成功返回true,键已存在返回false */ Redis.prototype.add = async function(key, value, seconds) { try { // 触发前置事件,允许修改或阻止操作 const beforeEventData = { key, value, ttl: seconds, cancel: false }; this.emit('add_before', beforeEventData); // 如果事件处理器标记为取消,则返回false if (beforeEventData.cancel) { $.log.debug(`[Redis] add操作被事件处理器取消 [${key}]`); return false; } // 使用可能被修改的值 let { key: finalKey, value: finalValue, ttl: finalSeconds } = beforeEventData; const connection = await this._getConnection(); try { finalKey = this.applyPrefix(finalKey); if (typeof(finalValue) === "object") { finalValue = JSON.stringify(finalValue); } // 使用NX选项实现SETNX操作 - 仅在键不存在时设置 const options = finalSeconds ? { NX: true, EX: finalSeconds } : { NX: true }; const result = await connection.set(finalKey, finalValue, options); const success = result === "OK"; // 触发后置事件 this.emit('add_after', { key: finalKey, value: finalValue, ttl: finalSeconds, success }); return success; } finally { this._recycleConnection(connection); } } catch (err) { $.log.error(`[Redis] 添加缓存失败 [${key}]:`, err); // 触发错误事件 this.emit('add_error', { key, value, ttl: seconds, error: err }); return false; } }; /** * @description 查询缓存 * @param {String} key 键 * @return {Promise<Object>} 查询值 */ /** * @description 获取缓存(兼容方法) * @param {String} key 键 * @return {Promise<*>} 获取到的值,不存在时返回null */ Redis.prototype.get = async function(key) { try { // 参数验证 if (!key || typeof key !== 'string') { $.log.warn(`[Redis] get: key参数必须是非空字符串`); return null; } // 触发前置事件,允许修改或阻止操作 const beforeEventData = { key, cancel: false }; this.emit('get_before', beforeEventData); // 如果事件处理器标记为取消,则返回null if (beforeEventData.cancel) { $.log.debug(`[Redis] get操作被事件处理器取消 [${key}]`); return null; } // 使用可能被修改的值 const finalKey = beforeEventData.key; const connection = await this._getConnection(); try { const prefixedKey = this.applyPrefix(finalKey); const value = await connection.get(prefixedKey); let result = value; if (typeof(value) === "string") { // 尝试解析JSON try { if (value.startsWith("{") && value.endsWith("}") || value.startsWith("[") && value.endsWith("]")) { result = JSON.parse(value); } } catch (e) { // 解析失败,返回原始字符串 } } // 触发后置事件 this.emit('get_after', { key: finalKey, value: result, success: true }); return result; } finally { this._recycleConnection(connection); } } catch (err) { // 触发错误事件 this.emit('get_error', { key, error: err }); $.log.error(`[Redis] 获取缓存失败 [${key}]:`, err); return null; } }; /** * @description 判断键是否存在(兼容方法) * @param {String} key 键 * @return {Promise<Boolean>} 有返回true, 没有返回false */ Redis.prototype.has = async function(key) { try { // 参数验证 if (!key || typeof key !== 'string') { $.log.warn(`[Redis] has: key参数必须是非空字符串`); return false; } // 触发前置事件,允许修改或阻止操作 const beforeEventData = { key, cancel: false }; this.emit('has_before', beforeEventData); // 如果事件处理器标记为取消,则返回false if (beforeEventData.cancel) { $.log.debug(`[Redis] has操作被事件处理器取消 [${key}]`); return false; } // 使用可能被修改的值 const finalKey = beforeEventData.key; const connection = await this._getConnection(); try { const prefixedKey = this.applyPrefix(finalKey); const result = await connection.exists(prefixedKey); const exists = result > 0; // 触发后置事件 this.emit('has_after', { key: finalKey, result: exists, success: true }); return exists; } finally { this._recycleConnection(connection); } } catch (err) { // 触发错误事件 this.emit('has_error', { key, error: err }); $.log.error(`[Redis] 检查键是否存在失败 [${key}]:`, err); return false; } }; /** * @description 检查键是否存在,返回存在的键数量(兼容方法) * @param {String|Array<String>} key 键或键数组 * @return {Promise<Number>} 存在的键数量 */ Redis.prototype.exists = async function(key) { try { // 参数验证 if (!key) { $.log.warn('[Redis] exists: key必须是非空字符串或非空字符串数组'); return 0; } // 如果是单个键,验证是否为非空字符串 if (typeof key === 'string') { if (!key) { $.log.warn('[Redis] exists: key必须是非空字符串'); return 0; } } // 如果是数组,验证每个元素是否为非空字符串 else if (Array.isArray(key)) { for (const k of key) { if (!k || typeof k !== 'string') { $.log.warn('[Redis] exists: key必须是非空字符串或非空字符串数组'); return 0; } } } else { $.log.warn('[Redis] exists: key必须是非空字符串或非空字符串数组'); return 0; } // 触发前置事件,允许修改或阻止操作 const beforeEventData = { key, cancel: false }; this.emit('exists_before', beforeEventData); // 如果事件处理器标记为取消,则返回0 if (beforeEventData.cancel) { $.log.debug(`[Redis] exists操作被事件处理器取消 [${key}]`); return 0; } // 使用可能被修改的值 const finalKey = beforeEventData.key; const connection = await this._getConnection(); try { let result; if (typeof finalKey === 'string') { const prefixedKey = this.applyPrefix(finalKey); result = await connection.exists(prefixedKey); } else if (Array.isArray(finalKey)) { const prefixedKeys = finalKey.map(k => this.applyPrefix(k)); result = await connection.exists(...prefixedKeys); } // 触发后置事件 this.emit('exists_after', { key: finalKey, result, success: true }); return result; } finally { this._recycleConnection(connection); } } catch (err) { // 触发错误事件 this.emit('exists_error', { key, error: err }); $.log.error(`[Redis] 检查键存在性失败 [${key}]:`, err); return 0; } }; /** * @description 查询缓存的字符串中的一段字符串 * @param {String} key 键 * @param {Number} start 开始位置 * @param {Number} end 结束位置 * @return {Promise<String>} 查询值 */ Redis.prototype.getrange = async function(key, start, end) { try { const connection = await this._getConnection(); try { key = this.applyPrefix(key); return await connection.getRange(key, start, end); } catch (err) { $.log.error('[Redis] 获取字符串范围失败:', err); // 回退方案 const value = await this.get(key); if (value && typeof value === 'string') { return value.substring(start, end + 1); } return ''; } finally { this._recycleConnection(connection); } } catch (err) { $.log.error(`[Redis] 获取字符串范围失败 [${key}]:`, err); return ''; } }; /** * 在指定位置插入文本 * @param {String} str 原字符串 * @param {Number} index 索引 * @param {String} str_sub 插入的字符串 * @return {String} 插入后的字符串 */ Redis.prototype.insertString = function(str, index, str_sub) { return str.substring(0, index) + str_sub + str.substring(index); }; /** * @description 在值的指定位置开始增加一段字符串 * @param {String} key 键 * @param {Number} index 开始位置 * @param {String} value 变更的值 * @return {Promise<Number>} 字符串长度 */ Redis.prototype.setrange = async function(key, index, value) { try { // 触发前置事件,允许修改或阻止操作 const beforeEventData = { key, index, value, cancel: false }; this.emit('setrange_before', beforeEventData); // 如果事件处理器标记为取消,则返回0 if (beforeEventData.cancel) { $.log.debug(`[Redis] setrange操作被事件处理器取消 [${key}]`); return 0; } // 使用可能被修改的值 let { key: finalKey, index: finalIndex, value: finalValue } = beforeEventData; let result; const connection = await this._getConnection(); try { finalKey = this.applyPrefix(finalKey); // 优先使用原生方法 if (typeof connection.setRange === 'function') { result = await connection.setRange(finalKey, finalIndex, finalValue); } // 尝试使用execute通用方法 else if (typeof connection.execute === 'function') { result = await connection.execute('SETRANGE', [finalKey, finalIndex, finalValue]); } // 回退方案 else { $.log.warn(`[Redis] 连接对象不支持setRange操作,使用回退方案`); let str = await this.get(finalKey) || ''; if (str.length < finalIndex) { // 填充空格到指定索引 str = str.padEnd(finalIndex, ' '); } str = this.insertString(str, finalIndex, finalValue); await this.set(finalKey, str); result = str.length; } // 触发后置事件 this.emit('setrange_after', { key: finalKey, index: finalIndex, value: finalValue, result, success: true }); return result; } catch (err) { $.log.error('[Redis] 设置字符串范围失败:', err); // 回退方案 let str = await this.get(finalKey) || ''; if (str.length < finalIndex) { // 填充空格到指定索引 str = str.padEnd(finalIndex, ' '); } str = this.insertString(str, finalIndex, finalValue); await this.set(finalKey, str); // 触发后置事件 this.emit('setrange_after', { key: finalKey, index: finalIndex, value: finalValue, result: str.length, success: true }); return str.length; } finally { this._recycleConnection(connection); } } catch (err) { // 触发错误事件 this.emit('setrange_error', { key, index, value, error: err }); $.log.error(`[Redis] 设置字符串范围失败 [${key}]:`, err); return 0; } }; /** * @description 清空缓存 * @param {String} key 键, 为空则清空所有 * @return {Promise<Boolean>} 执行结果 */ Redis.prototype.clear = async function(key) { try { // 触发前置事件,允许修改或阻止操作 const beforeEventData = { key, cancel: false }; this.emit('clear_before', beforeEventData); // 如果事件处理器标记为取消,则返回false if (beforeEventData.cancel) { $.log.debug(`[Redis] clear操作被事件处理器取消`); return false; } // 使用可能被修改的值 let { key: finalKey } = beforeEventData; let success = false; const connection = await this._getConnection(); try { if (finalKey) { // 清空指定键 success = await this.del(finalKey); } else { // 清空当前数据库 if (typeof connection.flushDb === 'function') { await connection.flushDb(); success = true; } else if (typeof connection.execute === 'function') { await connection.execute('FLUSHDB'); success = true; } else { $.log.error('[Redis] 连接对象不支持flushDb操作'); success = false; } } // 触发后置事件 this.emit('clear_after', { key: finalKey, success }); return success; } finally { this._recycleConnection(connection); } } catch (err) { // 触发错误事件 this.emit('clear_error', { key, error: err }); $.log.error('[Redis] 清空缓存失败:', err); return false; } }; /** * @description 排序 * @param {String} key 键 * @param {Object} options 排序选项 * @return {Promise<Array>} 排序后的数组 */ Redis.prototype.sort = async function(key, options = {}) { try { const connection = await this._getConnection(); try { key = this.applyPrefix(key); // 默认升序 let sortOptions = []; if (options.by) { sortOptions.push('BY', options.by); } if (options.limit) { sortOptions.push('LIMIT', options.limit.offset || 0, options.limit.count || -1); } if (options.order === 'desc') { sortOptions.push('DESC'); } else { sortOptions.push('ASC'); } if (options.alpha) { sortOptions.push('ALPHA'); } if (options.get) { if (Array.isArray(options.get)) { options.get.forEach(getKey => { sortOptions.push('GET', getKey); }); } else { sortOptions.push('GET', options.get); } } return await connection.sort(key, sortOptions); } catch (err) { $.log.error('[Redis] 排序失败:', err); // 回退方案:获取数据后在内存中排序 const data = await this.getForList(key); return data.sort((a, b) => { if (options.alpha) { return options.order === 'desc' ? b.localeCompare(a) : a.localeCompare(b); } else { return options.order === 'desc' ? Number(b) - Number(a) : Number(a) - Number(b); } }); } finally { this._recycleConnection(connection); } } catch (err) { $.log.error(`[Redis] 排序失败 [${key}]:`, err); return []; } }; /** * @description 获取所有键名 * @param {String} pattern 键模式 支持*号 * @return {Promise<Array>} 键数组 */ Redis.prototype.keys = async function(pattern = '*') { try { const connection = await this._getConnection(); try { // 应用前缀到模式 const fullPattern = this.config.prefix ? this.config.prefix + pattern : pattern; const keys = await connection.keys(fullPattern); // 返回移除前缀的键 return keys.map(key => this.removePrefix(key)); } finally { this._recycleConnection(connection); } } catch (err) { $.log.error('[Redis] 获取键列表失败:', err); return []; } }; /** * @description 修改数组缓存 * @param {String} key 键 * @param {Object|Array} value 值 * @param {Number} seconds 秒 * @return {Promise<Number>} 数组长度 */ Redis.prototype.setForList = async function(key, value, seconds) { try { // 触发前置事件,允许修改或阻止操作 const beforeEventData = { key, value, ttl: seconds, cancel: false }; this.emit('setForList_before', beforeEventData); // 如果事件处理器标记为取消,则返回0 if (beforeEventData.cancel) { $.log.debug(`[Redis] setForList操作被事件处理器取消 [${key}]`); return 0; } // 使用可能被修改的值 let { key: finalKey, value: finalValue, ttl: finalSeconds } = beforeEventData; let len = 0; const connection = await this._getConnection(); try { finalKey = this.applyPrefix(finalKey); // 先清空再添加 if (typeof connection.del === 'function') { await connection.del(finalKey); } else if (typeof connection.execute === 'function') { await connection.execute('DEL', [finalKey]); } if (Array.isArray(finalValue)) { if (finalValue.length > 0) { // 添加数组元素 if (typeof connection.rPush === 'function') { len = await connection.rPush(finalKey, finalValue); } else if (typeof connection.execute === 'function') { len = await connection.execute('RPUSH', [finalKey, ...finalValue]); } else { $.log.error('[Redis] 连接对象不支持rPush操作'); len = 0; } } } else { // 添加单个元素 if (typeof connection.rPush === 'function') { len = await connection.rPush(finalKey, finalValue); } else if (typeof connection.execute === 'function') { len = await connection.execute('RPUSH', [finalKey, finalValue]); } else { $.log.error('[Redis] 连接对象不支持rPush操作'); len = 0; } } if (finalSeconds) { // 设置过期时间 if (typeof connection.expire === 'function') { await connection.expire(finalKey, finalSeconds); } else if (typeof connection.execute === 'function') { await connection.execute('EXPIRE', [finalKey, finalSeconds]); } else { $.log.error('[Redis] 连接对象不支持expire操作'); } } // 触发后置事件 this.emit('setForList_after', { key: finalKey, value: finalValue, ttl: finalSeconds, result: len, success: len > 0 }); return len; } finally { this._recycleConnection(connection); } } catch (err) { // 触发错误事件 this.emit('setForList_error', { key, value, error: err }); $.log.error(`[Redis] 设置列表失败 [${key}]:`, err); return 0; } }; /** * @description 数组缓存追加对象 * @param {String} key 键 * @param {Object|Array} value 值 * @param {Number} seconds 秒 * @return {Promise<Number>} 追加后的数组长度 */ Redis.prototype.addForList = async function(key, value, seconds = 0) { try { // 触发前置事件,允许修改或阻止操作 const beforeEventData = { key, value, ttl: seconds, cancel: false }; this.emit('addForList_before', beforeEventData); // 如果事件处理器标记为取消,则返回0 if (beforeEventData.cancel) { $.log.debug(`[Redis] addForList操作被事件处理器取消 [${key}]`); return 0; } // 使用可能被修改的值 let { key: finalKey, value: finalValue, ttl: finalSeconds } = beforeEventData; let len = 0; const connection = await this._getConnection(); try { finalKey = this.applyPrefix(finalKey); if (Array.isArray(finalValue)) { if (finalValue.length > 0) { // 添加数组元素 if (typeof connection.rPush === 'function') { len = await connection.rPush(finalKey, finalValue); } else if (typeof connection.execute === 'function') { len = await connection.execute('RPUSH', [finalKey, ...finalValue]); } else { $.log.error('[Redis] 连接对象不支持rPush操作'); len = 0; } } } else { // 添加单个元素 if (typeof connection.rPush === 'function') { len = await connection.rPush(finalKey, finalValue); } else if (typeof connection.execute === 'function') { len = await connection.execute('RPUSH', [finalKey, finalValue]); } else { $.log.error('[Redis] 连接对象不支持rPush操作'); len = 0; } } if (finalSeconds) { // 设置过期时间 if (typeof connection.expire === 'function') { await connection.expire(finalKey, finalSeconds); } else if (typeof connection.execute === 'function') { await connection.execute('EXPIRE', [finalKey, finalSeconds]); } else { $.log.error('[Redis] 连接对象不支持expire操作'); } } // 触发后置事件 this.emit('addForList_after', { key: finalKey, value: finalValue, ttl: finalSeconds, result: len, success: len > 0 }); return len; } finally { this._recycleConnection(connection); } } catch (err) { // 触发错误事件 this.emit('addForList_error', { key, value, error: err }); $.log.error(`[Redis] 添加列表项失败 [${key}]:`, err); return 0; } }; /** * @description 判断成员是否存在 * @param {String} key 键 * @param {Object} value 值 * @return {Promise<Boolean>} 存在返回true, 否则返回false */ Redis.prototype.hasForList = async function(key, value) { if (!value) { $.log.error('[Redis] 参数错误: 值不能为空'); return false; } try { // 尝试使用Redis的LREM命令来检查是否存在 const keyWithPrefix = this.applyPrefix(key); // 转换value为字符串以便比较 const valueStr = JSON.stringify(value); // 使用scan来查找,避免在大列表上的性能问题 const arr = await this.getForList(key); return arr.some(item => { const itemStr = JSON.stringify(item); return itemStr === valueStr || item === value; }); } catch (err) { $.log.error(`[Redis] 检查列表成员是否存在失败 [${key}]:`, err); return false; } }; /** * @description 查询数组缓存 * @param {String} key 键 * @param {Number} start 起始位置 * @param {Number} end 结束位置 * @return {Promise<Array>} 查询到的数组 */ Redis.prototype.getForList = async function(key, start = 0, end = -1) { try { // 触发前置事件,允许修改或阻止操作 const beforeEventData = { key, start, end, cancel: false }; this.emit('getForList_before', beforeEventData); // 如果事件处理器标记为取消,则返回空数组 if (beforeEventData.cancel) { $.log.debug(`[Redis] getForList操作被事件处理器取消 [${key}]`); return []; } // 使用可能被修改的值 let { key: finalKey, start: finalStart, end: finalEnd } = beforeEventData; const connection = await this._getConnection(); try { finalKey = this.applyPrefix(finalKey); let result; // 检查连接是否支持lRange方法,Redis v4 API if (typeof connection.lRange === 'function') { result = await connection.lRange(finalKey, finalStart, finalEnd); } else if (typeof connection.lrange === 'function') { // 兼容旧版API result = await connection.lrange(finalKey, finalStart, finalEnd); } else if (typeof connection.execute === 'function') { // 回退到通用execute方法 result = await connection.execute('LRANGE', [finalKey, finalStart, finalEnd]); } else { $.log.error('[Redis] 连接对象不支持lRange或lrange操作'); return []; } // 确保结果是数组 if (!Array.isArray(result)) { $.log.error('[Redis] 列表查询结果不是数组格式'); return []; } // 尝试解析每个元素为JSON const parsedResult = result.map(item => { if (typeof item === "string") { try { // 尝试识别并解析JSON字符串 const trimmedItem = item.trim(); if ((trimmedItem.startsWith("{") && trimmedItem.endsWith("}")) || (trimmedItem.startsWith("[") && trimmedItem.endsWith("]"))) { return JSON.parse(trimmedItem); } } catch (e) { // 解析失败,返回原始字符串 } } return item; }); // 触发后置事件 this.emit('getForList_after', { key: finalKey, start: finalStart, end: finalEnd, result: parsedResult }); return parsedResult; } finally { this._recycleConnection(connection); } } catch (err) { // 触发错误事件 this.emit('getForList_error', { key, start, end, error: err }); $.log.error(`[Redis] 获取列表失败 [${key}]:`, err); return []; } }; /** * @description 清空列表,保留键和过期时间 * @param {String} key 键 * @return {Promise<Boolean>} 成功返回true,失败返回false */ Redis.prototype.clearForList = async function(key) { if (!key) { $.log.error('[Redis] 参数错误: 键不能为空'); return false; } try { // 触发前置事件,允许修改或阻止操作 const beforeEventData = { key, cancel: false }; this.emit('clearForList_before', beforeEventData); // 如果事件处理器标记为取消,则返回false if (beforeEventData.cancel) { $.log.debug(`[Redis] clearForList操作被事件处理器取消 [${key}]`); return false; } // 使用可能被修改的值 let finalKey = beforeEventData.key; // 直接使用set方法设置为空数组,这样更可靠且与其他模块行为一致 // 这样可以确保返回值是一个空数组,而不是null或undefined finalKey = this.applyPrefix(finalKey); await this.set(finalKey, []); // 触发后置事件 this.emit('clearForList_after', { key: finalKey, success: true }); return true; } catch (err) { // 触发错误事件 this.emit('clearForList_error', { key, error: err }); $.log.error(`[Redis] 清空列表失败 [${key}]:`, err); return false; } }; /** * @description 批量获取缓存 * @param {Array<String>} keys 键数组 * @return {Promise<Object>} 键值对对象 */ /** * @description 批量获取缓存(兼容方法) * @param {Array<String>} keys 键数组 * @return {Promise<Array>} 返回值数组,顺序与键数组一致 */ Redis.prototype.mget = async function(keys) { try { // 参数验证 if (!keys || !Array.isArray(keys)) { $.log.warn('[Redis] mget: keys必须是非空数组'); return []; } // 验证数组中的每个元素是否为非空字符串 for (const key of keys) { if (!key || typeof key !== 'string') { $.log.warn('[Redis] mget: keys数组中的每个元素必须是非空字符串'); return []; } } // 触发前置事件,允许修改或阻止操作 const beforeEventData = { keys, cancel: false }; this.emit('mget_before', beforeEventData); // 如果事件处理器标记为取消,则返回空数组 if (beforeEventData.cancel) { $.log.debug(`[Redis] mget操作被事件处理器取消 [${keys}]`); return []; } // 使用可能被修改的值 const finalKeys = beforeEventData.keys; const connection = await this._getConnection(); try { // 直接使用Redis v4的mGet方法,应用键前缀 const keysWithPrefix = finalKeys.map(k => this.applyPrefix(k)); const values = await connection.mGet(keysWithPrefix); // 处理值的格式,保持数组返回格式 const result = []; values.forEach((value, index) => { if (typeof(value) === "string") { try { if (value.startsWith("{") && value.endsWith("}") || value.startsWith("[") && value.endsWith("]")) { value = JSON.parse(value); } } catch (e) { // 解析失败,返回原始字符串 } } result[index] = value; }); // 触发后置事件 this.emit('mget_after', { keys: finalKeys, result, success: true }); return result; } finally { this._recycleConnection(connection); } } catch (err) { // 触发错误事件 this.emit('mget_error', { keys, error: err }); $.log.error(`[Redis] 批量获取缓存失败 [${keys}]:`, err); return []; } } /** * @description 批量设置缓存(兼容方法) * @param {Ob