/** * 文件模拟Redis服务实现 * * 功能描述: * - 在本地开发环境中使用文件系统模拟Redis功能 * - 支持完整的Redis基础操作和过期机制 * - 提供数据持久化和自动过期清理功能 * - 适用于开发测试环境的Redis功能模拟 * * 职责分离: * - 数据存储:使用JSON文件持久化Redis数据 * - 过期管理:实现TTL机制和自动过期清理 * - 接口实现:完整实现IRedisService接口规范 * - 文件操作:管理数据文件的读写和目录创建 * * 最近修改: * - 2025-01-07: 代码规范优化 - 为所有公共方法添加完整的三级注释,包含业务逻辑和示例代码 * - 2025-01-07: 代码规范优化 - 修复常量命名规范,为主要方法添加完整的三级注释 * - 2025-01-07: 代码规范优化 - 完善文件头注释和方法注释,添加详细业务逻辑说明 * * @author moyin * @version 1.0.3 * @since 2025-01-07 * @lastModified 2025-01-07 */ import { Injectable, Logger, OnModuleDestroy } from '@nestjs/common'; import { promises as fs } from 'fs'; import * as path from 'path'; import { IRedisService } from './redis.interface'; /** * 文件模拟Redis服务 * * 职责: * - 在本地开发环境中使用文件系统模拟Redis功能 * - 实现完整的Redis操作接口 * - 管理数据持久化和过期清理 * * 主要方法: * - initializeStorage() - 初始化文件存储 * - loadData/saveData() - 数据文件读写 * - cleanExpiredKeys() - 过期键清理 * - set/get/del() - 基础键值操作 * * 使用场景: * - 本地开发环境的Redis功能模拟 * - 单元测试和集成测试 * - 无需真实Redis服务器的开发场景 */ @Injectable() export class FileRedisService implements IRedisService, OnModuleDestroy { private readonly logger = new Logger(FileRedisService.name); private readonly DATA_DIR = path.join(process.cwd(), 'redis-data'); private readonly DATA_FILE = path.join(this.DATA_DIR, 'redis.json'); private readonly CLEANUP_INTERVAL = 60000; // 每分钟清理一次过期键 private data: Map = new Map(); private cleanupTimer?: NodeJS.Timeout; constructor() { this.initializeStorage(); } /** * 初始化存储 * * 业务逻辑: * 1. 创建数据存储目录(如果不存在) * 2. 尝试从文件加载现有数据 * 3. 启动定时过期清理任务 * 4. 记录初始化状态日志 * * @throws Error 文件系统操作失败时 * * @example * ```typescript * // 在构造函数中自动调用 * constructor() { * this.initializeStorage(); * } * ``` */ async initializeStorage(): Promise { try { // 确保数据目录存在 await fs.mkdir(this.DATA_DIR, { recursive: true }); // 尝试加载现有数据 await this.loadData(); // 启动过期清理任务 this.startExpirationCleanup(); this.logger.log('文件Redis服务初始化完成'); } catch (error) { this.logger.error('初始化文件Redis服务失败', error); } } /** * 从文件加载数据 * * 业务逻辑: * 1. 读取JSON数据文件内容 * 2. 解析JSON数据并转换为Map结构 * 3. 检查并过滤已过期的数据项 * 4. 初始化内存数据存储 * 5. 记录加载的数据条数 * * @throws Error 文件读取或JSON解析失败时 * * @example * ```typescript * await this.loadData(); * console.log(`加载了 ${this.data.size} 条数据`); * ``` */ private async loadData(): Promise { try { const fileContent = await fs.readFile(this.DATA_FILE, 'utf-8'); const jsonData = JSON.parse(fileContent); this.data = new Map(); for (const [key, item] of Object.entries(jsonData)) { const typedItem = item as { value: string; expireAt?: number }; // 检查是否已过期 if (!typedItem.expireAt || typedItem.expireAt > Date.now()) { this.data.set(key, typedItem); } } this.logger.log(`从文件加载了 ${this.data.size} 条Redis数据`); } catch (error) { // 文件不存在或格式错误,使用空数据 this.data = new Map(); this.logger.log('初始化空的Redis数据存储'); } } /** * 保存数据到文件 * * 业务逻辑: * 1. 确保数据目录存在 * 2. 将内存中的Map数据转换为JSON对象 * 3. 格式化JSON字符串(缩进2个空格) * 4. 异步写入到数据文件 * 5. 处理文件写入异常 * * @throws Error 文件写入失败时 * * @example * ```typescript * this.data.set('key', { value: 'data' }); * await this.saveData(); * ``` */ private async saveData(): Promise { try { // 确保数据目录存在 const dataDir = path.dirname(this.DATA_FILE); await fs.mkdir(dataDir, { recursive: true }); const jsonData = Object.fromEntries(this.data); await fs.writeFile(this.DATA_FILE, JSON.stringify(jsonData, null, 2)); } catch (error) { this.logger.error('保存Redis数据到文件失败', error); } } /** * 启动过期清理任务 * * 业务逻辑: * 1. 清理现有定时器(如果存在) * 2. 设置定时器,每60秒执行一次清理 * 3. 调用cleanExpiredKeys方法清理过期数据 * 4. 确保应用运行期间持续清理过期键 * 5. 保存定时器引用以便后续清理 * * @example * ```typescript * this.startExpirationCleanup(); * // 每分钟自动清理过期键 * ``` */ private startExpirationCleanup(): void { // 清理现有定时器 if (this.cleanupTimer) { clearInterval(this.cleanupTimer); } this.cleanupTimer = setInterval(async () => { await this.cleanExpiredKeys(); }, this.CLEANUP_INTERVAL); } /** * 清理过期的键 * * 业务逻辑: * 1. 获取当前时间戳 * 2. 遍历所有数据项检查过期时间 * 3. 删除已过期的键值对 * 4. 统计清理的键数量 * 5. 如有清理则保存数据并记录日志 * * @example * ```typescript * await this.cleanExpiredKeys(); * // 清理了 3 个过期的Redis键 * ``` */ private async cleanExpiredKeys(): Promise { const now = Date.now(); let cleanedCount = 0; for (const [key, item] of this.data.entries()) { if (item.expireAt && item.expireAt <= now) { this.data.delete(key); cleanedCount++; } } if (cleanedCount > 0) { this.logger.log(`清理了 ${cleanedCount} 个过期的Redis键`); await this.saveData(); // 保存清理后的数据 } } /** * 设置键值对 * * 业务逻辑: * 1. 创建数据项对象,包含值和可选的过期时间 * 2. 如果设置了TTL,计算过期时间戳 * 3. 将数据存储到内存Map中 * 4. 异步保存数据到文件 * 5. 记录操作日志 * * @param key 键名,不能为空 * @param value 值,支持字符串类型 * @param ttl 可选的过期时间(秒),不设置则永不过期 * @returns Promise 操作完成的Promise * @throws Error 当文件操作失败时 * * @example * ```typescript * await redisService.set('user:123', 'userData', 3600); * ``` */ async set(key: string, value: string, ttl?: number): Promise { const item: { value: string; expireAt?: number } = { value }; if (ttl && ttl > 0) { item.expireAt = Date.now() + ttl * 1000; } this.data.set(key, item); await this.saveData(); this.logger.debug(`设置Redis键: ${key}, TTL: ${ttl || '永不过期'}`); } /** * 获取键对应的值 * * 业务逻辑: * 1. 从内存Map中查找键对应的数据项 * 2. 检查数据项是否存在 * 3. 验证数据项是否已过期 * 4. 如果过期则删除并保存数据 * 5. 返回有效的值或null * * @param key 键名,不能为空 * @returns Promise 键对应的值,不存在或已过期返回null * @throws Error 当文件操作失败时 * * @example * ```typescript * const value = await redisService.get('user:123'); * ``` */ async get(key: string): Promise { const item = this.data.get(key); if (!item) { return null; } // 检查是否过期 if (item.expireAt && item.expireAt <= Date.now()) { this.data.delete(key); await this.saveData(); return null; } return item.value; } /** * 删除指定的键 * * 业务逻辑: * 1. 检查键是否存在于内存Map中 * 2. 从内存Map中删除键 * 3. 如果键存在则保存数据到文件 * 4. 记录删除操作日志 * 5. 返回删除是否成功 * * @param key 键名,不能为空 * @returns Promise 删除成功返回true,键不存在返回false * @throws Error 当文件操作失败时 * * @example * ```typescript * const deleted = await redisService.del('user:123'); * console.log(deleted ? '删除成功' : '键不存在'); * ``` */ async del(key: string): Promise { const existed = this.data.has(key); this.data.delete(key); if (existed) { await this.saveData(); this.logger.debug(`删除Redis键: ${key}`); } return existed; } /** * 检查键是否存在 * * 业务逻辑: * 1. 从内存Map中查找键对应的数据项 * 2. 检查数据项是否存在 * 3. 验证数据项是否已过期 * 4. 如果过期则删除并保存数据 * 5. 返回键的存在状态 * * @param key 键名,不能为空 * @returns Promise 键存在返回true,不存在或已过期返回false * @throws Error 当文件操作失败时 * * @example * ```typescript * const exists = await redisService.exists('user:123'); * if (exists) { * console.log('用户数据存在'); * } * ``` */ async exists(key: string): Promise { const item = this.data.get(key); if (!item) { return false; } // 检查是否过期 if (item.expireAt && item.expireAt <= Date.now()) { this.data.delete(key); await this.saveData(); return false; } return true; } /** * 设置键的过期时间 * * 业务逻辑: * 1. 从内存Map中查找键对应的数据项 * 2. 检查数据项是否存在 * 3. 计算过期时间戳并设置到数据项 * 4. 保存更新后的数据到文件 * 5. 记录过期时间设置日志 * * @param key 键名,不能为空 * @param ttl 过期时间(秒),必须大于0 * @returns Promise 操作完成的Promise * @throws Error 当文件操作失败时 * * @example * ```typescript * await redisService.expire('user:123', 3600); // 1小时后过期 * ``` */ async expire(key: string, ttl: number): Promise { const item = this.data.get(key); if (item) { item.expireAt = Date.now() + ttl * 1000; await this.saveData(); this.logger.debug(`设置Redis键过期时间: ${key}, TTL: ${ttl}秒`); } } /** * 获取键的剩余过期时间 * * 业务逻辑: * 1. 从内存Map中查找键对应的数据项 * 2. 检查数据项是否存在 * 3. 检查是否设置了过期时间 * 4. 计算剩余过期时间 * 5. 如果已过期则删除键并保存数据 * * @param key 键名,不能为空 * @returns Promise 剩余时间(秒),-1表示永不过期,-2表示键不存在 * @throws Error 当文件操作失败时 * * @example * ```typescript * const ttl = await redisService.ttl('user:123'); * if (ttl > 0) { * console.log(`还有${ttl}秒过期`); * } else if (ttl === -1) { * console.log('永不过期'); * } else { * console.log('键不存在'); * } * ``` */ async ttl(key: string): Promise { const item = this.data.get(key); if (!item) { return -2; // 键不存在 } if (!item.expireAt) { return -1; // 永不过期 } const remaining = Math.ceil((item.expireAt - Date.now()) / 1000); if (remaining <= 0) { // 已过期,删除键 this.data.delete(key); await this.saveData(); return -2; } return remaining; } /** * 清空所有数据 * * 业务逻辑: * 1. 清空内存Map中的所有数据 * 2. 保存空数据到文件 * 3. 记录清空操作日志 * * @returns Promise 操作完成的Promise * @throws Error 当文件操作失败时 * * @example * ```typescript * await redisService.flushall(); * console.log('所有数据已清空'); * ``` */ async flushall(): Promise { this.data.clear(); await this.saveData(); this.logger.log('清空所有Redis数据'); } /** * 设置键值对并指定过期时间 * * 业务逻辑: * 1. 创建数据项对象,包含值和过期时间戳 * 2. 计算过期时间戳(当前时间 + TTL秒数) * 3. 将数据存储到内存Map中 * 4. 异步保存数据到文件 * 5. 记录操作日志 * * @param key 键名,不能为空 * @param ttl 过期时间(秒),必须大于0 * @param value 值,支持字符串类型 * @returns Promise 操作完成的Promise * @throws Error 当文件操作失败时 * * @example * ```typescript * await redisService.setex('session:abc', 1800, 'sessionData'); * ``` */ async setex(key: string, ttl: number, value: string): Promise { const item: { value: string; expireAt?: number } = { value, expireAt: Date.now() + ttl * 1000, }; this.data.set(key, item); await this.saveData(); this.logger.debug(`设置Redis键(setex): ${key}, TTL: ${ttl}秒`); } /** * 键值自增操作 * * 业务逻辑: * 1. 从内存Map中查找键对应的数据项 * 2. 如果键不存在则初始化为1 * 3. 如果键存在则将值转换为数字并加1 * 4. 更新数据项的值 * 5. 保存数据到文件并记录日志 * * @param key 键名,不能为空 * @returns Promise 自增后的新值 * @throws Error 当文件操作失败或值不是数字时 * * @example * ```typescript * const newValue = await redisService.incr('counter'); * console.log(`计数器新值: ${newValue}`); * ``` */ async incr(key: string): Promise { const item = this.data.get(key); let newValue: number; if (!item) { newValue = 1; this.data.set(key, { value: '1' }); } else { newValue = parseInt(item.value, 10) + 1; item.value = newValue.toString(); } await this.saveData(); this.logger.debug(`自增Redis键: ${key}, 新值: ${newValue}`); return newValue; } /** * 向集合添加成员 * * 业务逻辑: * 1. 从内存Map中查找键对应的数据项 * 2. 如果键不存在则创建新的Set集合 * 3. 如果键存在则解析JSON数据为Set集合 * 4. 向集合中添加新成员 * 5. 将更新后的集合保存到内存Map和文件 * * @param key 集合键名,不能为空 * @param member 要添加的成员,不能为空 * @returns Promise 操作完成的Promise * @throws Error 当文件操作失败时 * * @example * ```typescript * await redisService.sadd('users', 'user123'); * ``` */ async sadd(key: string, member: string): Promise { const item = this.data.get(key); let members: Set; if (!item) { members = new Set([member]); } else { members = new Set(JSON.parse(item.value)); members.add(member); } this.data.set(key, { value: JSON.stringify([...members]), expireAt: item?.expireAt }); await this.saveData(); this.logger.debug(`添加集合成员: ${key} -> ${member}`); } /** * 从集合移除成员 * * 业务逻辑: * 1. 从内存Map中查找键对应的数据项 * 2. 如果键不存在则直接返回 * 3. 解析JSON数据为Set集合 * 4. 从集合中移除指定成员 * 5. 如果集合为空则删除键,否则更新集合数据 * * @param key 集合键名,不能为空 * @param member 要移除的成员,不能为空 * @returns Promise 操作完成的Promise * @throws Error 当文件操作失败时 * * @example * ```typescript * await redisService.srem('users', 'user123'); * ``` */ async srem(key: string, member: string): Promise { const item = this.data.get(key); if (!item) { return; } const members = new Set(JSON.parse(item.value)); members.delete(member); if (members.size === 0) { this.data.delete(key); } else { item.value = JSON.stringify([...members]); } await this.saveData(); this.logger.debug(`移除集合成员: ${key} -> ${member}`); } /** * 获取集合的所有成员 * * 业务逻辑: * 1. 从内存Map中查找键对应的数据项 * 2. 如果键不存在则返回空数组 * 3. 检查数据项是否已过期 * 4. 如果过期则删除键并保存数据,返回空数组 * 5. 解析JSON数据并返回成员列表 * * @param key 集合键名,不能为空 * @returns Promise 集合成员列表,集合不存在返回空数组 * @throws Error 当文件操作失败时 * * @example * ```typescript * const members = await redisService.smembers('users'); * console.log('用户列表:', members); * ``` */ async smembers(key: string): Promise { const item = this.data.get(key); if (!item) { return []; } // 检查是否过期 if (item.expireAt && item.expireAt <= Date.now()) { this.data.delete(key); await this.saveData(); return []; } return JSON.parse(item.value); } /** * 模块销毁时的清理操作 * * 业务逻辑: * 1. 清理定时器,防止内存泄漏 * 2. 保存当前数据到文件 * 3. 记录清理操作日志 * 4. 释放相关资源 * * @returns void 无返回值 * * @example * ```typescript * // NestJS框架会在模块销毁时自动调用 * onModuleDestroy() { * // 自动清理定时器和保存数据 * } * ``` */ onModuleDestroy(): void { if (this.cleanupTimer) { clearInterval(this.cleanupTimer); this.cleanupTimer = undefined; this.logger.log('清理定时器已停止'); } // 保存最后的数据 this.saveData().catch(error => { this.logger.error('模块销毁时保存数据失败', error); }); this.logger.log('FileRedisService已清理'); } }