主题
集成 Redis
使用 node-redis 封装 NestJS 全局 Redis Client
适用范围
Redis 客户端适用于缓存、计数器、幂等键、限流状态和临时数据等通用能力。本页使用官方 node-redis 客户端,不使用 Nest 缓存拦截器;业务模块通过 RedisService 访问 Redis,而不是各自创建连接。
安装与配置
bash
pnpm add redis连接信息通过单个 URL 配置,便于本地、容器与托管 Redis 环境复用。密码仅保存在 .env 或部署平台的密钥配置中。
md
REDIS_URL=redis://localhost:6379使用 TLS 时改为 rediss:// URL。生产环境应启用认证与 TLS,并限制 Redis 仅可由应用网络访问。
全局模块
RedisService 在应用启动时建立一个连接,在关闭时释放连接。客户端必须注册 error 监听器;否则网络错误可能导致 Node.js 进程异常退出。
ts
import { Global, Injectable, Logger, Module, OnApplicationShutdown, OnModuleInit } from '@nestjs/common'
import { ConfigService } from '@nestjs/config'
import { createClient } from 'redis'
@Injectable()
export class RedisService implements OnModuleInit, OnApplicationShutdown {
private readonly logger = new Logger(RedisService.name)
private readonly client
constructor(config: ConfigService) {
this.client = createClient({
url: config.getOrThrow<string>('REDIS_URL'),
})
this.client.on('error', (error) => {
this.logger.error('Redis client error', error.stack)
})
}
async onModuleInit() {
await this.client.connect()
}
async onApplicationShutdown() {
if (this.client.isOpen) await this.client.close()
}
get(key: string) {
return this.client.get(key)
}
set(key: string, value: string, ttlSeconds?: number) {
if (ttlSeconds) return this.client.set(key, value, { EX: ttlSeconds })
return this.client.set(key, value)
}
del(key: string) {
return this.client.del(key)
}
}
@Global()
@Module({
providers: [RedisService],
exports: [RedisService],
})
export class RedisModule {}将 RedisModule 导入 AppModule,并在入口启用 Nest 的关闭钩子;否则进程收到 SIGTERM 时不会调用 onApplicationShutdown()。
ts
const app = await NestFactory.create(AppModule)
app.enableShutdownHooks()
await app.listen(process.env.PORT ?? 3000)业务使用
为 key 添加业务前缀,并为临时数据显式设置 TTL。JSON 序列化应集中在 RedisService 或领域服务中,读取失败、解析失败时回退到数据源,而不是将缓存内容视为可信业务数据。
ts
import { Injectable } from '@nestjs/common'
@Injectable()
export class UsersService {
constructor(private readonly redis: RedisService) {}
async findOne(id: string) {
const key = `users:${id}`
const cached = await this.redis.get(key)
if (cached) return JSON.parse(cached) as UserResponseDto
const user = await this.usersRepository.findOneByOrFail({ id })
await this.redis.set(key, JSON.stringify(user), 60)
return user
}
async update(id: string, input: UpdateUserDto) {
const user = await this.usersRepository.updateAndReturn(id, input)
await this.redis.del(`users:${id}`)
return user
}
}缓存失效应与写操作放在同一用例中:数据更新成功后删除对应 key。对于需要强一致性的读路径,不应依赖 Redis 缓存。
连接与命令边界
- 业务请求只共享一个常规命令连接;Pub/Sub、阻塞命令和专用连接池需单独创建客户端,避免占用常规命令连接。
- 将连接错误、重连次数和命令延迟接入日志或监控;不要记录 URL 中的密码、完整 value 或用户敏感信息。
- 多条独立命令需原子执行时,使用 Redis 事务或 Lua;不要依赖应用层的“先读后写”保证并发正确性。
- Redis 不替代数据库约束。缓存未命中、失效或连接不可用时,业务应有明确的降级和失败策略。
