nestjs.prisma.postgresql作为缓存
在 Nest.js 项目中使用 Prisma 和 PostgreSQL 作为缓存,而不是 Redis,虽然这不是传统意义上的缓存方案(因为 Redis 是专门为高性能缓存设计的),但如果你已经拥有 PostgreSQL 数据库,并且不希望引入额外的技术栈(如 Redis),这确实是一个可行的替代方案。
这种方法的主要优点是:
- 减少依赖: 不需要额外部署和管理 Redis 服务器。
- 统一数据源: 缓存数据和业务数据都在同一个 PostgreSQL 数据库中。
- 事务一致性: 如果你的缓存更新需要与数据库中的其他操作保持事务一致性,PostgreSQL 可以提供。
主要缺点是:
- 性能: PostgreSQL 通常不如 Redis 快,尤其是在高并发读写场景下。磁盘 I/O 和事务开销会带来额外的延迟。
- 功能: 缺乏 Redis 提供的丰富数据结构(List, Set, Hash等)和高级功能(Pub/Sub, Lua脚本等)。
- 内存使用: PostgreSQL 是一个关系型数据库,它的内存占用通常比 Redis 更高。
- 可伸缩性: 垂直伸缩 PostgreSQL 比水平伸缩 Redis 更具挑战性。
实现思路:
- 创建缓存表: 在 PostgreSQL 中创建一个专门的表来存储缓存数据。
- Prisma 模型: 定义对应的 Prisma 模型。
- 缓存服务: 创建一个 Nest.js 服务来封装缓存的存取逻辑,包括 TTL(Time-To-Live,过期时间)管理。
- 后台清理: 实现一个定时任务来清理过期的缓存条目。
下面是详细的实现步骤:
1. 项目准备
确保你已经有一个 Nest.js 项目,并且已经配置好了 Prisma 连接到 PostgreSQL。
如果你还没有,可以快速搭建:
# 创建 Nest.js 项目
nest new my-postgres-cache-app
cd my-postgres-cache-app
# 安装 Prisma
npm install prisma @prisma/client
npx prisma init
# 修改 .env 文件,配置你的 PostgreSQL 数据库连接
# DATABASE_URL="postgresql://user:password@localhost:5432/mydatabase?schema=public"
# 修改 schema.prisma,设置 provider = "postgresql"
2. 创建 Prisma 模型和迁移
修改 prisma/schema.prisma 文件,添加一个 CacheEntry 模型:
// prisma/schema.prisma
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model CacheEntry {
// 缓存键,作为主键
key String @id
// 缓存值,使用 Json 类型可以存储任意结构的数据
value Json
// 过期时间,如果为 null 则永不过期
expiresAt DateTime? @map("expires_at")
// 创建时间
createdAt DateTime @default(now()) @map("created_at")
// 更新时间
updatedAt DateTime @updatedAt @map("updated_at")
@@map("cache_entries") // 数据库表名
}
然后,生成数据库迁移并应用:
npx prisma migrate dev --name create_cache_table
这会在你的 PostgreSQL 数据库中创建一个名为 cache_entries 的表。
3. 创建缓存模块和服务
我们将创建一个 CacheModule 和 CacheService 来处理缓存逻辑。
3.1 cache.module.ts
// src/cache/cache.module.ts
import { Module, Global } from '@nestjs/common';
import { CacheService } from './cache.service';
import { PrismaModule } from '../prisma/prisma.module'; // 假设你有一个 PrismaModule
@Global() // 使 CacheService 可以在任何地方被注入
@Module({
imports: [PrismaModule], // 导入 PrismaModule
providers: [CacheService],
exports: [CacheService],
})
export class CacheModule {}
3.2 cache.service.ts
// src/cache/cache.service.ts
import { Injectable, Logger } from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.service'; // 假设你有一个 PrismaService
@Injectable()
export class CacheService {
private readonly logger = new Logger(CacheService.name);
constructor(private readonly prisma: PrismaService) {}
/**
* 设置缓存
* @param key 缓存键
* @param value 缓存值 (可以是任何 JSON 可序列化的对象)
* @param ttl 缓存过期时间 (秒),如果为 null 或 0 则永不过期
*/
async set<T>(key: string, value: T, ttl: number | null = null): Promise<void> {
const expiresAt = ttl !== null && ttl > 0 ? new Date(Date.now() + ttl * 1000) : null;
const jsonValue = JSON.stringify(value); // 将值序列化为 JSON 字符串
try {
await this.prisma.cacheEntry.upsert({
where: { key },
update: { value: jsonValue, expiresAt },
create: { key, value: jsonValue, expiresAt },
});
this.logger.debug(`Cache set: ${key}, TTL: ${ttl !== null ? ttl + 's' : 'none'}`);
} catch (error) {
this.logger.error(`Failed to set cache for key ${key}: ${error.message}`, error.stack);
throw error;
}
}
/**
* 获取缓存
* @param key 缓存键
* @returns 缓存值,如果不存在或已过期则返回 null
*/
async get<T>(key: string): Promise<T | null> {
try {
const entry = await this.prisma.cacheEntry.findUnique({
where: { key },
});
if (!entry) {
this.logger.debug(`Cache miss: ${key} (not found)`);
return null;
}
// 检查是否过期
if (entry.expiresAt && entry.expiresAt.getTime() <= Date.now()) {
this.logger.debug(`Cache miss: ${key} (expired)`);
// 可以在这里选择立即删除过期条目,但更推荐使用后台任务进行批量清理
// await this.delete(key);
return null;
}
this.logger.debug(`Cache hit: ${key}`);
return JSON.parse(entry.value as string) as T; // 反序列化 JSON 字符串
} catch (error) {
this.logger.error(`Failed to get cache for key ${key}: ${error.message}`, error.stack);
throw error;
}
}
/**
* 删除缓存
* @param key 缓存键
*/
async delete(key: string): Promise<void> {
try {
await this.prisma.cacheEntry.delete({
where: { key },
});
this.logger.debug(`Cache deleted: ${key}`);
} catch (error) {
// 如果 key 不存在,Prisma 会抛出 P2025 错误,可以捕获并忽略
if (error.code === 'P2025') {
this.logger.warn(`Cache delete failed: key ${key} not found.`);
} else {
this.logger.error(`Failed to delete cache for key ${key}: ${error.message}`, error.stack);
throw error;
}
}
}
/**
* 检查缓存是否存在且未过期
* @param key 缓存键
* @returns true 如果缓存存在且未过期,否则 false
*/
async has(key: string): Promise<boolean> {
const value = await this.get(key);
return value !== null;
}
}
3.3 Prisma Service (如果你还没有)
// src/prisma/prisma.service.ts
import { INestApplication, Injectable, OnModuleInit } from '@nestjs/common';
import { PrismaClient } from '@prisma/client';
@Injectable()
export class PrismaService extends PrismaClient implements OnModuleInit {
async onModuleInit() {
await this.$connect();
}
async enableShutdownHooks(app: INestApplication) {
this.$on('beforeExit', async () => {
await app.close();
});
}
}
// src/prisma/prisma.module.ts
import { Module } from '@nestjs/common';
import { PrismaService } from './prisma.service';
@Module({
providers: [PrismaService],
exports: [PrismaService],
})
export class PrismaModule {}
4. 集成到主模块
在 app.module.ts 中导入 CacheModule 和 PrismaModule:
// src/app.module.ts
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { PrismaModule } from './prisma/prisma.module';
import { CacheModule } from './cache/cache.module'; // 导入你的 CacheModule
import { ScheduleModule } from '@nestjs/schedule'; // 用于定时任务
@Module({
imports: [
PrismaModule,
CacheModule,
ScheduleModule.forRoot(), // 启用 Nest.js 的定时任务模块
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
5. 使用缓存服务
现在你可以在任何 Nest.js 服务或控制器中注入并使用 CacheService:
// src/app.controller.ts
import { Controller, Get, Post, Body, Param } from '@nestjs/common';
import { CacheService } from './cache/cache.service';
import { AppService } from './app.service'; // 假设你有一个 AppService
@Controller('data')
export class AppController {
constructor(
private readonly cacheService: CacheService,
private readonly appService: AppService,
) {}
@Get(':id')
async getData(@Param('id') id: string): Promise<any> {
const cacheKey = `data:${id}`;
let data = await this.cacheService.get(cacheKey);
if (data) {
console.log(`Getting data for ${id} from cache.`);
return { source: 'cache', data };
}
// 缓存未命中,从数据库或其他源获取数据
console.log(`Getting data for ${id} from source.`);
data = await this.appService.getSomeDataFromDB(id); // 假设从你的业务服务获取数据
// 将数据存入缓存,设置 60 秒过期
await this.cacheService.set(cacheKey, data, 60);
return { source: 'db', data };
}
@Post()
async createData(@Body() payload: any) {
// 创建数据后,可能需要使相关缓存失效
const created = await this.appService.createSomeDataInDB(payload);
// 假设你有一个方法可以清除所有相关缓存或根据ID清除
await this.cacheService.delete(`data:${created.id}`); // 使特定ID的缓存失效
// 或者如果你知道它会影响某个列表缓存,也清除它
await this.cacheService.delete('data:list');
return created;
}
}
// src/app.service.ts (示例业务服务)
import { Injectable } from '@nestjs/common';
@Injectable()
export class AppService {
async getSomeDataFromDB(id: string): Promise<any> {
// 模拟从数据库获取数据
return new Promise((resolve) => {
setTimeout(() => {
resolve({ id, name: `Item ${id}`, description: 'This is some data from DB.' });
}, 500); // 模拟网络延迟
});
}
async createSomeDataInDB(payload: any): Promise<any> {
// 模拟向数据库写入数据
return new Promise((resolve) => {
setTimeout(() => {
const newId = Math.random().toString(36).substring(7);
resolve({ id: newId, ...payload, createdAt: new Date() });
}, 300);
});
}
}
6. 实现缓存清理任务
为了避免 cache_entries 表无限增长,我们需要一个后台任务来定期删除所有过期的缓存条目。
// src/cache/cache-cleanup.service.ts
import { Injectable, Logger } from '@nestjs/common';
import { Cron, CronExpression } from '@nestjs/schedule';
import { PrismaService } from '../prisma/prisma.service';
@Injectable()
export class CacheCleanupService {
private readonly logger = new Logger(CacheCleanupService.name);
constructor(private readonly prisma: PrismaService) {}
// 每小时运行一次,清理所有过期的缓存条目
@Cron(CronExpression.EVERY_HOUR)
async handleCron() {
this.logger.log('Starting cache cleanup job...');
try {
const { count } = await this.prisma.cacheEntry.deleteMany({
where: {
expiresAt: {
lte: new Date(), // 小于或等于当前时间的条目都已过期
},
},
});
this.logger.log(`Cache cleanup job finished. Deleted ${count} expired entries.`);
} catch (error) {
this.logger.error(`Cache cleanup job failed: ${error.message}`, error.stack);
}
}
}
将 CacheCleanupService 添加到 CacheModule 的 providers 中:
// src/cache/cache.module.ts
import { Module, Global } from '@nestjs/common';
import { CacheService } from './cache.service';
import { PrismaModule } from '../prisma/prisma.module';
import { CacheCleanupService } from './cache-cleanup.service'; // 导入清理服务
@Global()
@Module({
imports: [PrismaModule],
providers: [CacheService, CacheCleanupService], // 添加清理服务
exports: [CacheService],
})
export class CacheModule {}
总结与注意事项
何时选择 PostgreSQL 作为缓存:
- 你已经在使用 PostgreSQL,并且不想引入新的技术栈(如 Redis)。
- 缓存的数据量不大,且对访问延迟要求不高。
- 你需要缓存与主业务数据严格的事务一致性(虽然在大多数缓存场景下不常见)。
- 内存资源有限,无法为 Redis 分配大量内存,而 PostgreSQL 已经在使用现有内存。
何时不选择 PostgreSQL 作为缓存(并考虑 Redis):
- 高性能/低延迟要求: 如果你的应用需要极低的缓存访问延迟和极高的吞吐量,Redis 几乎总是更好的选择。
- 高并发写入: PostgreSQL 的写入操作通常比 Redis 慢,特别是在面对大量小的写入时。
- 复杂数据结构: Redis 提供 Lists, Sets, Hashes, Sorted Sets 等丰富的数据结构,这在某些缓存场景中非常有用。PostgreSQL 的
JSONB类型可以模拟一些,但不如 Redis 原生支持高效。 - 大规模数据或集群: Redis 提供了分片和集群功能,可以轻松扩展到处理非常大的数据集和高并发。PostgreSQL 的扩展通常更复杂。
- 发布/订阅模式: Redis 内置了 Pub/Sub 功能,适用于实时通信和事件驱动架构。
这种基于 PostgreSQL 的缓存方案是一个可行的权宜之计,但在评估其性能和可伸缩性时务必谨慎。对于生产环境中的高性能缓存需求,Redis 或其他专门的缓存系统通常是更优选。