refactor:项目架构重构和命名规范化
- 统一文件命名为snake_case格式(kebab-case snake_case) - 重构zulip模块为zulip_core,明确Core层职责 - 重构user-mgmt模块为user_mgmt,统一命名规范 - 调整模块依赖关系,优化架构分层 - 删除过时的文件和目录结构 - 更新相关文档和配置文件 本次重构涉及大量文件重命名和模块重组, 旨在建立更清晰的项目架构和统一的命名规范。
This commit is contained in:
@@ -1,200 +1,138 @@
|
||||
# Redis 适配器
|
||||
# Redis Redis缓存服务模块
|
||||
|
||||
这个Redis适配器提供了一个统一的接口,可以在本地开发环境使用文件存储模拟Redis,在生产环境使用真实的Redis服务。
|
||||
Redis 是应用的核心缓存服务模块,提供完整的Redis操作功能,支持开发环境的文件存储模拟和生产环境的真实Redis服务器连接,具备统一的接口规范、自动环境切换、完整的过期机制和错误处理能力。
|
||||
|
||||
## 功能特性
|
||||
## 基础键值操作
|
||||
|
||||
- 🔄 **自动切换**: 根据环境变量自动选择文件存储或真实Redis
|
||||
- 📁 **文件存储**: 本地开发时使用JSON文件模拟Redis功能
|
||||
- ⚡ **真实Redis**: 生产环境连接真实Redis服务器
|
||||
- 🕒 **过期支持**: 完整支持TTL和自动过期清理
|
||||
- 🔒 **类型安全**: 使用TypeScript接口确保类型安全
|
||||
- 📊 **日志记录**: 详细的操作日志和错误处理
|
||||
### set()
|
||||
设置键值对,支持可选的过期时间参数。
|
||||
|
||||
## 环境配置
|
||||
### get()
|
||||
获取键对应的值,不存在或已过期时返回null。
|
||||
|
||||
### 开发环境 (.env)
|
||||
```bash
|
||||
# 使用文件模拟Redis
|
||||
USE_FILE_REDIS=true
|
||||
NODE_ENV=development
|
||||
### del()
|
||||
删除指定的键,返回删除操作是否成功。
|
||||
|
||||
# Redis配置(文件模式下不会使用)
|
||||
REDIS_HOST=localhost
|
||||
REDIS_PORT=6379
|
||||
REDIS_PASSWORD=
|
||||
REDIS_DB=0
|
||||
```
|
||||
### exists()
|
||||
检查键是否存在且未过期。
|
||||
|
||||
### 生产环境 (.env.production)
|
||||
```bash
|
||||
# 使用真实Redis
|
||||
USE_FILE_REDIS=false
|
||||
NODE_ENV=production
|
||||
## 过期时间管理
|
||||
|
||||
# Redis服务器配置
|
||||
REDIS_HOST=your_redis_host
|
||||
REDIS_PORT=6379
|
||||
REDIS_PASSWORD=your_redis_password
|
||||
REDIS_DB=0
|
||||
```
|
||||
### setex()
|
||||
设置键值对并同时指定过期时间。
|
||||
|
||||
## 使用方法
|
||||
### expire()
|
||||
为现有键设置过期时间。
|
||||
|
||||
### 1. 在模块中导入
|
||||
```typescript
|
||||
import { Module } from '@nestjs/common';
|
||||
import { RedisModule } from './core/redis/redis.module';
|
||||
### ttl()
|
||||
获取键的剩余过期时间,支持状态码返回。
|
||||
|
||||
@Module({
|
||||
imports: [RedisModule],
|
||||
// ...
|
||||
})
|
||||
export class YourModule {}
|
||||
```
|
||||
## 数值操作
|
||||
|
||||
### 2. 在服务中注入
|
||||
```typescript
|
||||
import { Injectable, Inject } from '@nestjs/common';
|
||||
import { IRedisService } from './core/redis/redis.interface';
|
||||
### incr()
|
||||
键值自增操作,返回自增后的新值。
|
||||
|
||||
@Injectable()
|
||||
export class YourService {
|
||||
constructor(
|
||||
@Inject('REDIS_SERVICE') private readonly redis: IRedisService,
|
||||
) {}
|
||||
## 集合操作
|
||||
|
||||
async example() {
|
||||
// 设置键值对,30秒后过期
|
||||
await this.redis.set('user:123', 'user_data', 30);
|
||||
|
||||
// 获取值
|
||||
const value = await this.redis.get('user:123');
|
||||
|
||||
// 检查是否存在
|
||||
const exists = await this.redis.exists('user:123');
|
||||
|
||||
// 删除键
|
||||
await this.redis.del('user:123');
|
||||
}
|
||||
}
|
||||
```
|
||||
### sadd()
|
||||
向集合添加成员。
|
||||
|
||||
## API 接口
|
||||
### srem()
|
||||
从集合移除成员。
|
||||
|
||||
### set(key, value, ttl?)
|
||||
设置键值对,可选过期时间
|
||||
```typescript
|
||||
await redis.set('key', 'value', 60); // 60秒后过期
|
||||
await redis.set('key', 'value'); // 永不过期
|
||||
```
|
||||
### smembers()
|
||||
获取集合的所有成员列表。
|
||||
|
||||
### get(key)
|
||||
获取值,不存在或已过期返回null
|
||||
```typescript
|
||||
const value = await redis.get('key');
|
||||
```
|
||||
|
||||
### del(key)
|
||||
删除键,返回是否删除成功
|
||||
```typescript
|
||||
const deleted = await redis.del('key');
|
||||
```
|
||||
|
||||
### exists(key)
|
||||
检查键是否存在
|
||||
```typescript
|
||||
const exists = await redis.exists('key');
|
||||
```
|
||||
|
||||
### expire(key, ttl)
|
||||
设置键的过期时间
|
||||
```typescript
|
||||
await redis.expire('key', 300); // 5分钟后过期
|
||||
```
|
||||
|
||||
### ttl(key)
|
||||
获取键的剩余过期时间
|
||||
```typescript
|
||||
const remaining = await redis.ttl('key');
|
||||
// -1: 永不过期
|
||||
// -2: 键不存在
|
||||
// >0: 剩余秒数
|
||||
```
|
||||
## 系统操作
|
||||
|
||||
### flushall()
|
||||
清空所有数据
|
||||
```typescript
|
||||
await redis.flushall();
|
||||
```
|
||||
清空所有数据。
|
||||
|
||||
## 文件存储详情
|
||||
## 使用的项目内部依赖
|
||||
|
||||
### 数据存储位置
|
||||
- 数据目录: `./redis-data/`
|
||||
- 数据文件: `./redis-data/redis.json`
|
||||
### Injectable (来自 @nestjs/common)
|
||||
NestJS依赖注入装饰器,用于标记服务类可被注入。
|
||||
|
||||
### 过期清理
|
||||
- 自动清理: 每分钟检查并清理过期键
|
||||
- 访问时清理: 获取数据时自动检查过期状态
|
||||
- 持久化: 数据变更时自动保存到文件
|
||||
### Logger (来自 @nestjs/common)
|
||||
NestJS日志服务,用于记录操作日志和错误信息。
|
||||
|
||||
### 数据格式
|
||||
```json
|
||||
{
|
||||
"key1": {
|
||||
"value": "data",
|
||||
"expireAt": 1640995200000
|
||||
},
|
||||
"key2": {
|
||||
"value": "permanent_data"
|
||||
}
|
||||
}
|
||||
```
|
||||
### OnModuleDestroy (来自 @nestjs/common)
|
||||
NestJS生命周期接口,用于模块销毁时的资源清理。
|
||||
|
||||
## 切换模式
|
||||
### ConfigService (来自 @nestjs/config)
|
||||
NestJS配置服务,用于读取环境变量和应用配置。
|
||||
|
||||
### 自动切换规则
|
||||
1. `NODE_ENV=development` 且 `USE_FILE_REDIS=true` → 文件存储
|
||||
2. `USE_FILE_REDIS=false` → 真实Redis
|
||||
3. 生产环境默认使用真实Redis
|
||||
### ConfigModule (来自 @nestjs/config)
|
||||
NestJS配置模块,提供配置服务的依赖注入支持。
|
||||
|
||||
### 手动切换
|
||||
修改环境变量后重启应用即可切换模式:
|
||||
```bash
|
||||
# 切换到文件模式
|
||||
USE_FILE_REDIS=true
|
||||
### Redis (来自 ioredis)
|
||||
Redis客户端库,提供与Redis服务器的连接和操作功能。
|
||||
|
||||
# 切换到Redis模式
|
||||
USE_FILE_REDIS=false
|
||||
```
|
||||
### fs.promises (来自 Node.js)
|
||||
Node.js异步文件系统API,用于文件模式的数据持久化。
|
||||
|
||||
## 测试
|
||||
### path (来自 Node.js)
|
||||
Node.js路径处理工具,用于构建文件存储路径。
|
||||
|
||||
运行Redis适配器测试:
|
||||
```bash
|
||||
npm run build
|
||||
node test-redis-adapter.js
|
||||
```
|
||||
### IRedisService (本模块)
|
||||
Redis服务接口定义,规范所有Redis操作方法的签名和行为。
|
||||
|
||||
## 注意事项
|
||||
### FileRedisService (本模块)
|
||||
文件系统模拟Redis服务的实现类,适用于开发测试环境。
|
||||
|
||||
1. **数据迁移**: 文件存储和Redis之间的数据不会自动同步
|
||||
2. **性能**: 文件存储适合开发测试,生产环境建议使用Redis
|
||||
3. **并发**: 文件存储不支持高并发,仅适用于单进程开发环境
|
||||
4. **备份**: 生产环境请确保Redis数据的备份和高可用配置
|
||||
### RealRedisService (本模块)
|
||||
真实Redis服务器连接的实现类,适用于生产环境。
|
||||
|
||||
## 故障排除
|
||||
## 核心特性
|
||||
|
||||
### 文件权限错误
|
||||
确保应用有权限在项目目录创建 `redis-data` 文件夹
|
||||
### 双模式支持
|
||||
- 开发模式:使用FileRedisService进行文件存储模拟,无需外部Redis服务器
|
||||
- 生产模式:使用RealRedisService连接真实Redis服务器,提供高性能缓存
|
||||
- 自动切换:根据NODE_ENV和USE_FILE_REDIS环境变量自动选择合适的实现
|
||||
|
||||
### Redis连接失败
|
||||
检查Redis服务器配置和网络连接:
|
||||
```bash
|
||||
# 测试Redis连接
|
||||
redis-cli -h your_host -p 6379 ping
|
||||
```
|
||||
### 完整的Redis功能
|
||||
- 基础操作:支持set、get、del、exists等核心键值操作
|
||||
- 过期机制:完整的TTL支持,包括设置、查询和自动清理功能
|
||||
- 集合操作:支持sadd、srem、smembers等集合管理功能
|
||||
- 数值操作:支持incr自增操作,适用于计数器场景
|
||||
|
||||
### 模块导入错误
|
||||
确保在使用Redis服务的模块中正确导入了RedisModule
|
||||
### 数据持久化保障
|
||||
- 文件模式:使用JSON文件持久化数据,支持应用重启后数据恢复
|
||||
- 真实模式:依托Redis服务器的RDB和AOF持久化机制
|
||||
- 过期清理:文件模式提供定时过期键清理机制,每分钟自动清理
|
||||
|
||||
### 错误处理和监控
|
||||
- 连接监控:Redis连接状态监控,支持连接、错误、关闭事件处理
|
||||
- 异常处理:完整的错误捕获和日志记录,确保服务稳定性
|
||||
- 操作日志:详细的操作日志记录,便于调试和性能监控
|
||||
- 自动重连:Redis连接异常时支持自动重连机制
|
||||
|
||||
## 潜在风险
|
||||
|
||||
### 文件模式性能限制
|
||||
- 文件模式在高并发场景下性能有限,每次操作都需要文件I/O
|
||||
- 不适用于生产环境的高性能需求
|
||||
- 建议仅在开发测试环境使用,生产环境切换到真实Redis模式
|
||||
|
||||
### 数据一致性风险
|
||||
- 文件模式的过期清理是定时执行(每分钟一次),可能存在短暂的过期数据访问
|
||||
- 应用异常退出时可能导致内存数据与文件数据不一致
|
||||
- 建议在生产环境使用真实Redis服务,依托其原子操作保证一致性
|
||||
|
||||
### 环境配置依赖
|
||||
- 真实Redis模式依赖外部Redis服务器的可用性和网络连接稳定性
|
||||
- Redis服务器故障或网络异常可能导致缓存服务不可用
|
||||
- 建议配置Redis集群、主从复制和监控告警机制
|
||||
|
||||
### 内存使用风险
|
||||
- 文件模式将所有数据加载到内存Map中,大量数据可能导致内存溢出
|
||||
- 缺少内存使用限制和LRU淘汰机制
|
||||
- 建议控制缓存数据量,或在生产环境使用真实Redis的内存管理功能
|
||||
|
||||
---
|
||||
|
||||
**版本信息**
|
||||
- 模块版本:1.0.3
|
||||
- 创建日期:2025-01-07
|
||||
- 最后修改:2026-01-07
|
||||
- 作者:moyin
|
||||
@@ -1,286 +0,0 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { promises as fs } from 'fs';
|
||||
import * as path from 'path';
|
||||
import { IRedisService } from './redis.interface';
|
||||
|
||||
/**
|
||||
* 文件模拟Redis服务
|
||||
* 在本地开发环境中使用文件系统模拟Redis功能
|
||||
*/
|
||||
@Injectable()
|
||||
export class FileRedisService implements IRedisService {
|
||||
private readonly logger = new Logger(FileRedisService.name);
|
||||
private readonly dataDir = path.join(process.cwd(), 'redis-data');
|
||||
private readonly dataFile = path.join(this.dataDir, 'redis.json');
|
||||
private data: Map<string, { value: string; expireAt?: number }> = new Map();
|
||||
|
||||
constructor() {
|
||||
this.initializeStorage();
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化存储
|
||||
*/
|
||||
private async initializeStorage(): Promise<void> {
|
||||
try {
|
||||
// 确保数据目录存在
|
||||
await fs.mkdir(this.dataDir, { recursive: true });
|
||||
|
||||
// 尝试加载现有数据
|
||||
await this.loadData();
|
||||
|
||||
// 启动过期清理任务
|
||||
this.startExpirationCleanup();
|
||||
|
||||
this.logger.log('文件Redis服务初始化完成');
|
||||
} catch (error) {
|
||||
this.logger.error('初始化文件Redis服务失败', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从文件加载数据
|
||||
*/
|
||||
private async loadData(): Promise<void> {
|
||||
try {
|
||||
const fileContent = await fs.readFile(this.dataFile, '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数据存储');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存数据到文件
|
||||
*/
|
||||
private async saveData(): Promise<void> {
|
||||
try {
|
||||
const jsonData = Object.fromEntries(this.data);
|
||||
await fs.writeFile(this.dataFile, JSON.stringify(jsonData, null, 2));
|
||||
} catch (error) {
|
||||
this.logger.error('保存Redis数据到文件失败', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动过期清理任务
|
||||
*/
|
||||
private startExpirationCleanup(): void {
|
||||
setInterval(() => {
|
||||
this.cleanExpiredKeys();
|
||||
}, 60000); // 每分钟清理一次过期键
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理过期的键
|
||||
*/
|
||||
private cleanExpiredKeys(): 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键`);
|
||||
this.saveData(); // 保存清理后的数据
|
||||
}
|
||||
}
|
||||
|
||||
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 || '永不过期'}`);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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}秒`);
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
async flushall(): Promise<void> {
|
||||
this.data.clear();
|
||||
await this.saveData();
|
||||
this.logger.log('清空所有Redis数据');
|
||||
}
|
||||
|
||||
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}秒`);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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}`);
|
||||
}
|
||||
|
||||
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}`);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
587
src/core/redis/file_redis.integration.spec.ts
Normal file
587
src/core/redis/file_redis.integration.spec.ts
Normal file
@@ -0,0 +1,587 @@
|
||||
/**
|
||||
* FileRedisService集成测试
|
||||
*
|
||||
* 功能描述:
|
||||
* - 测试文件系统的真实读写操作
|
||||
* - 验证数据文件的创建和清理
|
||||
* - 测试过期清理任务的执行
|
||||
* - 验证文件Redis服务的完整工作流程
|
||||
*
|
||||
* 职责分离:
|
||||
* - 集成测试:测试与真实文件系统的交互
|
||||
* - 文件操作:验证数据文件的读写和管理
|
||||
* - 过期机制:测试自动过期清理功能
|
||||
* - 数据持久化:验证数据的持久化和恢复
|
||||
*
|
||||
* 最近修改:
|
||||
* - 2025-01-07: 功能新增 - 创建FileRedisService完整集成测试,验证真实文件系统交互
|
||||
*
|
||||
* @author moyin
|
||||
* @version 1.0.0
|
||||
* @since 2025-01-07
|
||||
* @lastModified 2025-01-07
|
||||
*/
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { FileRedisService } from './file_redis.service';
|
||||
import { promises as fs } from 'fs';
|
||||
import * as path from 'path';
|
||||
import * as os from 'os';
|
||||
|
||||
describe('FileRedisService Integration', () => {
|
||||
let service: FileRedisService;
|
||||
let module: TestingModule;
|
||||
let testDataDir: string;
|
||||
let testDataFile: string;
|
||||
|
||||
beforeAll(async () => {
|
||||
// 创建临时测试目录
|
||||
testDataDir = path.join(os.tmpdir(), 'redis-test-' + Date.now());
|
||||
testDataFile = path.join(testDataDir, 'redis.json');
|
||||
|
||||
// 确保测试目录存在
|
||||
await fs.mkdir(testDataDir, { recursive: true });
|
||||
|
||||
module = await Test.createTestingModule({
|
||||
providers: [FileRedisService],
|
||||
}).compile();
|
||||
|
||||
service = module.get<FileRedisService>(FileRedisService);
|
||||
|
||||
// 修改服务实例的数据目录路径
|
||||
(service as any).DATA_DIR = testDataDir;
|
||||
(service as any).DATA_FILE = testDataFile;
|
||||
|
||||
// 重新初始化存储以使用新的路径
|
||||
await (service as any).initializeStorage();
|
||||
|
||||
// 等待服务初始化完成
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
if (module) {
|
||||
await module.close();
|
||||
}
|
||||
|
||||
// 清理测试文件
|
||||
try {
|
||||
await fs.rm(testDataDir, { recursive: true, force: true });
|
||||
} catch (error) {
|
||||
// 忽略清理错误
|
||||
}
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
// 每个测试前清空数据
|
||||
await service.flushall();
|
||||
});
|
||||
|
||||
describe('文件系统初始化', () => {
|
||||
it('should create data directory on initialization', async () => {
|
||||
const stats = await fs.stat(testDataDir);
|
||||
expect(stats.isDirectory()).toBe(true);
|
||||
});
|
||||
|
||||
it('should create data file after first write operation', async () => {
|
||||
await service.set('test:init', 'initialization test');
|
||||
|
||||
const stats = await fs.stat(testDataFile);
|
||||
expect(stats.isFile()).toBe(true);
|
||||
});
|
||||
|
||||
it('should load existing data from file on restart', async () => {
|
||||
// 设置一些数据
|
||||
await service.set('test:persist', 'persistent data');
|
||||
await service.set('test:number', '42');
|
||||
await service.sadd('test:set', 'member1');
|
||||
|
||||
// 创建新的服务实例来模拟重启
|
||||
const newModule = await Test.createTestingModule({
|
||||
providers: [FileRedisService],
|
||||
}).compile();
|
||||
|
||||
const newService = newModule.get<FileRedisService>(FileRedisService);
|
||||
|
||||
// 修改新服务实例的数据目录路径
|
||||
(newService as any).DATA_DIR = testDataDir;
|
||||
(newService as any).DATA_FILE = testDataFile;
|
||||
|
||||
// 重新初始化存储
|
||||
await newService.initializeStorage();
|
||||
|
||||
// 等待新服务初始化完成
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
|
||||
// 验证数据是否被正确加载
|
||||
const persistentData = await newService.get('test:persist');
|
||||
const numberData = await newService.get('test:number');
|
||||
const setMembers = await newService.smembers('test:set');
|
||||
|
||||
expect(persistentData).toBe('persistent data');
|
||||
expect(numberData).toBe('42');
|
||||
expect(setMembers).toContain('member1');
|
||||
|
||||
// 清理新服务
|
||||
await newModule.close();
|
||||
});
|
||||
|
||||
it('should handle corrupted data file gracefully', async () => {
|
||||
// 写入无效的JSON数据
|
||||
await fs.writeFile(testDataFile, 'invalid json content');
|
||||
|
||||
// 创建新的服务实例
|
||||
const newModule = await Test.createTestingModule({
|
||||
providers: [FileRedisService],
|
||||
}).compile();
|
||||
|
||||
const newService = newModule.get<FileRedisService>(FileRedisService);
|
||||
|
||||
// 修改新服务实例的数据目录路径
|
||||
(newService as any).DATA_DIR = testDataDir;
|
||||
(newService as any).DATA_FILE = testDataFile;
|
||||
|
||||
// 重新初始化存储
|
||||
await newService.initializeStorage();
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
|
||||
// 应该能正常工作,从空数据开始
|
||||
await newService.set('test:recovery', 'recovered');
|
||||
const result = await newService.get('test:recovery');
|
||||
|
||||
expect(result).toBe('recovered');
|
||||
|
||||
// 清理新服务
|
||||
await newModule.close();
|
||||
});
|
||||
|
||||
it('should handle missing data file gracefully', async () => {
|
||||
// 删除数据文件
|
||||
try {
|
||||
await fs.unlink(testDataFile);
|
||||
} catch (error) {
|
||||
// 文件可能不存在,忽略错误
|
||||
}
|
||||
|
||||
// 创建新的服务实例
|
||||
const newModule = await Test.createTestingModule({
|
||||
providers: [FileRedisService],
|
||||
}).compile();
|
||||
|
||||
const newService = newModule.get<FileRedisService>(FileRedisService);
|
||||
|
||||
// 修改新服务实例的数据目录路径
|
||||
(newService as any).DATA_DIR = testDataDir;
|
||||
(newService as any).DATA_FILE = testDataFile;
|
||||
|
||||
// 重新初始化存储
|
||||
await newService.initializeStorage();
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
|
||||
// 应该能正常工作
|
||||
await newService.set('test:new_start', 'new beginning');
|
||||
const result = await newService.get('test:new_start');
|
||||
|
||||
expect(result).toBe('new beginning');
|
||||
|
||||
// 清理新服务
|
||||
await newModule.close();
|
||||
});
|
||||
});
|
||||
|
||||
describe('数据持久化', () => {
|
||||
it('should persist data to file after each operation', async () => {
|
||||
await service.set('test:file_persist', 'file persistence test');
|
||||
|
||||
// 读取文件内容
|
||||
const fileContent = await fs.readFile(testDataFile, 'utf-8');
|
||||
const data = JSON.parse(fileContent);
|
||||
|
||||
expect(data['test:file_persist']).toBeDefined();
|
||||
expect(data['test:file_persist'].value).toBe('file persistence test');
|
||||
});
|
||||
|
||||
it('should maintain data format in JSON file', async () => {
|
||||
await service.set('test:string', 'string value');
|
||||
await service.set('test:with_ttl', 'ttl value', 3600);
|
||||
await service.sadd('test:set', 'set member');
|
||||
|
||||
const fileContent = await fs.readFile(testDataFile, 'utf-8');
|
||||
const data = JSON.parse(fileContent);
|
||||
|
||||
// 验证字符串数据格式
|
||||
expect(data['test:string']).toEqual({
|
||||
value: 'string value'
|
||||
});
|
||||
|
||||
// 验证带TTL的数据格式
|
||||
expect(data['test:with_ttl']).toEqual({
|
||||
value: 'ttl value',
|
||||
expireAt: expect.any(Number)
|
||||
});
|
||||
|
||||
// 验证集合数据格式
|
||||
expect(data['test:set']).toEqual({
|
||||
value: expect.stringContaining('set member')
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle concurrent write operations', async () => {
|
||||
// 并发执行多个写操作
|
||||
const promises = [];
|
||||
for (let i = 0; i < 10; i++) {
|
||||
promises.push(service.set(`test:concurrent:${i}`, `value${i}`));
|
||||
}
|
||||
|
||||
await Promise.all(promises);
|
||||
|
||||
// 验证所有数据都被正确保存
|
||||
for (let i = 0; i < 10; i++) {
|
||||
const value = await service.get(`test:concurrent:${i}`);
|
||||
expect(value).toBe(`value${i}`);
|
||||
}
|
||||
|
||||
// 验证文件内容
|
||||
const fileContent = await fs.readFile(testDataFile, 'utf-8');
|
||||
const data = JSON.parse(fileContent);
|
||||
|
||||
for (let i = 0; i < 10; i++) {
|
||||
expect(data[`test:concurrent:${i}`]).toBeDefined();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('过期机制集成测试', () => {
|
||||
it('should automatically clean expired keys', async () => {
|
||||
jest.useFakeTimers();
|
||||
|
||||
// 设置一些带过期时间的键
|
||||
await service.set('test:expire1', 'expires in 1 sec', 1);
|
||||
await service.set('test:expire2', 'expires in 2 sec', 2);
|
||||
await service.set('test:permanent', 'never expires');
|
||||
|
||||
// 模拟时间流逝
|
||||
jest.advanceTimersByTime(1500); // 1.5秒后
|
||||
|
||||
// 手动触发清理(模拟定时器执行)
|
||||
await (service as any).cleanExpiredKeys();
|
||||
|
||||
// 验证过期键被清理
|
||||
const expired1 = await service.get('test:expire1');
|
||||
const notExpired = await service.get('test:expire2');
|
||||
const permanent = await service.get('test:permanent');
|
||||
|
||||
expect(expired1).toBeNull();
|
||||
expect(notExpired).toBe('expires in 2 sec');
|
||||
expect(permanent).toBe('never expires');
|
||||
|
||||
// 验证文件中也被清理
|
||||
const fileContent = await fs.readFile(testDataFile, 'utf-8');
|
||||
const data = JSON.parse(fileContent);
|
||||
|
||||
expect(data['test:expire1']).toBeUndefined();
|
||||
expect(data['test:expire2']).toBeDefined();
|
||||
expect(data['test:permanent']).toBeDefined();
|
||||
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
it('should filter expired data during file loading', async () => {
|
||||
const now = Date.now();
|
||||
|
||||
// 手动创建包含过期数据的文件
|
||||
const testData = {
|
||||
'valid_key': { value: 'valid value' },
|
||||
'expired_key': { value: 'expired value', expireAt: now - 1000 },
|
||||
'future_key': { value: 'future value', expireAt: now + 3600000 }
|
||||
};
|
||||
|
||||
await fs.writeFile(testDataFile, JSON.stringify(testData, null, 2));
|
||||
|
||||
// 创建新的服务实例来加载数据
|
||||
const newModule = await Test.createTestingModule({
|
||||
providers: [FileRedisService],
|
||||
}).compile();
|
||||
|
||||
const newService = newModule.get<FileRedisService>(FileRedisService);
|
||||
|
||||
// 修改新服务实例的数据目录路径
|
||||
(newService as any).DATA_DIR = testDataDir;
|
||||
(newService as any).DATA_FILE = testDataFile;
|
||||
|
||||
// 重新初始化存储
|
||||
await newService.initializeStorage();
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
|
||||
// 验证只有有效数据被加载
|
||||
const validValue = await newService.get('valid_key');
|
||||
const expiredValue = await newService.get('expired_key');
|
||||
const futureValue = await newService.get('future_key');
|
||||
|
||||
expect(validValue).toBe('valid value');
|
||||
expect(expiredValue).toBeNull();
|
||||
expect(futureValue).toBe('future value');
|
||||
|
||||
// 清理新服务
|
||||
await newModule.close();
|
||||
}, 10000);
|
||||
|
||||
it('should handle TTL operations correctly', async () => {
|
||||
jest.useFakeTimers();
|
||||
const now = Date.now();
|
||||
jest.setSystemTime(now);
|
||||
|
||||
// 设置带TTL的键
|
||||
await service.set('test:ttl', 'ttl test', 3600);
|
||||
|
||||
// 检查TTL
|
||||
const ttl1 = await service.ttl('test:ttl');
|
||||
expect(ttl1).toBe(3600);
|
||||
|
||||
// 模拟时间流逝
|
||||
jest.advanceTimersByTime(1800 * 1000); // 30分钟
|
||||
|
||||
const ttl2 = await service.ttl('test:ttl');
|
||||
expect(ttl2).toBe(1800);
|
||||
|
||||
// 设置新的过期时间
|
||||
await service.expire('test:ttl', 600);
|
||||
const ttl3 = await service.ttl('test:ttl');
|
||||
expect(ttl3).toBe(600);
|
||||
|
||||
jest.useRealTimers();
|
||||
});
|
||||
});
|
||||
|
||||
describe('集合操作集成测试', () => {
|
||||
it('should persist set operations to file', async () => {
|
||||
await service.sadd('test:file_set', 'member1');
|
||||
await service.sadd('test:file_set', 'member2');
|
||||
await service.sadd('test:file_set', 'member3');
|
||||
|
||||
// 验证文件内容
|
||||
const fileContent = await fs.readFile(testDataFile, 'utf-8');
|
||||
const data = JSON.parse(fileContent);
|
||||
|
||||
const setData = JSON.parse(data['test:file_set'].value);
|
||||
expect(setData).toContain('member1');
|
||||
expect(setData).toContain('member2');
|
||||
expect(setData).toContain('member3');
|
||||
});
|
||||
|
||||
it('should handle set operations with expiration', async () => {
|
||||
jest.useFakeTimers();
|
||||
|
||||
await service.sadd('test:expire_set', 'member1');
|
||||
await service.expire('test:expire_set', 2);
|
||||
await service.sadd('test:expire_set', 'member2');
|
||||
|
||||
// 验证过期时间被保持
|
||||
const ttl = await service.ttl('test:expire_set');
|
||||
expect(ttl).toBe(2);
|
||||
|
||||
// 验证成员都存在
|
||||
const members = await service.smembers('test:expire_set');
|
||||
expect(members).toContain('member1');
|
||||
expect(members).toContain('member2');
|
||||
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
it('should clean up empty sets after member removal', async () => {
|
||||
await service.sadd('test:cleanup_set', 'only_member');
|
||||
await service.srem('test:cleanup_set', 'only_member');
|
||||
|
||||
// 验证集合被删除
|
||||
const members = await service.smembers('test:cleanup_set');
|
||||
expect(members).toEqual([]);
|
||||
|
||||
// 验证文件中也被删除
|
||||
const fileContent = await fs.readFile(testDataFile, 'utf-8');
|
||||
const data = JSON.parse(fileContent);
|
||||
expect(data['test:cleanup_set']).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('数值操作集成测试', () => {
|
||||
it('should persist counter increments', async () => {
|
||||
await service.incr('test:file_counter');
|
||||
await service.incr('test:file_counter');
|
||||
await service.incr('test:file_counter');
|
||||
|
||||
// 验证内存中的值
|
||||
const memoryValue = await service.get('test:file_counter');
|
||||
expect(memoryValue).toBe('3');
|
||||
|
||||
// 验证文件中的值
|
||||
const fileContent = await fs.readFile(testDataFile, 'utf-8');
|
||||
const data = JSON.parse(fileContent);
|
||||
expect(data['test:file_counter'].value).toBe('3');
|
||||
});
|
||||
|
||||
it('should maintain counter state across service restarts', async () => {
|
||||
await service.incr('test:persistent_counter');
|
||||
await service.incr('test:persistent_counter');
|
||||
|
||||
// 创建新的服务实例
|
||||
const newModule = await Test.createTestingModule({
|
||||
providers: [FileRedisService],
|
||||
}).compile();
|
||||
|
||||
const newService = newModule.get<FileRedisService>(FileRedisService);
|
||||
|
||||
// 修改新服务实例的数据目录路径
|
||||
(newService as any).DATA_DIR = testDataDir;
|
||||
(newService as any).DATA_FILE = testDataFile;
|
||||
|
||||
// 重新初始化存储
|
||||
await newService.initializeStorage();
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
|
||||
// 继续递增
|
||||
const result = await newService.incr('test:persistent_counter');
|
||||
expect(result).toBe(3);
|
||||
|
||||
// 清理新服务
|
||||
await newModule.close();
|
||||
});
|
||||
});
|
||||
|
||||
describe('错误处理和恢复', () => {
|
||||
it('should handle file system permission errors gracefully', async () => {
|
||||
// 这个测试在某些环境下可能无法执行,所以使用try-catch
|
||||
try {
|
||||
// 尝试创建只读目录(在某些系统上可能不起作用)
|
||||
const readOnlyDir = path.join(os.tmpdir(), 'readonly-redis-test');
|
||||
await fs.mkdir(readOnlyDir, { mode: 0o444 });
|
||||
|
||||
// 修改服务的数据目录
|
||||
(service as any).DATA_DIR = readOnlyDir;
|
||||
(service as any).DATA_FILE = path.join(readOnlyDir, 'redis.json');
|
||||
|
||||
// 尝试写入数据(应该不会抛出异常)
|
||||
await expect(service.set('test:readonly', 'test')).resolves.not.toThrow();
|
||||
|
||||
// 清理
|
||||
await fs.rm(readOnlyDir, { recursive: true, force: true });
|
||||
} catch (error) {
|
||||
// 如果无法创建只读目录,跳过此测试
|
||||
console.warn('无法测试文件系统权限错误,跳过此测试');
|
||||
}
|
||||
});
|
||||
|
||||
it('should recover from disk space issues', async () => {
|
||||
// 模拟磁盘空间不足的情况比较困难,这里主要测试错误处理逻辑
|
||||
const originalWriteFile = fs.writeFile;
|
||||
|
||||
// Mock writeFile to simulate disk space error
|
||||
(fs.writeFile as jest.Mock) = jest.fn().mockRejectedValueOnce(
|
||||
new Error('ENOSPC: no space left on device')
|
||||
);
|
||||
|
||||
// 应该不会抛出异常
|
||||
await expect(service.set('test:disk_full', 'test')).resolves.not.toThrow();
|
||||
|
||||
// 恢复原始函数
|
||||
(fs.writeFile as any) = originalWriteFile;
|
||||
});
|
||||
});
|
||||
|
||||
describe('性能和大数据量测试', () => {
|
||||
it('should handle large amounts of data efficiently', async () => {
|
||||
const startTime = Date.now();
|
||||
const dataCount = 1000;
|
||||
|
||||
// 写入大量数据
|
||||
const writePromises = [];
|
||||
for (let i = 0; i < dataCount; i++) {
|
||||
writePromises.push(service.set(`test:large:${i}`, `value${i}`));
|
||||
}
|
||||
await Promise.all(writePromises);
|
||||
|
||||
// 读取所有数据
|
||||
const readPromises = [];
|
||||
for (let i = 0; i < dataCount; i++) {
|
||||
readPromises.push(service.get(`test:large:${i}`));
|
||||
}
|
||||
const results = await Promise.all(readPromises);
|
||||
|
||||
const endTime = Date.now();
|
||||
const duration = endTime - startTime;
|
||||
|
||||
// 验证数据正确性
|
||||
expect(results).toHaveLength(dataCount);
|
||||
results.forEach((result, index) => {
|
||||
expect(result).toBe(`value${index}`);
|
||||
});
|
||||
|
||||
// 验证文件大小合理
|
||||
const stats = await fs.stat(testDataFile);
|
||||
expect(stats.size).toBeGreaterThan(0);
|
||||
|
||||
// 性能检查(应该在合理时间内完成)
|
||||
expect(duration).toBeLessThan(10000); // 10秒内完成
|
||||
|
||||
console.log(`处理${dataCount}条数据耗时: ${duration}ms, 文件大小: ${stats.size} bytes`);
|
||||
}, 15000);
|
||||
|
||||
it('should handle very large values', async () => {
|
||||
const largeValue = 'x'.repeat(100000); // 100KB的数据
|
||||
|
||||
await service.set('test:large_value', largeValue);
|
||||
const result = await service.get('test:large_value');
|
||||
|
||||
expect(result).toBe(largeValue);
|
||||
|
||||
// 验证文件能正确存储大数据
|
||||
const fileContent = await fs.readFile(testDataFile, 'utf-8');
|
||||
const data = JSON.parse(fileContent);
|
||||
expect(data['test:large_value'].value).toBe(largeValue);
|
||||
});
|
||||
});
|
||||
|
||||
describe('数据完整性验证', () => {
|
||||
it('should maintain data integrity across multiple operations', async () => {
|
||||
// 执行各种操作的组合
|
||||
await service.set('test:integrity:string', 'string value');
|
||||
await service.set('test:integrity:number', '42');
|
||||
await service.set('test:integrity:ttl', 'ttl value', 3600);
|
||||
await service.sadd('test:integrity:set', 'member1');
|
||||
await service.sadd('test:integrity:set', 'member2');
|
||||
await service.incr('test:integrity:counter');
|
||||
await service.incr('test:integrity:counter');
|
||||
|
||||
// 验证所有数据的完整性
|
||||
const stringValue = await service.get('test:integrity:string');
|
||||
const numberValue = await service.get('test:integrity:number');
|
||||
const ttlValue = await service.get('test:integrity:ttl');
|
||||
const setMembers = await service.smembers('test:integrity:set');
|
||||
const counterValue = await service.get('test:integrity:counter');
|
||||
const ttl = await service.ttl('test:integrity:ttl');
|
||||
|
||||
expect(stringValue).toBe('string value');
|
||||
expect(numberValue).toBe('42');
|
||||
expect(ttlValue).toBe('ttl value');
|
||||
expect(setMembers).toHaveLength(2);
|
||||
expect(setMembers).toContain('member1');
|
||||
expect(setMembers).toContain('member2');
|
||||
expect(counterValue).toBe('2');
|
||||
expect(ttl).toBeGreaterThan(0);
|
||||
|
||||
// 验证文件内容的完整性
|
||||
const fileContent = await fs.readFile(testDataFile, 'utf-8');
|
||||
const data = JSON.parse(fileContent);
|
||||
|
||||
expect(Object.keys(data)).toHaveLength(5);
|
||||
expect(data['test:integrity:string'].value).toBe('string value');
|
||||
expect(data['test:integrity:number'].value).toBe('42');
|
||||
expect(data['test:integrity:ttl'].value).toBe('ttl value');
|
||||
expect(data['test:integrity:ttl'].expireAt).toBeGreaterThan(Date.now());
|
||||
expect(data['test:integrity:set'].value).toContain('member1');
|
||||
expect(data['test:integrity:counter'].value).toBe('2');
|
||||
});
|
||||
});
|
||||
});
|
||||
631
src/core/redis/file_redis.service.spec.ts
Normal file
631
src/core/redis/file_redis.service.spec.ts
Normal file
@@ -0,0 +1,631 @@
|
||||
/**
|
||||
* FileRedisService单元测试
|
||||
*
|
||||
* 功能描述:
|
||||
* - 测试文件模拟Redis服务的所有公共方法
|
||||
* - 验证文件系统操作和数据持久化
|
||||
* - 测试过期时间机制和自动清理功能
|
||||
* - 测试正常情况、异常情况和边界情况
|
||||
*
|
||||
* 职责分离:
|
||||
* - 单元测试:隔离测试每个方法的功能
|
||||
* - Mock测试:使用模拟文件系统避免真实文件操作
|
||||
* - 过期测试:验证TTL机制和自动清理
|
||||
* - 边界测试:测试参数边界和特殊情况
|
||||
*
|
||||
* 最近修改:
|
||||
* - 2025-01-07: 功能新增 - 创建FileRedisService完整单元测试,覆盖所有公共方法
|
||||
*
|
||||
* @author moyin
|
||||
* @version 1.0.0
|
||||
* @since 2025-01-07
|
||||
* @lastModified 2025-01-07
|
||||
*/
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { FileRedisService } from './file_redis.service';
|
||||
import { promises as fs } from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
// Mock fs promises
|
||||
jest.mock('fs', () => ({
|
||||
promises: {
|
||||
mkdir: jest.fn(),
|
||||
readFile: jest.fn(),
|
||||
writeFile: jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
// Mock path
|
||||
jest.mock('path');
|
||||
|
||||
// Mock global timers
|
||||
const mockSetInterval = jest.fn();
|
||||
const mockClearInterval = jest.fn();
|
||||
global.setInterval = mockSetInterval;
|
||||
global.clearInterval = mockClearInterval;
|
||||
|
||||
describe('FileRedisService', () => {
|
||||
let service: FileRedisService;
|
||||
let mockFs: jest.Mocked<typeof fs>;
|
||||
let mockPath: jest.Mocked<typeof path>;
|
||||
let mockSetInterval: jest.Mock;
|
||||
let mockClearInterval: jest.Mock;
|
||||
|
||||
beforeEach(async () => {
|
||||
mockFs = fs as jest.Mocked<typeof fs>;
|
||||
mockPath = path as jest.Mocked<typeof path>;
|
||||
mockSetInterval = global.setInterval as jest.Mock;
|
||||
mockClearInterval = global.clearInterval as jest.Mock;
|
||||
|
||||
// Mock path.join to return predictable paths
|
||||
mockPath.join.mockImplementation((...args) => args.join('/'));
|
||||
|
||||
// Mock process.cwd()
|
||||
jest.spyOn(process, 'cwd').mockReturnValue('/test/project');
|
||||
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [FileRedisService],
|
||||
}).compile();
|
||||
|
||||
service = module.get<FileRedisService>(FileRedisService);
|
||||
|
||||
// 等待构造函数中的异步初始化完成
|
||||
await new Promise(resolve => setTimeout(resolve, 10));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
jest.clearAllTimers();
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
describe('构造函数和初始化', () => {
|
||||
it('should create service successfully', () => {
|
||||
expect(service).toBeDefined();
|
||||
});
|
||||
|
||||
it('should create data directory during initialization', () => {
|
||||
expect(mockFs.mkdir).toHaveBeenCalledWith('/test/project/redis-data', { recursive: true });
|
||||
});
|
||||
|
||||
it('should attempt to load existing data', () => {
|
||||
expect(mockFs.readFile).toHaveBeenCalledWith('/test/project/redis-data/redis.json', 'utf-8');
|
||||
});
|
||||
});
|
||||
|
||||
describe('set', () => {
|
||||
beforeEach(() => {
|
||||
mockFs.writeFile.mockResolvedValue(undefined);
|
||||
});
|
||||
|
||||
it('should set key-value without TTL', async () => {
|
||||
await service.set('testKey', 'testValue');
|
||||
|
||||
expect(mockFs.writeFile).toHaveBeenCalledWith(
|
||||
'/test/project/redis-data/redis.json',
|
||||
expect.stringContaining('"testKey"')
|
||||
);
|
||||
});
|
||||
|
||||
it('should set key-value with TTL', async () => {
|
||||
jest.useFakeTimers();
|
||||
const now = Date.now();
|
||||
jest.setSystemTime(now);
|
||||
|
||||
await service.set('testKey', 'testValue', 3600);
|
||||
|
||||
const expectedExpireAt = now + 3600 * 1000;
|
||||
expect(mockFs.writeFile).toHaveBeenCalledWith(
|
||||
'/test/project/redis-data/redis.json',
|
||||
expect.stringContaining(expectedExpireAt.toString())
|
||||
);
|
||||
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
it('should not set TTL when TTL is 0', async () => {
|
||||
await service.set('testKey', 'testValue', 0);
|
||||
|
||||
expect(mockFs.writeFile).toHaveBeenCalledWith(
|
||||
'/test/project/redis-data/redis.json',
|
||||
expect.not.stringContaining('expireAt')
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle file write errors gracefully', async () => {
|
||||
const error = new Error('File write failed');
|
||||
mockFs.writeFile.mockRejectedValue(error);
|
||||
|
||||
// 应该不抛出异常,而是在内部处理
|
||||
await expect(service.set('testKey', 'testValue')).resolves.not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('get', () => {
|
||||
it('should return value when key exists and not expired', async () => {
|
||||
// 模拟内存中有数据
|
||||
const testData = new Map([
|
||||
['testKey', { value: 'testValue' }]
|
||||
]);
|
||||
(service as any).data = testData;
|
||||
|
||||
const result = await service.get('testKey');
|
||||
|
||||
expect(result).toBe('testValue');
|
||||
});
|
||||
|
||||
it('should return null when key does not exist', async () => {
|
||||
// 确保内存中没有数据
|
||||
(service as any).data = new Map();
|
||||
|
||||
const result = await service.get('nonExistentKey');
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it('should return null and remove expired key', async () => {
|
||||
jest.useFakeTimers();
|
||||
const now = Date.now();
|
||||
jest.setSystemTime(now);
|
||||
|
||||
// 模拟过期的数据
|
||||
const testData = new Map([
|
||||
['expiredKey', { value: 'expiredValue', expireAt: now - 1000 }]
|
||||
]);
|
||||
(service as any).data = testData;
|
||||
mockFs.writeFile.mockResolvedValue(undefined);
|
||||
|
||||
const result = await service.get('expiredKey');
|
||||
|
||||
expect(result).toBeNull();
|
||||
expect(testData.has('expiredKey')).toBe(false);
|
||||
expect(mockFs.writeFile).toHaveBeenCalled();
|
||||
|
||||
jest.useRealTimers();
|
||||
});
|
||||
});
|
||||
|
||||
describe('del', () => {
|
||||
beforeEach(() => {
|
||||
mockFs.writeFile.mockResolvedValue(undefined);
|
||||
});
|
||||
|
||||
it('should return true when key is deleted', async () => {
|
||||
const testData = new Map([
|
||||
['testKey', { value: 'testValue' }]
|
||||
]);
|
||||
(service as any).data = testData;
|
||||
|
||||
const result = await service.del('testKey');
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(testData.has('testKey')).toBe(false);
|
||||
expect(mockFs.writeFile).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should return false when key does not exist', async () => {
|
||||
(service as any).data = new Map();
|
||||
|
||||
const result = await service.del('nonExistentKey');
|
||||
|
||||
expect(result).toBe(false);
|
||||
expect(mockFs.writeFile).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('exists', () => {
|
||||
it('should return true when key exists and not expired', async () => {
|
||||
const testData = new Map([
|
||||
['testKey', { value: 'testValue' }]
|
||||
]);
|
||||
(service as any).data = testData;
|
||||
|
||||
const result = await service.exists('testKey');
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false when key does not exist', async () => {
|
||||
(service as any).data = new Map();
|
||||
|
||||
const result = await service.exists('nonExistentKey');
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false and remove expired key', async () => {
|
||||
jest.useFakeTimers();
|
||||
const now = Date.now();
|
||||
jest.setSystemTime(now);
|
||||
|
||||
const testData = new Map([
|
||||
['expiredKey', { value: 'expiredValue', expireAt: now - 1000 }]
|
||||
]);
|
||||
(service as any).data = testData;
|
||||
mockFs.writeFile.mockResolvedValue(undefined);
|
||||
|
||||
const result = await service.exists('expiredKey');
|
||||
|
||||
expect(result).toBe(false);
|
||||
expect(testData.has('expiredKey')).toBe(false);
|
||||
|
||||
jest.useRealTimers();
|
||||
});
|
||||
});
|
||||
|
||||
describe('expire', () => {
|
||||
beforeEach(() => {
|
||||
mockFs.writeFile.mockResolvedValue(undefined);
|
||||
});
|
||||
|
||||
it('should set expiration time for existing key', async () => {
|
||||
jest.useFakeTimers();
|
||||
const now = Date.now();
|
||||
jest.setSystemTime(now);
|
||||
|
||||
const testData = new Map([
|
||||
['testKey', { value: 'testValue' }]
|
||||
]);
|
||||
(service as any).data = testData;
|
||||
|
||||
await service.expire('testKey', 3600);
|
||||
|
||||
const item = testData.get('testKey');
|
||||
expect((item as any)?.expireAt).toBe(now + 3600 * 1000);
|
||||
expect(mockFs.writeFile).toHaveBeenCalled();
|
||||
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
it('should do nothing for non-existent key', async () => {
|
||||
(service as any).data = new Map();
|
||||
|
||||
await service.expire('nonExistentKey', 3600);
|
||||
|
||||
expect(mockFs.writeFile).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('ttl', () => {
|
||||
it('should return remaining TTL for key with expiration', async () => {
|
||||
jest.useFakeTimers();
|
||||
const now = Date.now();
|
||||
jest.setSystemTime(now);
|
||||
|
||||
const testData = new Map([
|
||||
['testKey', { value: 'testValue', expireAt: now + 3600 * 1000 }]
|
||||
]);
|
||||
(service as any).data = testData;
|
||||
|
||||
const result = await service.ttl('testKey');
|
||||
|
||||
expect(result).toBe(3600);
|
||||
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
it('should return -1 for key without expiration', async () => {
|
||||
const testData = new Map([
|
||||
['testKey', { value: 'testValue' }]
|
||||
]);
|
||||
(service as any).data = testData;
|
||||
|
||||
const result = await service.ttl('testKey');
|
||||
|
||||
expect(result).toBe(-1);
|
||||
});
|
||||
|
||||
it('should return -2 for non-existent key', async () => {
|
||||
(service as any).data = new Map();
|
||||
|
||||
const result = await service.ttl('nonExistentKey');
|
||||
|
||||
expect(result).toBe(-2);
|
||||
});
|
||||
|
||||
it('should return -2 and remove expired key', async () => {
|
||||
jest.useFakeTimers();
|
||||
const now = Date.now();
|
||||
jest.setSystemTime(now);
|
||||
|
||||
const testData = new Map([
|
||||
['expiredKey', { value: 'expiredValue', expireAt: now - 1000 }]
|
||||
]);
|
||||
(service as any).data = testData;
|
||||
mockFs.writeFile.mockResolvedValue(undefined);
|
||||
|
||||
const result = await service.ttl('expiredKey');
|
||||
|
||||
expect(result).toBe(-2);
|
||||
expect(testData.has('expiredKey')).toBe(false);
|
||||
|
||||
jest.useRealTimers();
|
||||
});
|
||||
});
|
||||
|
||||
describe('flushall', () => {
|
||||
it('should clear all data', async () => {
|
||||
const testData = new Map([
|
||||
['key1', { value: 'value1' }],
|
||||
['key2', { value: 'value2' }]
|
||||
]);
|
||||
(service as any).data = testData;
|
||||
mockFs.writeFile.mockResolvedValue(undefined);
|
||||
|
||||
await service.flushall();
|
||||
|
||||
expect(testData.size).toBe(0);
|
||||
expect(mockFs.writeFile).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('setex', () => {
|
||||
it('should set key with expiration time', async () => {
|
||||
jest.useFakeTimers();
|
||||
const now = Date.now();
|
||||
jest.setSystemTime(now);
|
||||
|
||||
mockFs.writeFile.mockResolvedValue(undefined);
|
||||
|
||||
await service.setex('testKey', 1800, 'testValue');
|
||||
|
||||
const testData = (service as any).data;
|
||||
const item = testData.get('testKey');
|
||||
expect(item.value).toBe('testValue');
|
||||
expect(item.expireAt).toBe(now + 1800 * 1000);
|
||||
|
||||
jest.useRealTimers();
|
||||
});
|
||||
});
|
||||
|
||||
describe('incr', () => {
|
||||
beforeEach(() => {
|
||||
mockFs.writeFile.mockResolvedValue(undefined);
|
||||
});
|
||||
|
||||
it('should increment existing numeric value', async () => {
|
||||
const testData = new Map([
|
||||
['counter', { value: '5' }]
|
||||
]);
|
||||
(service as any).data = testData;
|
||||
|
||||
const result = await service.incr('counter');
|
||||
|
||||
expect(result).toBe(6);
|
||||
expect(testData.get('counter').value).toBe('6');
|
||||
});
|
||||
|
||||
it('should initialize non-existent key to 1', async () => {
|
||||
(service as any).data = new Map();
|
||||
|
||||
const result = await service.incr('newCounter');
|
||||
|
||||
expect(result).toBe(1);
|
||||
const testData = (service as any).data;
|
||||
expect(testData.get('newCounter').value).toBe('1');
|
||||
});
|
||||
});
|
||||
|
||||
describe('sadd', () => {
|
||||
beforeEach(() => {
|
||||
mockFs.writeFile.mockResolvedValue(undefined);
|
||||
});
|
||||
|
||||
it('should add member to new set', async () => {
|
||||
(service as any).data = new Map();
|
||||
|
||||
await service.sadd('users', 'user123');
|
||||
|
||||
const testData = (service as any).data;
|
||||
const setData = JSON.parse(testData.get('users').value);
|
||||
expect(setData).toContain('user123');
|
||||
});
|
||||
|
||||
it('should add member to existing set', async () => {
|
||||
const testData = new Map([
|
||||
['users', { value: JSON.stringify(['user1', 'user2']) }]
|
||||
]);
|
||||
(service as any).data = testData;
|
||||
|
||||
await service.sadd('users', 'user3');
|
||||
|
||||
const setData = JSON.parse(testData.get('users').value);
|
||||
expect(setData).toContain('user1');
|
||||
expect(setData).toContain('user2');
|
||||
expect(setData).toContain('user3');
|
||||
});
|
||||
|
||||
it('should preserve expiration time when adding to existing set', async () => {
|
||||
const expireAt = Date.now() + 3600 * 1000;
|
||||
const testData = new Map([
|
||||
['users', { value: JSON.stringify(['user1']), expireAt }]
|
||||
]);
|
||||
(service as any).data = testData;
|
||||
|
||||
await service.sadd('users', 'user2');
|
||||
|
||||
expect(testData.get('users').expireAt).toBe(expireAt);
|
||||
});
|
||||
});
|
||||
|
||||
describe('srem', () => {
|
||||
beforeEach(() => {
|
||||
mockFs.writeFile.mockResolvedValue(undefined);
|
||||
});
|
||||
|
||||
it('should remove member from set', async () => {
|
||||
const testData = new Map([
|
||||
['users', { value: JSON.stringify(['user1', 'user2', 'user3']) }]
|
||||
]);
|
||||
(service as any).data = testData;
|
||||
|
||||
await service.srem('users', 'user2');
|
||||
|
||||
const setData = JSON.parse(testData.get('users').value);
|
||||
expect(setData).not.toContain('user2');
|
||||
expect(setData).toContain('user1');
|
||||
expect(setData).toContain('user3');
|
||||
});
|
||||
|
||||
it('should delete key when set becomes empty', async () => {
|
||||
const testData = new Map([
|
||||
['users', { value: JSON.stringify(['user1']) }]
|
||||
]);
|
||||
(service as any).data = testData;
|
||||
|
||||
await service.srem('users', 'user1');
|
||||
|
||||
expect(testData.has('users')).toBe(false);
|
||||
});
|
||||
|
||||
it('should do nothing for non-existent key', async () => {
|
||||
(service as any).data = new Map();
|
||||
|
||||
await service.srem('nonExistentSet', 'member');
|
||||
|
||||
expect(mockFs.writeFile).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('smembers', () => {
|
||||
it('should return all set members', async () => {
|
||||
const members = ['user1', 'user2', 'user3'];
|
||||
const testData = new Map([
|
||||
['users', { value: JSON.stringify(members) }]
|
||||
]);
|
||||
(service as any).data = testData;
|
||||
|
||||
const result = await service.smembers('users');
|
||||
|
||||
expect(result).toEqual(members);
|
||||
});
|
||||
|
||||
it('should return empty array for non-existent set', async () => {
|
||||
(service as any).data = new Map();
|
||||
|
||||
const result = await service.smembers('nonExistentSet');
|
||||
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it('should return empty array and remove expired set', async () => {
|
||||
jest.useFakeTimers();
|
||||
const now = Date.now();
|
||||
jest.setSystemTime(now);
|
||||
|
||||
const testData = new Map([
|
||||
['expiredSet', { value: JSON.stringify(['user1']), expireAt: now - 1000 }]
|
||||
]);
|
||||
(service as any).data = testData;
|
||||
mockFs.writeFile.mockResolvedValue(undefined);
|
||||
|
||||
const result = await service.smembers('expiredSet');
|
||||
|
||||
expect(result).toEqual([]);
|
||||
expect(testData.has('expiredSet')).toBe(false);
|
||||
|
||||
jest.useRealTimers();
|
||||
});
|
||||
});
|
||||
|
||||
describe('过期清理机制', () => {
|
||||
it('should start expiration cleanup on initialization', () => {
|
||||
jest.useFakeTimers();
|
||||
|
||||
// 创建新的服务实例来测试定时器
|
||||
const newService = new FileRedisService();
|
||||
|
||||
// 验证定时器被设置 - 检查是否有setInterval调用
|
||||
expect(mockSetInterval).toHaveBeenCalled();
|
||||
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
it('should clean expired keys during cleanup', async () => {
|
||||
jest.useFakeTimers();
|
||||
const now = Date.now();
|
||||
jest.setSystemTime(now);
|
||||
|
||||
const testData = new Map([
|
||||
['validKey', { value: 'validValue', expireAt: now + 3600 * 1000 }],
|
||||
['expiredKey1', { value: 'expiredValue1', expireAt: now - 1000 }],
|
||||
['expiredKey2', { value: 'expiredValue2', expireAt: now - 2000 }],
|
||||
['permanentKey', { value: 'permanentValue' }]
|
||||
]);
|
||||
(service as any).data = testData;
|
||||
mockFs.writeFile.mockResolvedValue(undefined);
|
||||
|
||||
// 手动调用清理方法
|
||||
(service as any).cleanExpiredKeys();
|
||||
|
||||
expect(testData.has('validKey')).toBe(true);
|
||||
expect(testData.has('permanentKey')).toBe(true);
|
||||
expect(testData.has('expiredKey1')).toBe(false);
|
||||
expect(testData.has('expiredKey2')).toBe(false);
|
||||
|
||||
jest.useRealTimers();
|
||||
});
|
||||
});
|
||||
|
||||
describe('边界情况测试', () => {
|
||||
beforeEach(() => {
|
||||
mockFs.writeFile.mockResolvedValue(undefined);
|
||||
});
|
||||
|
||||
it('should handle empty string key', async () => {
|
||||
await service.set('', 'value');
|
||||
|
||||
const testData = (service as any).data;
|
||||
expect(testData.has('')).toBe(true);
|
||||
});
|
||||
|
||||
it('should handle empty string value', async () => {
|
||||
await service.set('key', '');
|
||||
|
||||
const testData = (service as any).data;
|
||||
expect(testData.get('key').value).toBe('');
|
||||
});
|
||||
|
||||
it('should handle very large TTL values', async () => {
|
||||
jest.useFakeTimers();
|
||||
const now = Date.now();
|
||||
jest.setSystemTime(now);
|
||||
|
||||
await service.set('key', 'value', 2147483647); // Max 32-bit integer
|
||||
|
||||
const testData = (service as any).data;
|
||||
expect(testData.get('key').expireAt).toBe(now + 2147483647 * 1000);
|
||||
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
it('should handle negative TTL values', async () => {
|
||||
await service.set('key', 'value', -1);
|
||||
|
||||
const testData = (service as any).data;
|
||||
expect(testData.get('key').expireAt).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should handle JSON parsing errors during data loading', async () => {
|
||||
mockFs.readFile.mockResolvedValue('invalid json');
|
||||
|
||||
// 创建新的服务实例来测试数据加载
|
||||
const newService = new FileRedisService();
|
||||
await new Promise(resolve => setTimeout(resolve, 10));
|
||||
|
||||
// 应该初始化为空数据而不是抛出异常
|
||||
expect((newService as any).data.size).toBe(0);
|
||||
});
|
||||
|
||||
it('should handle file read errors during data loading', async () => {
|
||||
mockFs.readFile.mockRejectedValue(new Error('File not found'));
|
||||
|
||||
// 创建新的服务实例来测试数据加载
|
||||
const newService = new FileRedisService();
|
||||
await new Promise(resolve => setTimeout(resolve, 10));
|
||||
|
||||
// 应该初始化为空数据而不是抛出异常
|
||||
expect((newService as any).data.size).toBe(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
689
src/core/redis/file_redis.service.ts
Normal file
689
src/core/redis/file_redis.service.ts
Normal file
@@ -0,0 +1,689 @@
|
||||
/**
|
||||
* 文件模拟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已清理');
|
||||
}
|
||||
}
|
||||
@@ -1,177 +0,0 @@
|
||||
import { Injectable, Logger, OnModuleDestroy } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import Redis from 'ioredis';
|
||||
import { IRedisService } from './redis.interface';
|
||||
|
||||
/**
|
||||
* 真实Redis服务
|
||||
* 连接到真实的Redis服务器
|
||||
*/
|
||||
@Injectable()
|
||||
export class RealRedisService implements IRedisService, OnModuleDestroy {
|
||||
private readonly logger = new Logger(RealRedisService.name);
|
||||
private redis: Redis;
|
||||
|
||||
constructor(private configService: ConfigService) {
|
||||
this.initializeRedis();
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化Redis连接
|
||||
*/
|
||||
private initializeRedis(): void {
|
||||
const redisConfig = {
|
||||
host: this.configService.get<string>('REDIS_HOST', 'localhost'),
|
||||
port: this.configService.get<number>('REDIS_PORT', 6379),
|
||||
password: this.configService.get<string>('REDIS_PASSWORD') || undefined,
|
||||
db: this.configService.get<number>('REDIS_DB', 0),
|
||||
retryDelayOnFailover: 100,
|
||||
maxRetriesPerRequest: 3,
|
||||
lazyConnect: true,
|
||||
};
|
||||
|
||||
this.redis = new Redis(redisConfig);
|
||||
|
||||
this.redis.on('connect', () => {
|
||||
this.logger.log('Redis连接成功');
|
||||
});
|
||||
|
||||
this.redis.on('error', (error) => {
|
||||
this.logger.error('Redis连接错误', error);
|
||||
});
|
||||
|
||||
this.redis.on('close', () => {
|
||||
this.logger.warn('Redis连接关闭');
|
||||
});
|
||||
}
|
||||
|
||||
async set(key: string, value: string, ttl?: number): Promise<void> {
|
||||
try {
|
||||
if (ttl && ttl > 0) {
|
||||
await this.redis.setex(key, ttl, value);
|
||||
} else {
|
||||
await this.redis.set(key, value);
|
||||
}
|
||||
this.logger.debug(`设置Redis键: ${key}, TTL: ${ttl || '永不过期'}`);
|
||||
} catch (error) {
|
||||
this.logger.error(`设置Redis键失败: ${key}`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async get(key: string): Promise<string | null> {
|
||||
try {
|
||||
return await this.redis.get(key);
|
||||
} catch (error) {
|
||||
this.logger.error(`获取Redis键失败: ${key}`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async del(key: string): Promise<boolean> {
|
||||
try {
|
||||
const result = await this.redis.del(key);
|
||||
this.logger.debug(`删除Redis键: ${key}, 结果: ${result > 0}`);
|
||||
return result > 0;
|
||||
} catch (error) {
|
||||
this.logger.error(`删除Redis键失败: ${key}`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async exists(key: string): Promise<boolean> {
|
||||
try {
|
||||
const result = await this.redis.exists(key);
|
||||
return result > 0;
|
||||
} catch (error) {
|
||||
this.logger.error(`检查Redis键存在性失败: ${key}`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async expire(key: string, ttl: number): Promise<void> {
|
||||
try {
|
||||
await this.redis.expire(key, ttl);
|
||||
this.logger.debug(`设置Redis键过期时间: ${key}, TTL: ${ttl}秒`);
|
||||
} catch (error) {
|
||||
this.logger.error(`设置Redis键过期时间失败: ${key}`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async ttl(key: string): Promise<number> {
|
||||
try {
|
||||
return await this.redis.ttl(key);
|
||||
} catch (error) {
|
||||
this.logger.error(`获取Redis键TTL失败: ${key}`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async flushall(): Promise<void> {
|
||||
try {
|
||||
await this.redis.flushall();
|
||||
this.logger.log('清空所有Redis数据');
|
||||
} catch (error) {
|
||||
this.logger.error('清空Redis数据失败', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async setex(key: string, ttl: number, value: string): Promise<void> {
|
||||
try {
|
||||
await this.redis.setex(key, ttl, value);
|
||||
this.logger.debug(`设置Redis键(setex): ${key}, TTL: ${ttl}秒`);
|
||||
} catch (error) {
|
||||
this.logger.error(`设置Redis键失败(setex): ${key}`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async incr(key: string): Promise<number> {
|
||||
try {
|
||||
const result = await this.redis.incr(key);
|
||||
this.logger.debug(`自增Redis键: ${key}, 新值: ${result}`);
|
||||
return result;
|
||||
} catch (error) {
|
||||
this.logger.error(`自增Redis键失败: ${key}`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async sadd(key: string, member: string): Promise<void> {
|
||||
try {
|
||||
await this.redis.sadd(key, member);
|
||||
this.logger.debug(`添加集合成员: ${key} -> ${member}`);
|
||||
} catch (error) {
|
||||
this.logger.error(`添加集合成员失败: ${key}`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async srem(key: string, member: string): Promise<void> {
|
||||
try {
|
||||
await this.redis.srem(key, member);
|
||||
this.logger.debug(`移除集合成员: ${key} -> ${member}`);
|
||||
} catch (error) {
|
||||
this.logger.error(`移除集合成员失败: ${key}`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async smembers(key: string): Promise<string[]> {
|
||||
try {
|
||||
return await this.redis.smembers(key);
|
||||
} catch (error) {
|
||||
this.logger.error(`获取集合成员失败: ${key}`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
onModuleDestroy(): void {
|
||||
if (this.redis) {
|
||||
this.redis.disconnect();
|
||||
this.logger.log('Redis连接已断开');
|
||||
}
|
||||
}
|
||||
}
|
||||
553
src/core/redis/real_redis.integration.spec.ts
Normal file
553
src/core/redis/real_redis.integration.spec.ts
Normal file
@@ -0,0 +1,553 @@
|
||||
/**
|
||||
* RealRedisService集成测试
|
||||
*
|
||||
* 功能描述:
|
||||
* - 使用真实Redis连接进行集成测试
|
||||
* - 测试Redis服务器连接和断开
|
||||
* - 验证数据持久性和一致性
|
||||
* - 测试Redis服务的完整工作流程
|
||||
*
|
||||
* 职责分离:
|
||||
* - 集成测试:测试与真实Redis服务器的交互
|
||||
* - 连接测试:验证Redis连接管理
|
||||
* - 数据一致性:测试数据的持久化和读取
|
||||
* - 性能测试:验证Redis操作的性能表现
|
||||
*
|
||||
* 最近修改:
|
||||
* - 2025-01-07: 功能新增 - 创建RealRedisService完整集成测试,验证真实Redis交互
|
||||
*
|
||||
* @author moyin
|
||||
* @version 1.0.0
|
||||
* @since 2025-01-07
|
||||
* @lastModified 2025-01-07
|
||||
*/
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { RealRedisService } from './real_redis.service';
|
||||
import Redis from 'ioredis';
|
||||
|
||||
describe('RealRedisService Integration', () => {
|
||||
let service: RealRedisService;
|
||||
let module: TestingModule;
|
||||
let configService: ConfigService;
|
||||
|
||||
// 测试配置 - 使用测试Redis实例
|
||||
const testRedisConfig = {
|
||||
REDIS_HOST: process.env.TEST_REDIS_HOST || 'localhost',
|
||||
REDIS_PORT: parseInt(process.env.TEST_REDIS_PORT || '6379'),
|
||||
REDIS_PASSWORD: process.env.TEST_REDIS_PASSWORD,
|
||||
REDIS_DB: parseInt(process.env.TEST_REDIS_DB || '15'), // 使用DB 15进行测试
|
||||
};
|
||||
|
||||
beforeAll(async () => {
|
||||
// 检查是否有可用的Redis服务器
|
||||
const testRedis = new Redis({
|
||||
host: testRedisConfig.REDIS_HOST,
|
||||
port: testRedisConfig.REDIS_PORT,
|
||||
password: testRedisConfig.REDIS_PASSWORD,
|
||||
db: testRedisConfig.REDIS_DB,
|
||||
lazyConnect: true,
|
||||
maxRetriesPerRequest: 1,
|
||||
});
|
||||
|
||||
try {
|
||||
await testRedis.ping();
|
||||
// 确保连接被正确断开
|
||||
testRedis.disconnect(false);
|
||||
} catch (error) {
|
||||
console.warn('Redis服务器不可用,跳过集成测试:', (error as Error).message);
|
||||
// 确保连接被正确断开
|
||||
testRedis.disconnect(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// 创建测试模块
|
||||
module = await Test.createTestingModule({
|
||||
providers: [
|
||||
RealRedisService,
|
||||
{
|
||||
provide: ConfigService,
|
||||
useValue: {
|
||||
get: jest.fn((key: string, defaultValue?: any) => {
|
||||
return testRedisConfig[key] || defaultValue;
|
||||
}),
|
||||
},
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
service = module.get<RealRedisService>(RealRedisService);
|
||||
configService = module.get<ConfigService>(ConfigService);
|
||||
|
||||
// 清空测试数据库
|
||||
try {
|
||||
await service.flushall();
|
||||
} catch (error) {
|
||||
// 忽略清空数据时的错误
|
||||
}
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
if (service) {
|
||||
try {
|
||||
// 清空测试数据
|
||||
await service.flushall();
|
||||
} catch (error) {
|
||||
// 忽略清空数据时的错误
|
||||
}
|
||||
|
||||
try {
|
||||
// 断开连接
|
||||
service.onModuleDestroy();
|
||||
} catch (error) {
|
||||
// 忽略断开连接时的错误
|
||||
}
|
||||
}
|
||||
if (module) {
|
||||
await module.close();
|
||||
}
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
// 每个测试前清空数据
|
||||
if (service) {
|
||||
try {
|
||||
await service.flushall();
|
||||
} catch (error) {
|
||||
// 忽略清空数据时的错误
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 检查Redis是否可用的辅助函数
|
||||
const skipIfRedisUnavailable = () => {
|
||||
if (!service) {
|
||||
return true; // 返回true表示应该跳过测试
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
describe('Redis连接管理', () => {
|
||||
it('should connect to Redis server successfully', () => {
|
||||
if (skipIfRedisUnavailable()) {
|
||||
console.log('跳过测试:Redis服务器不可用');
|
||||
return;
|
||||
}
|
||||
expect(service).toBeDefined();
|
||||
});
|
||||
|
||||
it('should use correct Redis configuration', () => {
|
||||
if (skipIfRedisUnavailable()) {
|
||||
console.log('跳过测试:Redis服务器不可用');
|
||||
return;
|
||||
}
|
||||
expect(configService.get).toHaveBeenCalledWith('REDIS_HOST', 'localhost');
|
||||
expect(configService.get).toHaveBeenCalledWith('REDIS_PORT', 6379);
|
||||
expect(configService.get).toHaveBeenCalledWith('REDIS_DB', 0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('基础键值操作', () => {
|
||||
it('should set and get string values', async () => {
|
||||
if (skipIfRedisUnavailable()) {
|
||||
console.log('跳过测试:Redis服务器不可用');
|
||||
return;
|
||||
}
|
||||
|
||||
await service.set('test:string', 'Hello Redis');
|
||||
const result = await service.get('test:string');
|
||||
|
||||
expect(result).toBe('Hello Redis');
|
||||
});
|
||||
|
||||
it('should handle non-existent keys', async () => {
|
||||
if (skipIfRedisUnavailable()) {
|
||||
console.log('跳过测试:Redis服务器不可用');
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await service.get('test:nonexistent');
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it('should delete existing keys', async () => {
|
||||
if (skipIfRedisUnavailable()) {
|
||||
console.log('跳过测试:Redis服务器不可用');
|
||||
return;
|
||||
}
|
||||
|
||||
await service.set('test:delete', 'to be deleted');
|
||||
const deleted = await service.del('test:delete');
|
||||
const result = await service.get('test:delete');
|
||||
|
||||
expect(deleted).toBe(true);
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it('should return false when deleting non-existent keys', async () => {
|
||||
if (skipIfRedisUnavailable()) {
|
||||
console.log('跳过测试:Redis服务器不可用');
|
||||
return;
|
||||
}
|
||||
|
||||
const deleted = await service.del('test:nonexistent');
|
||||
|
||||
expect(deleted).toBe(false);
|
||||
});
|
||||
|
||||
it('should check key existence', async () => {
|
||||
if (skipIfRedisUnavailable()) {
|
||||
console.log('跳过测试:Redis服务器不可用');
|
||||
return;
|
||||
}
|
||||
|
||||
await service.set('test:exists', 'exists');
|
||||
const exists = await service.exists('test:exists');
|
||||
const notExists = await service.exists('test:notexists');
|
||||
|
||||
expect(exists).toBe(true);
|
||||
expect(notExists).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('过期时间管理', () => {
|
||||
it('should set keys with TTL', async () => {
|
||||
if (skipIfRedisUnavailable()) {
|
||||
console.log('跳过测试:Redis服务器不可用');
|
||||
return;
|
||||
}
|
||||
|
||||
await service.set('test:ttl', 'expires soon', 2);
|
||||
const ttl = await service.ttl('test:ttl');
|
||||
|
||||
expect(ttl).toBeGreaterThan(0);
|
||||
expect(ttl).toBeLessThanOrEqual(2);
|
||||
});
|
||||
|
||||
it('should expire keys after TTL', async () => {
|
||||
if (skipIfRedisUnavailable()) {
|
||||
console.log('跳过测试:Redis服务器不可用');
|
||||
return;
|
||||
}
|
||||
|
||||
await service.set('test:expire', 'will expire', 1);
|
||||
|
||||
// 等待过期
|
||||
await new Promise(resolve => setTimeout(resolve, 1100));
|
||||
|
||||
const result = await service.get('test:expire');
|
||||
expect(result).toBeNull();
|
||||
}, 2000);
|
||||
|
||||
it('should set expiration on existing keys', async () => {
|
||||
if (skipIfRedisUnavailable()) {
|
||||
console.log('跳过测试:Redis服务器不可用');
|
||||
return;
|
||||
}
|
||||
|
||||
await service.set('test:expire_later', 'set expiration later');
|
||||
await service.expire('test:expire_later', 2);
|
||||
|
||||
const ttl = await service.ttl('test:expire_later');
|
||||
expect(ttl).toBeGreaterThan(0);
|
||||
expect(ttl).toBeLessThanOrEqual(2);
|
||||
});
|
||||
|
||||
it('should return -1 for keys without expiration', async () => {
|
||||
if (skipIfRedisUnavailable()) {
|
||||
console.log('跳过测试:Redis服务器不可用');
|
||||
return;
|
||||
}
|
||||
|
||||
await service.set('test:no_expire', 'never expires');
|
||||
const ttl = await service.ttl('test:no_expire');
|
||||
|
||||
expect(ttl).toBe(-1);
|
||||
});
|
||||
|
||||
it('should return -2 for non-existent keys', async () => {
|
||||
if (skipIfRedisUnavailable()) {
|
||||
console.log('跳过测试:Redis服务器不可用');
|
||||
return;
|
||||
}
|
||||
|
||||
const ttl = await service.ttl('test:nonexistent');
|
||||
|
||||
expect(ttl).toBe(-2);
|
||||
});
|
||||
|
||||
it('should use setex for atomic set with expiration', async () => {
|
||||
if (skipIfRedisUnavailable()) {
|
||||
console.log('跳过测试:Redis服务器不可用');
|
||||
return;
|
||||
}
|
||||
|
||||
await service.setex('test:setex', 2, 'atomic set with expiration');
|
||||
const value = await service.get('test:setex');
|
||||
const ttl = await service.ttl('test:setex');
|
||||
|
||||
expect(value).toBe('atomic set with expiration');
|
||||
expect(ttl).toBeGreaterThan(0);
|
||||
expect(ttl).toBeLessThanOrEqual(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('数值操作', () => {
|
||||
it('should increment numeric values', async () => {
|
||||
if (skipIfRedisUnavailable()) {
|
||||
console.log('跳过测试:Redis服务器不可用');
|
||||
return;
|
||||
}
|
||||
|
||||
const result1 = await service.incr('test:counter');
|
||||
const result2 = await service.incr('test:counter');
|
||||
const result3 = await service.incr('test:counter');
|
||||
|
||||
expect(result1).toBe(1);
|
||||
expect(result2).toBe(2);
|
||||
expect(result3).toBe(3);
|
||||
});
|
||||
|
||||
it('should increment existing numeric values', async () => {
|
||||
if (skipIfRedisUnavailable()) {
|
||||
console.log('跳过测试:Redis服务器不可用');
|
||||
return;
|
||||
}
|
||||
|
||||
await service.set('test:existing_counter', '10');
|
||||
const result = await service.incr('test:existing_counter');
|
||||
|
||||
expect(result).toBe(11);
|
||||
});
|
||||
});
|
||||
|
||||
describe('集合操作', () => {
|
||||
it('should add and retrieve set members', async () => {
|
||||
if (skipIfRedisUnavailable()) {
|
||||
console.log('跳过测试:Redis服务器不可用');
|
||||
return;
|
||||
}
|
||||
|
||||
await service.sadd('test:set', 'member1');
|
||||
await service.sadd('test:set', 'member2');
|
||||
await service.sadd('test:set', 'member3');
|
||||
|
||||
const members = await service.smembers('test:set');
|
||||
|
||||
expect(members).toHaveLength(3);
|
||||
expect(members).toContain('member1');
|
||||
expect(members).toContain('member2');
|
||||
expect(members).toContain('member3');
|
||||
});
|
||||
|
||||
it('should remove set members', async () => {
|
||||
if (skipIfRedisUnavailable()) {
|
||||
console.log('跳过测试:Redis服务器不可用');
|
||||
return;
|
||||
}
|
||||
|
||||
await service.sadd('test:set_remove', 'member1');
|
||||
await service.sadd('test:set_remove', 'member2');
|
||||
await service.sadd('test:set_remove', 'member3');
|
||||
|
||||
await service.srem('test:set_remove', 'member2');
|
||||
|
||||
const members = await service.smembers('test:set_remove');
|
||||
|
||||
expect(members).toHaveLength(2);
|
||||
expect(members).toContain('member1');
|
||||
expect(members).toContain('member3');
|
||||
expect(members).not.toContain('member2');
|
||||
});
|
||||
|
||||
it('should return empty array for non-existent sets', async () => {
|
||||
if (skipIfRedisUnavailable()) {
|
||||
console.log('跳过测试:Redis服务器不可用');
|
||||
return;
|
||||
}
|
||||
|
||||
const members = await service.smembers('test:nonexistent_set');
|
||||
|
||||
expect(members).toEqual([]);
|
||||
});
|
||||
|
||||
it('should handle duplicate set members', async () => {
|
||||
if (skipIfRedisUnavailable()) {
|
||||
console.log('跳过测试:Redis服务器不可用');
|
||||
return;
|
||||
}
|
||||
|
||||
await service.sadd('test:duplicate_set', 'member1');
|
||||
await service.sadd('test:duplicate_set', 'member1'); // 重复添加
|
||||
await service.sadd('test:duplicate_set', 'member2');
|
||||
|
||||
const members = await service.smembers('test:duplicate_set');
|
||||
|
||||
expect(members).toHaveLength(2);
|
||||
expect(members).toContain('member1');
|
||||
expect(members).toContain('member2');
|
||||
});
|
||||
});
|
||||
|
||||
describe('数据持久性和一致性', () => {
|
||||
it('should persist data across operations', async () => {
|
||||
if (skipIfRedisUnavailable()) {
|
||||
console.log('跳过测试:Redis服务器不可用');
|
||||
return;
|
||||
}
|
||||
|
||||
// 设置多种类型的数据
|
||||
await service.set('test:persist:string', 'persistent string');
|
||||
await service.set('test:persist:number', '42');
|
||||
await service.sadd('test:persist:set', 'set_member');
|
||||
await service.incr('test:persist:counter');
|
||||
|
||||
// 验证数据持久性
|
||||
const stringValue = await service.get('test:persist:string');
|
||||
const numberValue = await service.get('test:persist:number');
|
||||
const setMembers = await service.smembers('test:persist:set');
|
||||
const counterValue = await service.get('test:persist:counter');
|
||||
|
||||
expect(stringValue).toBe('persistent string');
|
||||
expect(numberValue).toBe('42');
|
||||
expect(setMembers).toContain('set_member');
|
||||
expect(counterValue).toBe('1');
|
||||
});
|
||||
|
||||
it('should maintain data consistency during concurrent operations', async () => {
|
||||
if (skipIfRedisUnavailable()) {
|
||||
console.log('跳过测试:Redis服务器不可用');
|
||||
return;
|
||||
}
|
||||
|
||||
// 并发执行多个操作
|
||||
const promises = [];
|
||||
for (let i = 0; i < 10; i++) {
|
||||
promises.push(service.incr('test:concurrent:counter'));
|
||||
promises.push(service.sadd('test:concurrent:set', `member${i}`));
|
||||
}
|
||||
|
||||
await Promise.all(promises);
|
||||
|
||||
// 验证结果一致性
|
||||
const counterValue = await service.get('test:concurrent:counter');
|
||||
const setMembers = await service.smembers('test:concurrent:set');
|
||||
|
||||
expect(parseInt(counterValue)).toBe(10);
|
||||
expect(setMembers).toHaveLength(10);
|
||||
});
|
||||
});
|
||||
|
||||
describe('清空操作', () => {
|
||||
it('should clear all data with flushall', async () => {
|
||||
if (skipIfRedisUnavailable()) {
|
||||
console.log('跳过测试:Redis服务器不可用');
|
||||
return;
|
||||
}
|
||||
|
||||
// 设置一些测试数据
|
||||
await service.set('test:flush1', 'value1');
|
||||
await service.set('test:flush2', 'value2');
|
||||
await service.sadd('test:flush_set', 'member');
|
||||
|
||||
// 清空所有数据
|
||||
await service.flushall();
|
||||
|
||||
// 验证数据已清空
|
||||
const value1 = await service.get('test:flush1');
|
||||
const value2 = await service.get('test:flush2');
|
||||
const setMembers = await service.smembers('test:flush_set');
|
||||
|
||||
expect(value1).toBeNull();
|
||||
expect(value2).toBeNull();
|
||||
expect(setMembers).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('错误处理和边界情况', () => {
|
||||
it('should handle empty string keys and values', async () => {
|
||||
if (skipIfRedisUnavailable()) {
|
||||
console.log('跳过测试:Redis服务器不可用');
|
||||
return;
|
||||
}
|
||||
|
||||
await service.set('', 'empty key');
|
||||
await service.set('empty_value', '');
|
||||
|
||||
const emptyKeyValue = await service.get('');
|
||||
const emptyValue = await service.get('empty_value');
|
||||
|
||||
expect(emptyKeyValue).toBe('empty key');
|
||||
expect(emptyValue).toBe('');
|
||||
});
|
||||
|
||||
it('should handle very long keys and values', async () => {
|
||||
if (skipIfRedisUnavailable()) {
|
||||
console.log('跳过测试:Redis服务器不可用');
|
||||
return;
|
||||
}
|
||||
|
||||
const longKey = 'test:' + 'a'.repeat(1000);
|
||||
const longValue = 'b'.repeat(10000);
|
||||
|
||||
await service.set(longKey, longValue);
|
||||
const result = await service.get(longKey);
|
||||
|
||||
expect(result).toBe(longValue);
|
||||
});
|
||||
|
||||
it('should handle special characters in keys and values', async () => {
|
||||
if (skipIfRedisUnavailable()) {
|
||||
console.log('跳过测试:Redis服务器不可用');
|
||||
return;
|
||||
}
|
||||
|
||||
const specialKey = 'test:特殊字符:🚀:key';
|
||||
const specialValue = 'Special value with 特殊字符 and 🎉 emojis';
|
||||
|
||||
await service.set(specialKey, specialValue);
|
||||
const result = await service.get(specialKey);
|
||||
|
||||
expect(result).toBe(specialValue);
|
||||
});
|
||||
});
|
||||
|
||||
describe('性能测试', () => {
|
||||
it('should handle multiple operations efficiently', async () => {
|
||||
if (skipIfRedisUnavailable()) {
|
||||
console.log('跳过测试:Redis服务器不可用');
|
||||
return;
|
||||
}
|
||||
|
||||
const startTime = Date.now();
|
||||
const operations = 100;
|
||||
|
||||
// 执行大量操作
|
||||
const promises = [];
|
||||
for (let i = 0; i < operations; i++) {
|
||||
promises.push(service.set(`test:perf:${i}`, `value${i}`));
|
||||
}
|
||||
await Promise.all(promises);
|
||||
|
||||
// 读取所有数据
|
||||
const readPromises = [];
|
||||
for (let i = 0; i < operations; i++) {
|
||||
readPromises.push(service.get(`test:perf:${i}`));
|
||||
}
|
||||
const results = await Promise.all(readPromises);
|
||||
|
||||
const endTime = Date.now();
|
||||
const duration = endTime - startTime;
|
||||
|
||||
// 验证结果正确性
|
||||
expect(results).toHaveLength(operations);
|
||||
results.forEach((result, index) => {
|
||||
expect(result).toBe(`value${index}`);
|
||||
});
|
||||
|
||||
// 性能检查(应该在合理时间内完成)
|
||||
expect(duration).toBeLessThan(5000); // 5秒内完成
|
||||
}, 10000);
|
||||
});
|
||||
});
|
||||
453
src/core/redis/real_redis.service.spec.ts
Normal file
453
src/core/redis/real_redis.service.spec.ts
Normal file
@@ -0,0 +1,453 @@
|
||||
/**
|
||||
* RealRedisService单元测试
|
||||
*
|
||||
* 功能描述:
|
||||
* - 测试真实Redis服务的所有公共方法
|
||||
* - 验证Redis连接管理和错误处理
|
||||
* - 测试正常情况、异常情况和边界情况
|
||||
* - 使用Mock Redis客户端进行隔离测试
|
||||
*
|
||||
* 职责分离:
|
||||
* - 单元测试:隔离测试每个方法的功能
|
||||
* - Mock测试:使用模拟Redis客户端避免真实连接
|
||||
* - 异常测试:验证错误处理机制
|
||||
* - 边界测试:测试参数边界和特殊情况
|
||||
*
|
||||
* 最近修改:
|
||||
* - 2025-01-07: 功能新增 - 创建RealRedisService完整单元测试,覆盖所有公共方法
|
||||
*
|
||||
* @author moyin
|
||||
* @version 1.0.0
|
||||
* @since 2025-01-07
|
||||
* @lastModified 2025-01-07
|
||||
*/
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { Logger } from '@nestjs/common';
|
||||
import { RealRedisService } from './real_redis.service';
|
||||
import Redis from 'ioredis';
|
||||
|
||||
// Mock ioredis
|
||||
jest.mock('ioredis');
|
||||
|
||||
describe('RealRedisService', () => {
|
||||
let service: RealRedisService;
|
||||
let mockRedis: jest.Mocked<Redis>;
|
||||
let mockConfigService: jest.Mocked<ConfigService>;
|
||||
|
||||
beforeEach(async () => {
|
||||
// 创建Mock Redis实例
|
||||
mockRedis = {
|
||||
set: jest.fn(),
|
||||
get: jest.fn(),
|
||||
del: jest.fn(),
|
||||
exists: jest.fn(),
|
||||
expire: jest.fn(),
|
||||
ttl: jest.fn(),
|
||||
flushall: jest.fn(),
|
||||
setex: jest.fn(),
|
||||
incr: jest.fn(),
|
||||
sadd: jest.fn(),
|
||||
srem: jest.fn(),
|
||||
smembers: jest.fn(),
|
||||
disconnect: jest.fn(),
|
||||
on: jest.fn(),
|
||||
} as any;
|
||||
|
||||
// Mock Redis构造函数
|
||||
(Redis as jest.MockedClass<typeof Redis>).mockImplementation(() => mockRedis);
|
||||
|
||||
// 创建Mock ConfigService
|
||||
mockConfigService = {
|
||||
get: jest.fn(),
|
||||
} as any;
|
||||
|
||||
mockConfigService.get.mockImplementation((key: string, defaultValue?: any) => {
|
||||
const config = {
|
||||
'REDIS_HOST': 'localhost',
|
||||
'REDIS_PORT': 6379,
|
||||
'REDIS_PASSWORD': undefined,
|
||||
'REDIS_DB': 0,
|
||||
};
|
||||
return config[key] || defaultValue;
|
||||
});
|
||||
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
RealRedisService,
|
||||
{ provide: ConfigService, useValue: mockConfigService },
|
||||
],
|
||||
}).compile();
|
||||
|
||||
service = module.get<RealRedisService>(RealRedisService);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('构造函数和初始化', () => {
|
||||
it('should create service successfully', () => {
|
||||
expect(service).toBeDefined();
|
||||
});
|
||||
|
||||
it('should initialize Redis with correct config', () => {
|
||||
expect(Redis).toHaveBeenCalledWith({
|
||||
host: 'localhost',
|
||||
port: 6379,
|
||||
password: undefined,
|
||||
db: 0,
|
||||
retryDelayOnFailover: 100,
|
||||
maxRetriesPerRequest: 3,
|
||||
lazyConnect: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('should setup Redis event listeners', () => {
|
||||
expect(mockRedis.on).toHaveBeenCalledWith('connect', expect.any(Function));
|
||||
expect(mockRedis.on).toHaveBeenCalledWith('error', expect.any(Function));
|
||||
expect(mockRedis.on).toHaveBeenCalledWith('close', expect.any(Function));
|
||||
});
|
||||
});
|
||||
|
||||
describe('set', () => {
|
||||
it('should set key-value without TTL', async () => {
|
||||
mockRedis.set.mockResolvedValue('OK');
|
||||
|
||||
await service.set('testKey', 'testValue');
|
||||
|
||||
expect(mockRedis.set).toHaveBeenCalledWith('testKey', 'testValue');
|
||||
expect(mockRedis.setex).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should set key-value with TTL', async () => {
|
||||
mockRedis.setex.mockResolvedValue('OK');
|
||||
|
||||
await service.set('testKey', 'testValue', 3600);
|
||||
|
||||
expect(mockRedis.setex).toHaveBeenCalledWith('testKey', 3600, 'testValue');
|
||||
expect(mockRedis.set).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not set TTL when TTL is 0', async () => {
|
||||
mockRedis.set.mockResolvedValue('OK');
|
||||
|
||||
await service.set('testKey', 'testValue', 0);
|
||||
|
||||
expect(mockRedis.set).toHaveBeenCalledWith('testKey', 'testValue');
|
||||
expect(mockRedis.setex).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should throw error when Redis operation fails', async () => {
|
||||
const error = new Error('Redis connection failed');
|
||||
mockRedis.set.mockRejectedValue(error);
|
||||
|
||||
await expect(service.set('testKey', 'testValue')).rejects.toThrow(error);
|
||||
});
|
||||
});
|
||||
|
||||
describe('get', () => {
|
||||
it('should return value when key exists', async () => {
|
||||
mockRedis.get.mockResolvedValue('testValue');
|
||||
|
||||
const result = await service.get('testKey');
|
||||
|
||||
expect(result).toBe('testValue');
|
||||
expect(mockRedis.get).toHaveBeenCalledWith('testKey');
|
||||
});
|
||||
|
||||
it('should return null when key does not exist', async () => {
|
||||
mockRedis.get.mockResolvedValue(null);
|
||||
|
||||
const result = await service.get('nonExistentKey');
|
||||
|
||||
expect(result).toBeNull();
|
||||
expect(mockRedis.get).toHaveBeenCalledWith('nonExistentKey');
|
||||
});
|
||||
|
||||
it('should throw error when Redis operation fails', async () => {
|
||||
const error = new Error('Redis connection failed');
|
||||
mockRedis.get.mockRejectedValue(error);
|
||||
|
||||
await expect(service.get('testKey')).rejects.toThrow(error);
|
||||
});
|
||||
});
|
||||
|
||||
describe('del', () => {
|
||||
it('should return true when key is deleted', async () => {
|
||||
mockRedis.del.mockResolvedValue(1);
|
||||
|
||||
const result = await service.del('testKey');
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(mockRedis.del).toHaveBeenCalledWith('testKey');
|
||||
});
|
||||
|
||||
it('should return false when key does not exist', async () => {
|
||||
mockRedis.del.mockResolvedValue(0);
|
||||
|
||||
const result = await service.del('nonExistentKey');
|
||||
|
||||
expect(result).toBe(false);
|
||||
expect(mockRedis.del).toHaveBeenCalledWith('nonExistentKey');
|
||||
});
|
||||
|
||||
it('should throw error when Redis operation fails', async () => {
|
||||
const error = new Error('Redis connection failed');
|
||||
mockRedis.del.mockRejectedValue(error);
|
||||
|
||||
await expect(service.del('testKey')).rejects.toThrow(error);
|
||||
});
|
||||
});
|
||||
|
||||
describe('exists', () => {
|
||||
it('should return true when key exists', async () => {
|
||||
mockRedis.exists.mockResolvedValue(1);
|
||||
|
||||
const result = await service.exists('testKey');
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(mockRedis.exists).toHaveBeenCalledWith('testKey');
|
||||
});
|
||||
|
||||
it('should return false when key does not exist', async () => {
|
||||
mockRedis.exists.mockResolvedValue(0);
|
||||
|
||||
const result = await service.exists('nonExistentKey');
|
||||
|
||||
expect(result).toBe(false);
|
||||
expect(mockRedis.exists).toHaveBeenCalledWith('nonExistentKey');
|
||||
});
|
||||
|
||||
it('should throw error when Redis operation fails', async () => {
|
||||
const error = new Error('Redis connection failed');
|
||||
mockRedis.exists.mockRejectedValue(error);
|
||||
|
||||
await expect(service.exists('testKey')).rejects.toThrow(error);
|
||||
});
|
||||
});
|
||||
|
||||
describe('expire', () => {
|
||||
it('should set expiration time successfully', async () => {
|
||||
mockRedis.expire.mockResolvedValue(1);
|
||||
|
||||
await service.expire('testKey', 3600);
|
||||
|
||||
expect(mockRedis.expire).toHaveBeenCalledWith('testKey', 3600);
|
||||
});
|
||||
|
||||
it('should throw error when Redis operation fails', async () => {
|
||||
const error = new Error('Redis connection failed');
|
||||
mockRedis.expire.mockRejectedValue(error);
|
||||
|
||||
await expect(service.expire('testKey', 3600)).rejects.toThrow(error);
|
||||
});
|
||||
});
|
||||
|
||||
describe('ttl', () => {
|
||||
it('should return remaining TTL', async () => {
|
||||
mockRedis.ttl.mockResolvedValue(3600);
|
||||
|
||||
const result = await service.ttl('testKey');
|
||||
|
||||
expect(result).toBe(3600);
|
||||
expect(mockRedis.ttl).toHaveBeenCalledWith('testKey');
|
||||
});
|
||||
|
||||
it('should return -1 for keys without expiration', async () => {
|
||||
mockRedis.ttl.mockResolvedValue(-1);
|
||||
|
||||
const result = await service.ttl('testKey');
|
||||
|
||||
expect(result).toBe(-1);
|
||||
});
|
||||
|
||||
it('should return -2 for non-existent keys', async () => {
|
||||
mockRedis.ttl.mockResolvedValue(-2);
|
||||
|
||||
const result = await service.ttl('nonExistentKey');
|
||||
|
||||
expect(result).toBe(-2);
|
||||
});
|
||||
|
||||
it('should throw error when Redis operation fails', async () => {
|
||||
const error = new Error('Redis connection failed');
|
||||
mockRedis.ttl.mockRejectedValue(error);
|
||||
|
||||
await expect(service.ttl('testKey')).rejects.toThrow(error);
|
||||
});
|
||||
});
|
||||
|
||||
describe('flushall', () => {
|
||||
it('should clear all data successfully', async () => {
|
||||
mockRedis.flushall.mockResolvedValue('OK');
|
||||
|
||||
await service.flushall();
|
||||
|
||||
expect(mockRedis.flushall).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should throw error when Redis operation fails', async () => {
|
||||
const error = new Error('Redis connection failed');
|
||||
mockRedis.flushall.mockRejectedValue(error);
|
||||
|
||||
await expect(service.flushall()).rejects.toThrow(error);
|
||||
});
|
||||
});
|
||||
|
||||
describe('setex', () => {
|
||||
it('should set key with expiration time', async () => {
|
||||
mockRedis.setex.mockResolvedValue('OK');
|
||||
|
||||
await service.setex('testKey', 1800, 'testValue');
|
||||
|
||||
expect(mockRedis.setex).toHaveBeenCalledWith('testKey', 1800, 'testValue');
|
||||
});
|
||||
|
||||
it('should throw error when Redis operation fails', async () => {
|
||||
const error = new Error('Redis connection failed');
|
||||
mockRedis.setex.mockRejectedValue(error);
|
||||
|
||||
await expect(service.setex('testKey', 1800, 'testValue')).rejects.toThrow(error);
|
||||
});
|
||||
});
|
||||
|
||||
describe('incr', () => {
|
||||
it('should increment existing numeric value', async () => {
|
||||
mockRedis.incr.mockResolvedValue(6);
|
||||
|
||||
const result = await service.incr('counter');
|
||||
|
||||
expect(result).toBe(6);
|
||||
expect(mockRedis.incr).toHaveBeenCalledWith('counter');
|
||||
});
|
||||
|
||||
it('should initialize non-existent key to 1', async () => {
|
||||
mockRedis.incr.mockResolvedValue(1);
|
||||
|
||||
const result = await service.incr('newCounter');
|
||||
|
||||
expect(result).toBe(1);
|
||||
expect(mockRedis.incr).toHaveBeenCalledWith('newCounter');
|
||||
});
|
||||
|
||||
it('should throw error when Redis operation fails', async () => {
|
||||
const error = new Error('Redis connection failed');
|
||||
mockRedis.incr.mockRejectedValue(error);
|
||||
|
||||
await expect(service.incr('counter')).rejects.toThrow(error);
|
||||
});
|
||||
});
|
||||
|
||||
describe('sadd', () => {
|
||||
it('should add member to set successfully', async () => {
|
||||
mockRedis.sadd.mockResolvedValue(1);
|
||||
|
||||
await service.sadd('users', 'user123');
|
||||
|
||||
expect(mockRedis.sadd).toHaveBeenCalledWith('users', 'user123');
|
||||
});
|
||||
|
||||
it('should throw error when Redis operation fails', async () => {
|
||||
const error = new Error('Redis connection failed');
|
||||
mockRedis.sadd.mockRejectedValue(error);
|
||||
|
||||
await expect(service.sadd('users', 'user123')).rejects.toThrow(error);
|
||||
});
|
||||
});
|
||||
|
||||
describe('srem', () => {
|
||||
it('should remove member from set successfully', async () => {
|
||||
mockRedis.srem.mockResolvedValue(1);
|
||||
|
||||
await service.srem('users', 'user123');
|
||||
|
||||
expect(mockRedis.srem).toHaveBeenCalledWith('users', 'user123');
|
||||
});
|
||||
|
||||
it('should throw error when Redis operation fails', async () => {
|
||||
const error = new Error('Redis connection failed');
|
||||
mockRedis.srem.mockRejectedValue(error);
|
||||
|
||||
await expect(service.srem('users', 'user123')).rejects.toThrow(error);
|
||||
});
|
||||
});
|
||||
|
||||
describe('smembers', () => {
|
||||
it('should return all set members', async () => {
|
||||
const members = ['user1', 'user2', 'user3'];
|
||||
mockRedis.smembers.mockResolvedValue(members);
|
||||
|
||||
const result = await service.smembers('users');
|
||||
|
||||
expect(result).toEqual(members);
|
||||
expect(mockRedis.smembers).toHaveBeenCalledWith('users');
|
||||
});
|
||||
|
||||
it('should return empty array for non-existent set', async () => {
|
||||
mockRedis.smembers.mockResolvedValue([]);
|
||||
|
||||
const result = await service.smembers('nonExistentSet');
|
||||
|
||||
expect(result).toEqual([]);
|
||||
expect(mockRedis.smembers).toHaveBeenCalledWith('nonExistentSet');
|
||||
});
|
||||
|
||||
it('should throw error when Redis operation fails', async () => {
|
||||
const error = new Error('Redis connection failed');
|
||||
mockRedis.smembers.mockRejectedValue(error);
|
||||
|
||||
await expect(service.smembers('users')).rejects.toThrow(error);
|
||||
});
|
||||
});
|
||||
|
||||
describe('onModuleDestroy', () => {
|
||||
it('should disconnect Redis when module is destroyed', () => {
|
||||
service.onModuleDestroy();
|
||||
|
||||
expect(mockRedis.disconnect).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle case when Redis is not initialized', () => {
|
||||
// 创建一个没有Redis实例的服务
|
||||
const serviceWithoutRedis = Object.create(RealRedisService.prototype);
|
||||
|
||||
expect(() => serviceWithoutRedis.onModuleDestroy()).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('边界情况测试', () => {
|
||||
it('should handle empty string key', async () => {
|
||||
mockRedis.set.mockResolvedValue('OK');
|
||||
|
||||
await service.set('', 'value');
|
||||
|
||||
expect(mockRedis.set).toHaveBeenCalledWith('', 'value');
|
||||
});
|
||||
|
||||
it('should handle empty string value', async () => {
|
||||
mockRedis.set.mockResolvedValue('OK');
|
||||
|
||||
await service.set('key', '');
|
||||
|
||||
expect(mockRedis.set).toHaveBeenCalledWith('key', '');
|
||||
});
|
||||
|
||||
it('should handle very large TTL values', async () => {
|
||||
mockRedis.setex.mockResolvedValue('OK');
|
||||
|
||||
await service.set('key', 'value', 2147483647); // Max 32-bit integer
|
||||
|
||||
expect(mockRedis.setex).toHaveBeenCalledWith('key', 2147483647, 'value');
|
||||
});
|
||||
|
||||
it('should handle negative TTL values', async () => {
|
||||
mockRedis.set.mockResolvedValue('OK');
|
||||
|
||||
await service.set('key', 'value', -1);
|
||||
|
||||
expect(mockRedis.set).toHaveBeenCalledWith('key', 'value');
|
||||
expect(mockRedis.setex).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
489
src/core/redis/real_redis.service.ts
Normal file
489
src/core/redis/real_redis.service.ts
Normal file
@@ -0,0 +1,489 @@
|
||||
/**
|
||||
* 真实Redis服务实现
|
||||
*
|
||||
* 功能描述:
|
||||
* - 连接真实的Redis服务器进行数据操作
|
||||
* - 实现完整的Redis基础操作功能
|
||||
* - 提供连接管理和错误处理机制
|
||||
* - 支持自动重连和连接状态监控
|
||||
*
|
||||
* 职责分离:
|
||||
* - 连接管理:负责Redis服务器的连接建立和维护
|
||||
* - 数据操作:实现IRedisService接口的所有方法
|
||||
* - 错误处理:处理网络异常和Redis操作错误
|
||||
* - 日志记录:记录连接状态和操作日志
|
||||
*
|
||||
* 最近修改:
|
||||
* - 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 { ConfigService } from '@nestjs/config';
|
||||
import Redis from 'ioredis';
|
||||
import { IRedisService } from './redis.interface';
|
||||
|
||||
/**
|
||||
* 真实Redis服务
|
||||
*
|
||||
* 职责:
|
||||
* - 连接到真实的Redis服务器
|
||||
* - 实现完整的Redis操作接口
|
||||
* - 管理连接生命周期和错误处理
|
||||
*
|
||||
* 主要方法:
|
||||
* - initializeRedis() - 初始化Redis连接
|
||||
* - set/get/del() - 基础键值操作
|
||||
* - expire/ttl() - 过期时间管理
|
||||
* - sadd/srem/smembers() - 集合操作
|
||||
*
|
||||
* 使用场景:
|
||||
* - 生产环境的Redis数据存储
|
||||
* - 高性能和高并发的数据访问需求
|
||||
*/
|
||||
@Injectable()
|
||||
export class RealRedisService implements IRedisService, OnModuleDestroy {
|
||||
private readonly logger = new Logger(RealRedisService.name);
|
||||
private redis: Redis;
|
||||
|
||||
constructor(private configService: ConfigService) {
|
||||
this.initializeRedis();
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化Redis连接
|
||||
*
|
||||
* 业务逻辑:
|
||||
* 1. 从环境变量读取Redis连接配置
|
||||
* 2. 创建Redis客户端实例并配置连接参数
|
||||
* 3. 设置连接事件监听器
|
||||
* 4. 配置重连策略和错误处理
|
||||
*
|
||||
* @throws Error Redis连接配置错误时
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // 在构造函数中自动调用
|
||||
* constructor(configService: ConfigService) {
|
||||
* this.initializeRedis();
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
private initializeRedis(): void {
|
||||
const redisConfig = {
|
||||
host: this.configService.get<string>('REDIS_HOST', 'localhost'),
|
||||
port: this.configService.get<number>('REDIS_PORT', 6379),
|
||||
password: this.configService.get<string>('REDIS_PASSWORD') || undefined,
|
||||
db: this.configService.get<number>('REDIS_DB', 0),
|
||||
retryDelayOnFailover: 100,
|
||||
maxRetriesPerRequest: 3,
|
||||
lazyConnect: true,
|
||||
};
|
||||
|
||||
this.redis = new Redis(redisConfig);
|
||||
|
||||
this.redis.on('connect', () => {
|
||||
this.logger.log('Redis连接成功');
|
||||
});
|
||||
|
||||
this.redis.on('error', (error) => {
|
||||
this.logger.error('Redis连接错误', error);
|
||||
});
|
||||
|
||||
this.redis.on('close', () => {
|
||||
this.logger.warn('Redis连接关闭');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置键值对
|
||||
*
|
||||
* 业务逻辑:
|
||||
* 1. 验证键和值的有效性
|
||||
* 2. 根据TTL参数决定使用set还是setex命令
|
||||
* 3. 执行Redis设置操作
|
||||
* 4. 记录操作日志和错误处理
|
||||
*
|
||||
* @param key 键名,不能为空
|
||||
* @param value 值,支持字符串类型
|
||||
* @param ttl 可选的过期时间(秒),不设置则永不过期
|
||||
* @returns Promise<void> 操作完成的Promise
|
||||
* @throws Error 当Redis操作失败时
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* await redisService.set('user:123', 'userData', 3600);
|
||||
* ```
|
||||
*/
|
||||
async set(key: string, value: string, ttl?: number): Promise<void> {
|
||||
try {
|
||||
if (ttl && ttl > 0) {
|
||||
await this.redis.setex(key, ttl, value);
|
||||
} else {
|
||||
await this.redis.set(key, value);
|
||||
}
|
||||
this.logger.debug(`设置Redis键: ${key}, TTL: ${ttl || '永不过期'}`);
|
||||
} catch (error) {
|
||||
this.logger.error(`设置Redis键失败: ${key}`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取键对应的值
|
||||
*
|
||||
* 业务逻辑:
|
||||
* 1. 验证键名的有效性
|
||||
* 2. 执行Redis get命令
|
||||
* 3. 返回查询结果
|
||||
* 4. 处理查询异常
|
||||
*
|
||||
* @param key 键名,不能为空
|
||||
* @returns Promise<string | null> 键对应的值,不存在返回null
|
||||
* @throws Error 当Redis操作失败时
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const value = await redisService.get('user:123');
|
||||
* ```
|
||||
*/
|
||||
async get(key: string): Promise<string | null> {
|
||||
try {
|
||||
return await this.redis.get(key);
|
||||
} catch (error) {
|
||||
this.logger.error(`获取Redis键失败: ${key}`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除指定的键
|
||||
*
|
||||
* 业务逻辑:
|
||||
* 1. 验证键名的有效性
|
||||
* 2. 执行Redis del命令删除键
|
||||
* 3. 检查删除操作的结果
|
||||
* 4. 记录删除操作日志
|
||||
* 5. 返回删除是否成功
|
||||
*
|
||||
* @param key 键名,不能为空
|
||||
* @returns Promise<boolean> 删除成功返回true,键不存在返回false
|
||||
* @throws Error 当Redis操作失败时
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const deleted = await redisService.del('user:123');
|
||||
* console.log(deleted ? '删除成功' : '键不存在');
|
||||
* ```
|
||||
*/
|
||||
async del(key: string): Promise<boolean> {
|
||||
try {
|
||||
const result = await this.redis.del(key);
|
||||
this.logger.debug(`删除Redis键: ${key}, 结果: ${result > 0}`);
|
||||
return result > 0;
|
||||
} catch (error) {
|
||||
this.logger.error(`删除Redis键失败: ${key}`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查键是否存在
|
||||
*
|
||||
* 业务逻辑:
|
||||
* 1. 验证键名的有效性
|
||||
* 2. 执行Redis exists命令
|
||||
* 3. 检查返回结果是否大于0
|
||||
* 4. 处理查询异常
|
||||
* 5. 返回键的存在状态
|
||||
*
|
||||
* @param key 键名,不能为空
|
||||
* @returns Promise<boolean> 键存在返回true,不存在返回false
|
||||
* @throws Error 当Redis操作失败时
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const exists = await redisService.exists('user:123');
|
||||
* if (exists) {
|
||||
* console.log('用户数据存在');
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
async exists(key: string): Promise<boolean> {
|
||||
try {
|
||||
const result = await this.redis.exists(key);
|
||||
return result > 0;
|
||||
} catch (error) {
|
||||
this.logger.error(`检查Redis键存在性失败: ${key}`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置键的过期时间
|
||||
*
|
||||
* 业务逻辑:
|
||||
* 1. 验证键名和TTL参数的有效性
|
||||
* 2. 执行Redis expire命令设置过期时间
|
||||
* 3. 记录过期时间设置日志
|
||||
* 4. 处理设置异常
|
||||
*
|
||||
* @param key 键名,不能为空
|
||||
* @param ttl 过期时间(秒),必须大于0
|
||||
* @returns Promise<void> 操作完成的Promise
|
||||
* @throws Error 当Redis操作失败时
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* await redisService.expire('user:123', 3600); // 1小时后过期
|
||||
* ```
|
||||
*/
|
||||
async expire(key: string, ttl: number): Promise<void> {
|
||||
try {
|
||||
await this.redis.expire(key, ttl);
|
||||
this.logger.debug(`设置Redis键过期时间: ${key}, TTL: ${ttl}秒`);
|
||||
} catch (error) {
|
||||
this.logger.error(`设置Redis键过期时间失败: ${key}`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取键的剩余过期时间
|
||||
*
|
||||
* 业务逻辑:
|
||||
* 1. 验证键名的有效性
|
||||
* 2. 执行Redis ttl命令查询剩余时间
|
||||
* 3. 返回剩余时间或状态码
|
||||
* 4. 处理查询异常
|
||||
*
|
||||
* @param key 键名,不能为空
|
||||
* @returns Promise<number> 剩余时间(秒),-1表示永不过期,-2表示键不存在
|
||||
* @throws Error 当Redis操作失败时
|
||||
*
|
||||
* @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> {
|
||||
try {
|
||||
return await this.redis.ttl(key);
|
||||
} catch (error) {
|
||||
this.logger.error(`获取Redis键TTL失败: ${key}`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空所有数据
|
||||
*
|
||||
* 业务逻辑:
|
||||
* 1. 执行Redis flushall命令清空所有数据
|
||||
* 2. 记录清空操作日志
|
||||
* 3. 处理清空异常
|
||||
*
|
||||
* @returns Promise<void> 操作完成的Promise
|
||||
* @throws Error 当Redis操作失败时
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* await redisService.flushall();
|
||||
* console.log('所有数据已清空');
|
||||
* ```
|
||||
*/
|
||||
async flushall(): Promise<void> {
|
||||
try {
|
||||
await this.redis.flushall();
|
||||
this.logger.log('清空所有Redis数据');
|
||||
} catch (error) {
|
||||
this.logger.error('清空Redis数据失败', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置键值对并指定过期时间
|
||||
*
|
||||
* 业务逻辑:
|
||||
* 1. 验证键、值和TTL参数的有效性
|
||||
* 2. 执行Redis setex命令同时设置值和过期时间
|
||||
* 3. 记录操作日志
|
||||
* 4. 处理设置异常
|
||||
*
|
||||
* @param key 键名,不能为空
|
||||
* @param ttl 过期时间(秒),必须大于0
|
||||
* @param value 值,支持字符串类型
|
||||
* @returns Promise<void> 操作完成的Promise
|
||||
* @throws Error 当Redis操作失败时
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* await redisService.setex('session:abc', 1800, 'sessionData');
|
||||
* ```
|
||||
*/
|
||||
async setex(key: string, ttl: number, value: string): Promise<void> {
|
||||
try {
|
||||
await this.redis.setex(key, ttl, value);
|
||||
this.logger.debug(`设置Redis键(setex): ${key}, TTL: ${ttl}秒`);
|
||||
} catch (error) {
|
||||
this.logger.error(`设置Redis键失败(setex): ${key}`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 键值自增操作
|
||||
*
|
||||
* 业务逻辑:
|
||||
* 1. 验证键名的有效性
|
||||
* 2. 执行Redis incr命令进行自增操作
|
||||
* 3. 获取自增后的新值
|
||||
* 4. 记录自增操作日志
|
||||
* 5. 返回新值
|
||||
*
|
||||
* @param key 键名,不能为空
|
||||
* @returns Promise<number> 自增后的新值
|
||||
* @throws Error 当Redis操作失败或值不是数字时
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const newValue = await redisService.incr('counter');
|
||||
* console.log(`计数器新值: ${newValue}`);
|
||||
* ```
|
||||
*/
|
||||
async incr(key: string): Promise<number> {
|
||||
try {
|
||||
const result = await this.redis.incr(key);
|
||||
this.logger.debug(`自增Redis键: ${key}, 新值: ${result}`);
|
||||
return result;
|
||||
} catch (error) {
|
||||
this.logger.error(`自增Redis键失败: ${key}`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 向集合添加成员
|
||||
*
|
||||
* 业务逻辑:
|
||||
* 1. 验证键名和成员的有效性
|
||||
* 2. 执行Redis sadd命令添加成员到集合
|
||||
* 3. 记录添加操作日志
|
||||
* 4. 处理添加异常
|
||||
*
|
||||
* @param key 集合键名,不能为空
|
||||
* @param member 要添加的成员,不能为空
|
||||
* @returns Promise<void> 操作完成的Promise
|
||||
* @throws Error 当Redis操作失败时
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* await redisService.sadd('users', 'user123');
|
||||
* ```
|
||||
*/
|
||||
async sadd(key: string, member: string): Promise<void> {
|
||||
try {
|
||||
await this.redis.sadd(key, member);
|
||||
this.logger.debug(`添加集合成员: ${key} -> ${member}`);
|
||||
} catch (error) {
|
||||
this.logger.error(`添加集合成员失败: ${key}`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从集合移除成员
|
||||
*
|
||||
* 业务逻辑:
|
||||
* 1. 验证键名和成员的有效性
|
||||
* 2. 执行Redis srem命令从集合中移除成员
|
||||
* 3. 记录移除操作日志
|
||||
* 4. 处理移除异常
|
||||
*
|
||||
* @param key 集合键名,不能为空
|
||||
* @param member 要移除的成员,不能为空
|
||||
* @returns Promise<void> 操作完成的Promise
|
||||
* @throws Error 当Redis操作失败时
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* await redisService.srem('users', 'user123');
|
||||
* ```
|
||||
*/
|
||||
async srem(key: string, member: string): Promise<void> {
|
||||
try {
|
||||
await this.redis.srem(key, member);
|
||||
this.logger.debug(`移除集合成员: ${key} -> ${member}`);
|
||||
} catch (error) {
|
||||
this.logger.error(`移除集合成员失败: ${key}`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取集合的所有成员
|
||||
*
|
||||
* 业务逻辑:
|
||||
* 1. 验证键名的有效性
|
||||
* 2. 执行Redis smembers命令获取集合所有成员
|
||||
* 3. 返回成员列表
|
||||
* 4. 处理查询异常
|
||||
*
|
||||
* @param key 集合键名,不能为空
|
||||
* @returns Promise<string[]> 集合成员列表,集合不存在返回空数组
|
||||
* @throws Error 当Redis操作失败时
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const members = await redisService.smembers('users');
|
||||
* console.log('用户列表:', members);
|
||||
* ```
|
||||
*/
|
||||
async smembers(key: string): Promise<string[]> {
|
||||
try {
|
||||
return await this.redis.smembers(key);
|
||||
} catch (error) {
|
||||
this.logger.error(`获取集合成员失败: ${key}`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 模块销毁时的清理操作
|
||||
*
|
||||
* 业务逻辑:
|
||||
* 1. 检查Redis连接是否存在
|
||||
* 2. 断开Redis连接
|
||||
* 3. 记录连接断开日志
|
||||
* 4. 释放相关资源
|
||||
*
|
||||
* @returns void 无返回值
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // NestJS框架会在模块销毁时自动调用
|
||||
* onModuleDestroy() {
|
||||
* // 自动清理Redis连接
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
onModuleDestroy(): void {
|
||||
if (this.redis) {
|
||||
this.redis.disconnect();
|
||||
this.logger.log('Redis连接已断开');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,89 +1,284 @@
|
||||
/**
|
||||
* Redis接口定义
|
||||
* 定义统一的Redis操作接口,支持文件存储和真实Redis切换
|
||||
* Redis服务接口定义
|
||||
*
|
||||
* 功能描述:
|
||||
* - 定义统一的Redis操作接口规范
|
||||
* - 支持文件存储和真实Redis服务的无缝切换
|
||||
* - 提供完整的Redis基础操作方法
|
||||
* - 支持键值对存储、过期时间、集合操作等功能
|
||||
*
|
||||
* 职责分离:
|
||||
* - 接口定义:规范Redis服务的标准操作方法
|
||||
* - 类型约束:确保不同实现类的方法签名一致性
|
||||
* - 抽象层:为上层业务提供统一的Redis访问接口
|
||||
*
|
||||
* 最近修改:
|
||||
* - 2025-01-07: 代码规范优化 - 为所有接口方法添加完整的三级注释,包含业务逻辑和示例代码
|
||||
* - 2025-01-07: 代码规范优化 - 完善文件头注释,添加详细的功能描述和职责说明
|
||||
*
|
||||
* @author moyin
|
||||
* @version 1.0.2
|
||||
* @since 2025-01-07
|
||||
* @lastModified 2025-01-07
|
||||
*/
|
||||
export interface IRedisService {
|
||||
/**
|
||||
* 设置键值对
|
||||
* @param key 键
|
||||
* @param value 值
|
||||
* @param ttl 过期时间(秒)
|
||||
*
|
||||
* 业务逻辑:
|
||||
* 1. 验证键和值的有效性
|
||||
* 2. 根据TTL参数决定是否设置过期时间
|
||||
* 3. 存储键值对到Redis
|
||||
* 4. 记录操作日志
|
||||
*
|
||||
* @param key 键名,不能为空
|
||||
* @param value 值,支持字符串类型
|
||||
* @param ttl 可选的过期时间(秒),不设置则永不过期
|
||||
* @returns Promise<void> 操作完成的Promise
|
||||
* @throws Error 当键名为空或存储失败时
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* await redisService.set('user:123', 'userData', 3600);
|
||||
* await redisService.set('config', 'value'); // 永不过期
|
||||
* ```
|
||||
*/
|
||||
set(key: string, value: string, ttl?: number): Promise<void>;
|
||||
|
||||
/**
|
||||
* 设置键值对并指定过期时间
|
||||
* @param key 键
|
||||
* @param ttl 过期时间(秒)
|
||||
* @param value 值
|
||||
*
|
||||
* 业务逻辑:
|
||||
* 1. 验证键、值和TTL参数的有效性
|
||||
* 2. 设置键值对并同时设置过期时间
|
||||
* 3. 记录操作日志
|
||||
*
|
||||
* @param key 键名,不能为空
|
||||
* @param ttl 过期时间(秒),必须大于0
|
||||
* @param value 值,支持字符串类型
|
||||
* @returns Promise<void> 操作完成的Promise
|
||||
* @throws Error 当参数无效或存储失败时
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* await redisService.setex('session:abc', 1800, 'sessionData');
|
||||
* ```
|
||||
*/
|
||||
setex(key: string, ttl: number, value: string): Promise<void>;
|
||||
|
||||
/**
|
||||
* 获取值
|
||||
* @param key 键
|
||||
* @returns 值或null
|
||||
* 获取键对应的值
|
||||
*
|
||||
* 业务逻辑:
|
||||
* 1. 验证键名的有效性
|
||||
* 2. 从Redis中查找对应的值
|
||||
* 3. 检查键是否存在或已过期
|
||||
* 4. 返回值或null
|
||||
*
|
||||
* @param key 键名,不能为空
|
||||
* @returns Promise<string | null> 键对应的值,不存在或已过期返回null
|
||||
* @throws Error 当键名为空或查询失败时
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const value = await redisService.get('user:123');
|
||||
* if (value !== null) {
|
||||
* console.log('用户数据:', value);
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
get(key: string): Promise<string | null>;
|
||||
|
||||
/**
|
||||
* 删除键
|
||||
* @param key 键
|
||||
* @returns 是否删除成功
|
||||
* 删除指定的键
|
||||
*
|
||||
* 业务逻辑:
|
||||
* 1. 验证键名的有效性
|
||||
* 2. 从Redis中删除指定键
|
||||
* 3. 返回删除操作的结果
|
||||
* 4. 记录删除操作日志
|
||||
*
|
||||
* @param key 键名,不能为空
|
||||
* @returns Promise<boolean> 删除成功返回true,键不存在返回false
|
||||
* @throws Error 当键名为空或删除失败时
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const deleted = await redisService.del('user:123');
|
||||
* console.log(deleted ? '删除成功' : '键不存在');
|
||||
* ```
|
||||
*/
|
||||
del(key: string): Promise<boolean>;
|
||||
|
||||
/**
|
||||
* 检查键是否存在
|
||||
* @param key 键
|
||||
* @returns 是否存在
|
||||
*
|
||||
* 业务逻辑:
|
||||
* 1. 验证键名的有效性
|
||||
* 2. 查询Redis中是否存在该键
|
||||
* 3. 检查键是否已过期
|
||||
* 4. 返回存在性检查结果
|
||||
*
|
||||
* @param key 键名,不能为空
|
||||
* @returns Promise<boolean> 键存在返回true,不存在或已过期返回false
|
||||
* @throws Error 当键名为空或查询失败时
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const exists = await redisService.exists('user:123');
|
||||
* if (exists) {
|
||||
* console.log('用户数据存在');
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
exists(key: string): Promise<boolean>;
|
||||
|
||||
/**
|
||||
* 设置过期时间
|
||||
* @param key 键
|
||||
* @param ttl 过期时间(秒)
|
||||
* 设置键的过期时间
|
||||
*
|
||||
* 业务逻辑:
|
||||
* 1. 验证键名和TTL参数的有效性
|
||||
* 2. 为现有键设置过期时间
|
||||
* 3. 记录过期时间设置日志
|
||||
*
|
||||
* @param key 键名,不能为空
|
||||
* @param ttl 过期时间(秒),必须大于0
|
||||
* @returns Promise<void> 操作完成的Promise
|
||||
* @throws Error 当参数无效或设置失败时
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* await redisService.expire('user:123', 3600); // 1小时后过期
|
||||
* ```
|
||||
*/
|
||||
expire(key: string, ttl: number): Promise<void>;
|
||||
|
||||
/**
|
||||
* 获取剩余过期时间
|
||||
* @param key 键
|
||||
* @returns 剩余时间(秒),-1表示永不过期,-2表示不存在
|
||||
* 获取键的剩余过期时间
|
||||
*
|
||||
* 业务逻辑:
|
||||
* 1. 验证键名的有效性
|
||||
* 2. 查询键的剩余过期时间
|
||||
* 3. 返回相应的时间值或状态码
|
||||
*
|
||||
* @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('键不存在');
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
ttl(key: string): Promise<number>;
|
||||
|
||||
/**
|
||||
* 自增
|
||||
* @param key 键
|
||||
* @returns 自增后的值
|
||||
* 键值自增操作
|
||||
*
|
||||
* 业务逻辑:
|
||||
* 1. 验证键名的有效性
|
||||
* 2. 获取当前值并转换为数字
|
||||
* 3. 执行自增操作(+1)
|
||||
* 4. 返回自增后的新值
|
||||
*
|
||||
* @param key 键名,不能为空
|
||||
* @returns Promise<number> 自增后的新值
|
||||
* @throws Error 当键名为空、值不是数字或操作失败时
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const newValue = await redisService.incr('counter');
|
||||
* console.log(`计数器新值: ${newValue}`);
|
||||
* ```
|
||||
*/
|
||||
incr(key: string): Promise<number>;
|
||||
|
||||
/**
|
||||
* 添加元素到集合
|
||||
* @param key 键
|
||||
* @param member 成员
|
||||
* 向集合添加成员
|
||||
*
|
||||
* 业务逻辑:
|
||||
* 1. 验证键名和成员的有效性
|
||||
* 2. 获取现有集合或创建新集合
|
||||
* 3. 添加成员到集合中
|
||||
* 4. 保存更新后的集合
|
||||
*
|
||||
* @param key 集合键名,不能为空
|
||||
* @param member 要添加的成员,不能为空
|
||||
* @returns Promise<void> 操作完成的Promise
|
||||
* @throws Error 当参数无效或操作失败时
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* await redisService.sadd('users', 'user123');
|
||||
* ```
|
||||
*/
|
||||
sadd(key: string, member: string): Promise<void>;
|
||||
|
||||
/**
|
||||
* 从集合移除元素
|
||||
* @param key 键
|
||||
* @param member 成员
|
||||
* 从集合移除成员
|
||||
*
|
||||
* 业务逻辑:
|
||||
* 1. 验证键名和成员的有效性
|
||||
* 2. 获取现有集合
|
||||
* 3. 从集合中移除指定成员
|
||||
* 4. 保存更新后的集合或删除空集合
|
||||
*
|
||||
* @param key 集合键名,不能为空
|
||||
* @param member 要移除的成员,不能为空
|
||||
* @returns Promise<void> 操作完成的Promise
|
||||
* @throws Error 当参数无效或操作失败时
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* await redisService.srem('users', 'user123');
|
||||
* ```
|
||||
*/
|
||||
srem(key: string, member: string): Promise<void>;
|
||||
|
||||
/**
|
||||
* 获取集合所有成员
|
||||
* @param key 键
|
||||
* @returns 成员列表
|
||||
* 获取集合的所有成员
|
||||
*
|
||||
* 业务逻辑:
|
||||
* 1. 验证键名的有效性
|
||||
* 2. 获取集合数据
|
||||
* 3. 检查集合是否存在或已过期
|
||||
* 4. 返回成员列表
|
||||
*
|
||||
* @param key 集合键名,不能为空
|
||||
* @returns Promise<string[]> 集合成员列表,集合不存在返回空数组
|
||||
* @throws Error 当键名为空或查询失败时
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const members = await redisService.smembers('users');
|
||||
* console.log('用户列表:', members);
|
||||
* ```
|
||||
*/
|
||||
smembers(key: string): Promise<string[]>;
|
||||
|
||||
/**
|
||||
* 清空所有数据
|
||||
*
|
||||
* 业务逻辑:
|
||||
* 1. 清空Redis中的所有键值对
|
||||
* 2. 重置所有数据结构
|
||||
* 3. 记录清空操作日志
|
||||
*
|
||||
* @returns Promise<void> 操作完成的Promise
|
||||
* @throws Error 当清空操作失败时
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* await redisService.flushall();
|
||||
* console.log('所有数据已清空');
|
||||
* ```
|
||||
*/
|
||||
flushall(): Promise<void>;
|
||||
}
|
||||
@@ -1,12 +1,46 @@
|
||||
/**
|
||||
* Redis模块配置
|
||||
*
|
||||
* 功能描述:
|
||||
* - 根据环境变量自动选择Redis实现方式
|
||||
* - 开发环境使用文件存储模拟Redis功能
|
||||
* - 生产环境连接真实Redis服务器
|
||||
* - 提供统一的Redis服务注入接口
|
||||
*
|
||||
* 职责分离:
|
||||
* - 服务工厂:根据配置创建合适的Redis服务实例
|
||||
* - 依赖注入:为其他模块提供REDIS_SERVICE令牌
|
||||
* - 环境适配:自动适配不同环境的Redis需求
|
||||
*
|
||||
* 最近修改:
|
||||
* - 2025-01-07: 代码规范优化 - 更新导入路径,修正文件重命名后的引用关系
|
||||
* - 2025-01-07: 代码规范优化 - 完善文件头注释和类注释,添加详细功能说明
|
||||
*
|
||||
* @author moyin
|
||||
* @version 1.0.2
|
||||
* @since 2025-01-07
|
||||
* @lastModified 2025-01-07
|
||||
*/
|
||||
import { Module } from '@nestjs/common';
|
||||
import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||
import { FileRedisService } from './file-redis.service';
|
||||
import { RealRedisService } from './real-redis.service';
|
||||
import { FileRedisService } from './file_redis.service';
|
||||
import { RealRedisService } from './real_redis.service';
|
||||
import { IRedisService } from './redis.interface';
|
||||
|
||||
/**
|
||||
* Redis模块
|
||||
* 根据环境变量自动选择文件存储或真实Redis服务
|
||||
*
|
||||
* 职责:
|
||||
* - 根据环境变量自动选择文件存储或真实Redis服务
|
||||
* - 提供统一的Redis服务注入接口
|
||||
* - 管理Redis服务的生命周期
|
||||
*
|
||||
* 主要方法:
|
||||
* - useFactory() - 根据配置创建Redis服务实例
|
||||
*
|
||||
* 使用场景:
|
||||
* - 在需要Redis功能的模块中导入此模块
|
||||
* - 通过@Inject('REDIS_SERVICE')注入Redis服务
|
||||
*/
|
||||
@Module({
|
||||
imports: [ConfigModule],
|
||||
|
||||
Reference in New Issue
Block a user