1. 背景与设计考量
在高并发或频繁访问的数据场景下,缓存是提升系统性能、降低数据库压力的重要手段。设计 Redis 缓存服务时,我们通常关注以下几点:
- 缓存统一在 Redis:所有数据存储在 Redis 上,确保所有实例缓存一致。
- Key 组织策略:普通 key 用于简单对象,哈希表用于结构化或分组数据。
- 字段 TTL:哈希表字段支持自动失效,通过字段值记录过期时间。
- 环境变量配置:便于在开发、测试、生产环境中使用不同 Redis 配置。
- 统一接口:缓存服务提供统一方法,方便业务模块复用和维护。
- 易扩展:可增加集群支持、定时清理过期字段等生产级需求。
2. 实现
说明: 如果想查看
CacheService的完整实现,包括所有辅助方法、Redis 哈希操作以及 TTL 处理,可以访问我们的 GitHub 仓库:RaytonX NestJS Setup。完整代码有助于更好地理解实现逻辑,并方便在项目中复用。
1. 安装依赖
pnpm add @nestjs/cache-manager @keyv/redis
2. 配置缓存模块
在 cache.module.ts 中配置缓存:
// cache.module.ts
import { CacheModule } from "@nestjs/cache-manager";
import { ConfigModule, ConfigService } from "@nestjs/config";
import { Global, Module } from "@nestjs/common";
import KeyvRedis from "@keyv/redis";
@Module({
imports: [
CacheModule.registerAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: (configService: ConfigService) => {
const redisUrl = configService.get<string>("REDIS_URL");
if (!redisUrl) throw new Error("Missing REDIS_URL in env");
return {
stores: [new KeyvRedis(redisUrl)],
ttl: 60 * 60 * 1000,
};
},
}),
],
})
export class AppModule {}
3. 使用 CACHE_MANAGER 注入缓存服务 CacheService
import { CACHE_MANAGER } from "@nestjs/cache-manager";
import { Inject, Injectable } from "@nestjs/common";
import { Cache } from "cache-manager";
import { RedisService } from "../redis/redis.service";
@Injectable()
export class CacheService {
private readonly ttlSeconds = 60 * 60;
private readonly emptyTtlSeconds = 60 * 60;
constructor(
@Inject(CACHE_MANAGER) private cacheManager: Cache,
private readonly redisService: RedisService
) {}
}
4. 普通 Key 缓存: 使用 CACHE_MANAGER
对于单个对象或简单值,直接使用 Redis key 存储,并支持 TTL。缓存服务提供统一接口:先从 Redis 读取,如果不存在则执行回调获取数据并写入缓存,同时支持空值处理和 TTL 设置。
export class CacheService {
...
async getOrSet<T>(key: string, fn: () => Promise<T>, ttl = this.ttlSeconds): Promise<T> {
const cached = await this.cacheManager.get<T>(key);
if (cached) return cached;
let result = await fn();
if (result !== undefined && result !== null) {
await this.cacheManager.set(key, result, ttl * 1000);
} else {
result = {} as Awaited<T>;
await this.cacheManager.set(key, result, this.emptyTtlSeconds * 1000);
}
return result;
}
async set<T>(key: string, value: T, ttl = this.ttlSeconds): Promise<void> {
await this.cacheManager.set(key, value, ttl);
}
async get<T>(key: string): Promise<T | undefined> {
return this.cacheManager.get<T>(key);
}
...
}
5. 哈希表缓存
对于结构化数据或需要分组管理的缓存,可以使用 Redis 哈希表(hash):
- 哈希表用于组织字段,便于按照业务分组存储。
- 每个字段可以包含过期时间,通过读取时判断是否失效。
- 可以提供
hgetOrSet、hset、hget等统一接口,方便业务调用。
export class CacheService {
...
async hgetOrSet<T>(
hash: string,
field: string,
fn: () => Promise<T>,
ttlSeconds?: number
): Promise<T> {
const redis = this.redisService.getClient();
const value = await redis.hget(hash, field);
if (value) {
return JSON.parse(value) as T;
}
const data = await fn();
const cacheData = ttlSeconds
? { data, expireAt: Date.now() + ttlSeconds * 1000 }
: { data };
await redis.hset(hash, field, JSON.stringify(cacheData));
return data;
}
async hset<T>(hash: string, field: string, data: T) {
const redis = this.redisService.getClient();
const cacheData = ttlSeconds
? { data, expireAt: Date.now() + ttlSeconds * 1000 }
: { data };
await redis.hset(hash, field, JSON.stringify(cacheData));
}
async hget<T>(hash: string, field: string): Promise<T | undefined> {
const redis = this.redisService.getClient();
const value = await redis.hget(hash, field);
if (!value) return null;
const parsed = JSON.parse(value);
if (parsed.expireAt && Date.now() > parsed.expireAt) {
await this.redisService.hdel(hash, field);
return null;
}
return parsed.data;
}
...
}
6. 清理过期哈希缓存
哈希表字段的 TTL 需要手动判断,生产中通常会配合定时任务清理过期字段:
- 定期遍历哈希表所有字段
- 检查过期时间,删除已过期的字段
- 保证缓存数据准确性,同时减少 Redis 内存占用
async clearExpiredUsers() {
const allFields = await this.redisService.hKeys('users');
for (const userId of allFields) {
const data = await this.redisService.getHash('users', userId);
if (data?.expireAt && Date.now() > data.expireAt) {
await this.redisService.hDel('users', userId);
}
}
}
3. 总结
通过以上设计,NestJS 项目可以实现一个全 Redis 缓存服务,特点包括:
- 统一在 Redis:无需内存一级缓存,所有数据集中管理
- Key 组织策略:普通 key + hash 分组,便于维护和规范化
- 字段级 TTL:支持哈希表字段自动失效
- 多环境可配置:支持开发/测试/生产独立配置
- 统一接口:缓存服务方法统一,便于业务模块复用
- 生产级扩展性:支持集群、定时清理等高可用需求
这种方案简单、清晰,同时便于在实际项目中管理大量缓存数据。