Skip to content

集成 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 不替代数据库约束。缓存未命中、失效或连接不可用时,业务应有明确的降级和失败策略。

参考

基于 MIT 许可发布