【NestJS】在 nest.js 项目中,如何使用 Postgresql 来做缓存?

在 NestJS 项目中使用 PostgreSQL 作为缓存存储是一个可行的方案,尽管它通常不如 Redis 等专门的内存缓存系统高效。但是,如果你已经在使用 PostgreSQL 并且希望避免引入额外的服务(如 Redis),或者你的缓存需求量不大、对延迟不那么敏感,那么这确实是一个不错的选择。

本指南将详细介绍如何在 NestJS 中使用 TypeORM 和 PostgreSQL 来实现一个基本的缓存机制。


重要提示 (Disclaimer)

  • 性能考量: PostgreSQL 是一个关系型数据库,主要设计用于持久化存储和复杂查询。与 Redis 这种内存数据库相比,它在处理大量高并发的读写操作时通常会慢得多,因为它涉及磁盘 I/O、事务管理等开销。
  • 适用场景: 这种方案更适合:
    • 对缓存性能要求不那么极致的场景。
    • 缓存数据量相对较小或更新不频繁的场景。
    • 希望简化部署,避免引入新服务的场景。
    • 已经深度依赖 PostgreSQL 的项目。
  • 功能限制: Redis 提供了更多高级的缓存功能,如原子操作、发布/订阅、各种数据结构(列表、集合、哈希等)。PostgreSQL 只能模拟简单的键值对缓存。

步骤概览

  1. 数据库Schema设计: 创建一个 cache 表来存储键值对和过期时间。
  2. NestJS Entity定义:cache 表创建 TypeORM Entity。
  3. Cache Service实现: 创建一个服务来封装缓存的 getsetdelreset 逻辑。
  4. Cache Module整合: 将 Entity 和 Service 整合到 NestJS 模块中。
  5. AppModule集成: 在根模块中导入和配置缓存模块。
  6. 使用示例: 在应用中使用缓存服务。

详细步骤

前提条件
  • 一个 NestJS 项目。

  • 已安装并配置 TypeORM 和 PostgreSQL 驱动:

    bash 复制代码
    npm install @nestjs/typeorm typeorm pg
    # 或者 yarn add @nestjs/typeorm typeorm pg
  • 确保你的 AppModule 中已经配置了 TypeORM 的数据库连接。

1. 数据库Schema设计

在你的 PostgreSQL 数据库中创建一个 application_cache 表(或你喜欢的任何名称):

sql 复制代码
CREATE TABLE IF NOT EXISTS application_cache (
    key VARCHAR(255) PRIMARY KEY, -- 缓存键,建议加上索引以提高查询速度
    value JSONB NOT NULL,         -- 缓存值,使用 JSONB 存储任意类型的数据
    expires_at TIMESTAMP WITH TIME ZONE, -- 缓存过期时间
    created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);

-- 为 expires_at 列添加索引,以便更高效地清理过期数据
CREATE INDEX IF NOT EXISTS idx_application_cache_expires_at ON application_cache (expires_at);

解释:

  • key: 缓存的唯一标识符,设为主键,确保快速查找。
  • value: 使用 JSONB 类型可以存储任何 JSON 兼容的数据结构(对象、数组、字符串、数字等),这是最佳实践。
  • expires_at: 存储缓存的过期时间。如果为 NULL,表示永不过期。
  • created_at, updated_at: 审计字段。
2. NestJS Entity定义

创建 src/cache/cache.entity.ts 文件:

typescript 复制代码
// src/cache/cache.entity.ts
import { Entity, PrimaryColumn, Column } from 'typeorm';

@Entity('application_cache') // 对应数据库表名
export class CacheEntity {
  @PrimaryColumn({ type: 'varchar', length: 255 })
  key: string;

  @Column({ type: 'jsonb', nullable: false })
  value: any; // 使用 any 来存储 JSONB 类型的数据,TypeORM 会自动处理

  @Column({ type: 'timestamp with time zone', name: 'expires_at', nullable: true })
  expiresAt: Date | null;

  @Column({ type: 'timestamp with time zone', name: 'created_at', default: () => 'CURRENT_TIMESTAMP' })
  createdAt: Date;

  @Column({ type: 'timestamp with time zone', name: 'updated_at', default: () => 'CURRENT_TIMESTAMP', onUpdate: 'CURRENT_TIMESTAMP' })
  updatedAt: Date;
}
3. Cache Service实现

创建 src/cache/cache.service.ts 文件:

typescript 复制代码
// src/cache/cache.service.ts
import { Injectable, Logger } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { LessThan, Repository, IsNull } from 'typeorm'; // 导入 LessThan 和 IsNull
import { CacheEntity } from './cache.entity';

@Injectable()
export class CacheService {
  private readonly logger = new Logger(CacheService.name);

  constructor(
    @InjectRepository(CacheEntity)
    private cacheRepository: Repository<CacheEntity>,
  ) {
    // 定期清理过期缓存,例如每小时运行一次
    setInterval(() => this.cleanupExpiredCache(), 60 * 60 * 1000);
  }

  /**
   * 获取缓存值
   * @param key 缓存键
   * @returns 缓存值或 null
   */
  async get<T>(key: string): Promise<T | null> {
    const cacheEntry = await this.cacheRepository.findOne({ where: { key } });

    if (!cacheEntry) {
      return null;
    }

    // 检查缓存是否过期
    if (cacheEntry.expiresAt && cacheEntry.expiresAt < new Date()) {
      this.logger.debug(`Cache entry for key "${key}" expired. Deleting.`);
      await this.del(key); // 自动删除过期缓存
      return null;
    }

    return cacheEntry.value as T;
  }

  /**
   * 设置缓存值
   * @param key 缓存键
   * @param value 缓存值
   * @param ttlSeconds 缓存存活时间 (秒)。如果为 undefined 或 0,则永不过期。
   */
  async set(key: string, value: any, ttlSeconds?: number): Promise<void> {
    const expiresAt = ttlSeconds
      ? new Date(Date.now() + ttlSeconds * 1000)
      : null;

    // 使用 upsert 确保原子性:如果键存在则更新,不存在则插入
    await this.cacheRepository.upsert(
      { key, value, expiresAt },
      ['key'] // 冲突解决策略:如果 key 冲突,则更新
    );
  }

  /**
   * 删除缓存
   * @param key 缓存键
   */
  async del(key: string): Promise<void> {
    await this.cacheRepository.delete({ key });
  }

  /**
   * 清空所有缓存 (谨慎使用!)
   */
  async reset(): Promise<void> {
    this.logger.warn('Clearing ALL cache entries!');
    await this.cacheRepository.clear();
  }

  /**
   * 后台任务:清理所有已过期的缓存条目
   */
  private async cleanupExpiredCache(): Promise<void> {
    this.logger.debug('Starting cleanup of expired cache entries...');
    try {
      // 删除 expiresAt 字段存在且小于当前时间的记录
      const deleteResult = await this.cacheRepository.delete({
        expiresAt: LessThan(new Date()),
      });
      this.logger.debug(`Cleaned up ${deleteResult.affected || 0} expired cache entries.`);
    } catch (error) {
      this.logger.error('Error during cache cleanup:', error.stack);
    }
  }
}

解释:

  • get<T>(key): 查询数据库获取缓存,并手动检查 expiresAt。如果过期,则删除并返回 null
  • set(key, value, ttlSeconds): 计算过期时间,然后使用 TypeORM 的 upsert 方法(如果 TypeORM 版本支持)或 save + findBy 逻辑来插入或更新缓存。upsert 是更好的选择,因为它在数据库层面处理了冲突,保证了原子性。
  • del(key): 根据键删除缓存。
  • reset(): 清空整个缓存表。在生产环境中需谨慎使用。
  • cleanupExpiredCache(): 一个私有方法,通过 setInterval 定期运行,负责从数据库中物理删除所有过期的缓存条目。这对于维护数据库大小和性能很重要。
4. Cache Module整合

创建 src/cache/cache.module.ts 文件:

typescript 复制代码
// src/cache/cache.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { CacheService } from './cache.service';
import { CacheEntity } from './cache.entity';

@Module({
  imports: [TypeOrmModule.forFeature([CacheEntity])], // 注册 CacheEntity
  providers: [CacheService],
  exports: [CacheService], // 导出 CacheService,以便其他模块可以使用
})
export class CacheModule {}
5. AppModule集成

在你的 src/app.module.ts 中导入并注册 CacheModule,并确保 TypeOrmModule.forRoot 中包含了 CacheEntity

typescript 复制代码
// src/app.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { CacheModule } from './cache/cache.module';
import { CacheEntity } from './cache/cache.entity'; // 引入 CacheEntity

@Module({
  imports: [
    TypeOrmModule.forRoot({
      type: 'postgres',
      host: 'localhost',
      port: 5432,
      username: 'your_user',
      password: 'your_password',
      database: 'your_database',
      entities: [CacheEntity], // *** 确保这里包含了 CacheEntity ***
      synchronize: true, // 开发环境使用,生产环境请使用 Migrations
      logging: false,
    }),
    CacheModule, // 导入你的 CacheModule
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

注意:

  • entities: [CacheEntity] 是非常重要的,它告诉 TypeORM 管理这个实体。
  • synchronize: true 在开发环境下很方便,它会自动创建或更新数据库Schema。但在生产环境中,强烈建议使用 TypeORM 的 Migrations
6. 使用示例

现在,你可以在你的任何服务或控制器中注入并使用 CacheService 了。

typescript 复制代码
// src/app.service.ts
import { Injectable } from '@nestjs/common';
import { CacheService } from './cache/cache.service';

@Injectable()
export class AppService {
  constructor(private readonly cacheService: CacheService) {}

  async getHello(): Promise<string> {
    const cacheKey = 'hello_greeting';
    let greeting = await this.cacheService.get<string>(cacheKey);

    if (greeting) {
      console.log('Fetching greeting from cache.');
      return `Hello from cache: ${greeting}`;
    }

    console.log('Fetching greeting from slow source...');
    // 模拟一个耗时操作
    await new Promise(resolve => setTimeout(resolve, 2000));
    greeting = 'World! (from slow source)';

    // 将结果缓存 60 秒
    await this.cacheService.set(cacheKey, greeting, 60);

    return `Hello freshly: ${greeting}`;
  }

  async getUserData(userId: number): Promise<any> {
    const cacheKey = `user_${userId}_data`;
    let userData = await this.cacheService.get<any>(cacheKey);

    if (userData) {
      console.log(`User ${userId} data from cache.`);
      return userData;
    }

    console.log(`Fetching user ${userId} data from DB...`);
    // 模拟从数据库获取用户数据
    await new Promise(resolve => setTimeout(resolve, 1500));
    userData = { id: userId, name: `User ${userId}`, email: `user${userId}@example.com` };

    // 缓存用户数据 5 分钟 (300秒)
    await this.cacheService.set(cacheKey, userData, 300);

    return userData;
  }
}

进一步的优化和考虑

  1. 索引: 确保 key 列有主键或唯一索引,expires_at 列有普通索引,以加速查找和过期清理。
  2. 错误处理:CacheService 中添加更健壮的 try/catch 块来处理数据库操作可能出现的错误。
  3. 连接池: TypeORM 会自动管理数据库连接池,但在高并发场景下,确保你的数据库和 TypeORM 配置有足够的连接数。
  4. 序列化/反序列化: JSONB 列在 TypeORM 中会自动处理 JSON 对象的序列化和反序列化。如果存储的是简单字符串或数字,它也会将其转换为 JSON 兼容格式。
  5. 性能监控: 监控 PostgreSQL 的 CPU、内存、I/O 使用情况,以及缓存表的读写延迟。如果缓存成为瓶颈,则需要重新考虑引入专门的缓存系统(如 Redis)。
  6. 高级缓存策略:
    • "Cache-Aside"(旁路缓存)模式: 这是我们目前实现的模式,应用程序代码负责检查缓存,如果未命中则从原始数据源获取,然后更新缓存。
    • "Write-Through"(直写)/ "Write-Back"(回写)模式: 通常需要更复杂的抽象层,可能不太适合直接在关系型数据库上实现。
  7. 分布式缓存: 如果你的 NestJS 应用是多实例部署的,那么所有实例都会使用同一个 PostgreSQL 缓存表,这天然支持了分布式缓存。而 Redis 也同样支持。

通过上述步骤,你就可以在 NestJS 项目中成功地使用 PostgreSQL 作为缓存存储了。再次强调,请根据你的实际需求和性能预算来选择最合适的缓存方案。

相关推荐
图灵信徒3 小时前
R语言数据结构与数据处理基础内容
开发语言·数据挖掘·数据分析·r语言
oioihoii3 小时前
高性能推理引擎的基石:C++与硬件加速的完美融合
开发语言·c++
weixin_456904273 小时前
基于C#的文档处理
开发语言·c#
扶苏-su3 小时前
Java---StringBuilder
java·开发语言
EndingCoder3 小时前
Node.js 数据查询优化技巧
服务器·javascript·数据库·node.js·数据查询优化
我来整一篇3 小时前
[java] JVM 内存泄漏分析案例
java·开发语言·jvm
芒果Cake3 小时前
【Node.js】Node.js 模块系统
javascript·node.js
程序员黄同学3 小时前
解释 Python 中的属性查找顺序(Attribute Lookup Order)
开发语言·python
苏打水com3 小时前
深入浅出 JavaScript 异步编程:从回调地狱到 Async/Await
开发语言·javascript·ecmascript