Files
whale-town-end/src/core/redis/file_redis.service.ts
moyin bb796a2469 refactor:项目架构重构和命名规范化
- 统一文件命名为snake_case格式(kebab-case  snake_case)
- 重构zulip模块为zulip_core,明确Core层职责
- 重构user-mgmt模块为user_mgmt,统一命名规范
- 调整模块依赖关系,优化架构分层
- 删除过时的文件和目录结构
- 更新相关文档和配置文件

本次重构涉及大量文件重命名和模块重组,
旨在建立更清晰的项目架构和统一的命名规范。
2026-01-08 00:14:14 +08:00

689 lines
19 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 文件模拟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<string, { value: string; expireAt?: number }> = new Map();
private cleanupTimer?: NodeJS.Timeout;
constructor() {
this.initializeStorage();
}
/**
* 初始化存储
*
* 业务逻辑:
* 1. 创建数据存储目录(如果不存在)
* 2. 尝试从文件加载现有数据
* 3. 启动定时过期清理任务
* 4. 记录初始化状态日志
*
* @throws Error 文件系统操作失败时
*
* @example
* ```typescript
* // 在构造函数中自动调用
* constructor() {
* this.initializeStorage();
* }
* ```
*/
async initializeStorage(): Promise<void> {
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<void> {
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<void> {
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<void> {
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<void> 操作完成的Promise
* @throws Error 当文件操作失败时
*
* @example
* ```typescript
* await redisService.set('user:123', 'userData', 3600);
* ```
*/
async set(key: string, value: string, ttl?: number): Promise<void> {
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<string | null> 键对应的值不存在或已过期返回null
* @throws Error 当文件操作失败时
*
* @example
* ```typescript
* const value = await redisService.get('user:123');
* ```
*/
async get(key: string): Promise<string | null> {
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<boolean> 删除成功返回true键不存在返回false
* @throws Error 当文件操作失败时
*
* @example
* ```typescript
* const deleted = await redisService.del('user:123');
* console.log(deleted ? '删除成功' : '键不存在');
* ```
*/
async del(key: string): Promise<boolean> {
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<boolean> 键存在返回true不存在或已过期返回false
* @throws Error 当文件操作失败时
*
* @example
* ```typescript
* const exists = await redisService.exists('user:123');
* if (exists) {
* console.log('用户数据存在');
* }
* ```
*/
async exists(key: string): Promise<boolean> {
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<void> 操作完成的Promise
* @throws Error 当文件操作失败时
*
* @example
* ```typescript
* await redisService.expire('user:123', 3600); // 1小时后过期
* ```
*/
async expire(key: string, ttl: number): Promise<void> {
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<number> 剩余时间(秒),-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<number> {
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<void> 操作完成的Promise
* @throws Error 当文件操作失败时
*
* @example
* ```typescript
* await redisService.flushall();
* console.log('所有数据已清空');
* ```
*/
async flushall(): Promise<void> {
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<void> 操作完成的Promise
* @throws Error 当文件操作失败时
*
* @example
* ```typescript
* await redisService.setex('session:abc', 1800, 'sessionData');
* ```
*/
async setex(key: string, ttl: number, value: string): Promise<void> {
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<number> 自增后的新值
* @throws Error 当文件操作失败或值不是数字时
*
* @example
* ```typescript
* const newValue = await redisService.incr('counter');
* console.log(`计数器新值: ${newValue}`);
* ```
*/
async incr(key: string): Promise<number> {
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<void> 操作完成的Promise
* @throws Error 当文件操作失败时
*
* @example
* ```typescript
* await redisService.sadd('users', 'user123');
* ```
*/
async sadd(key: string, member: string): Promise<void> {
const item = this.data.get(key);
let members: Set<string>;
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<void> 操作完成的Promise
* @throws Error 当文件操作失败时
*
* @example
* ```typescript
* await redisService.srem('users', 'user123');
* ```
*/
async srem(key: string, member: string): Promise<void> {
const item = this.data.get(key);
if (!item) {
return;
}
const members = new Set<string>(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<string[]> 集合成员列表,集合不存在返回空数组
* @throws Error 当文件操作失败时
*
* @example
* ```typescript
* const members = await redisService.smembers('users');
* console.log('用户列表:', members);
* ```
*/
async smembers(key: string): Promise<string[]> {
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已清理');
}
}