forked from datawhale/whale-town-end
feat:添加Redis缓存服务
- 实现Redis服务接口和抽象层 - 提供真实Redis服务实现 (RealRedisService) - 提供文件模拟Redis服务 (FileRedisService) 用于开发测试 - 支持基本的Redis操作:get、set、del、exists、ttl - 添加Redis模块配置和依赖注入
This commit is contained in:
204
src/core/redis/file-redis.service.ts
Normal file
204
src/core/redis/file-redis.service.ts
Normal file
@@ -0,0 +1,204 @@
|
||||
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数据');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user