1、缓存基础概念
| 概念 | 说明 |
|---|---|
| 缓存命中/未命中 | 命中:请求数据存在于缓存;未命中:需从数据源加载并缓存。 |
| TTL(生存时间) | 缓存数据的有效期,超时后自动失效,避免脏数据。 |
| 缓存穿透 | 频繁查询不存在的数据,绕过缓存直达数据库。解决方案:缓存空值或布隆过滤器。 |
| 缓存雪崩 | 大量缓存在同一时间失效,引发数据库瞬时高负载。解决方案:随机化 TTL。 |
| 缓存击穿 | 热点数据失效后,高并发请求穿透到数据库。解决方案:互斥锁或永不过期 + 异步更新。 |
2、缓存策略对比
2.1、 自动缓存(CacheInterceptor)
- 优点:快速集成、代码简洁、统一策略。
- 缺点:灵活性低、仅限 GET 请求、无法精细控制失效。
- 适用场景:简单高频读取接口(如公共配置)。
2.2、 手动缓存(CacheManager)
- 优点:完全控制键/值、支持复杂逻辑、可操作任意请求类型。
- 缺点:代码侵入性强、维护成本高。
- 适用场景:动态参数查询、高频写操作场景(如订单系统)。
2.3、 分页缓存
- 键设计:
page:2:sort:date:filter:active(参数动态组合)。 - 失效策略:事件驱动精准删除或版本号兜底。
- 挑战:多条件组合导致缓存键爆炸,需结合业务优化。
3、Redis 高级功能应用
| 功能 | 用途 | 示例场景 |
|---|---|---|
| 发布订阅 | 跨服务/实例缓存同步 | 订单删除后通知所有节点清理分页缓存 |
| Pipeline | 批量执行命令,减少网络往返 | 批量清理匹配的缓存键 |
| Lua 脚本 | 原子操作复杂逻辑 | 库存扣减后刷新商品列表缓存 |
| SCAN 命令 | 安全遍历大量键(替代 KEYS) | 按模式查找需失效的分页缓存键 |
4、缓存键设计规范
- 结构化命名
模块:业务:参数1=值1:参数2=值2(如order:list:supplier=A:page=1)。 - Hash Tag 优化
user:{id=123}:profile确保相同业务的键在 Redis 集群中分布在同一 Slot。 - 版本号控制
order:v2:page=1数据变更时递增版本,旧键自动淘汰。 - 参数归一化
将连续值(如日期)转换为范围(如按周),减少键数量。
5、缓存失效策略
| 策略 | 实现方式 | 适用场景 |
|---|---|---|
| TTL 自动过期 | 设置合理的生存时间 | 数据变更频率低的场景 |
| 事件驱动精准失效 | 通过 Redis Pub/Sub 通知关联缓存失效 | 分布式系统、多条件组合分页缓存 |
| 版本号全局失效 | 更新时递增版本号,旧版本缓存自然过期 | 高频写操作且条件组合固定的场景 |
| 定时任务补偿 | 定时扫描数据库,修复缓存不一致 | 兜底方案,防止事件丢失或逻辑漏洞 |
6、最佳实践清单
6.1、分层缓存
- L1 本地缓存(高频热点数据)
- L2 Redis 缓存(共享数据)
- L3 数据库(持久化存储)。
6.2、监控指标
- 缓存命中率(>90% 为健康)
- 平均加载时间
- 内存使用率(避免超过 70%)。
6.3、代码规范
// 示例:封装缓存服务
@Injectable()
export class CacheService {
constructor(@Inject(CACHE_MANAGER) private cache: Cache) {}
async getOrSet<T>(key: string, loader: () => Promise<T>, ttl: number): Promise<T> {
const cached = await this.cache.get<T>(key);
if (cached) return cached;
const data = await loader();
await this.cache.set(key, data, ttl);
return data;
}
}
6.4、避免全量缓存陷阱
- 仅缓存热数据或分页结果,禁止缓存大表全量数据。
6.5、防御性设计
- 对缓存操作添加熔断机制(如 Redis 超时自动降级)。
- 缓存空值(
NULL)防止穿透,设置较短 TTL(如 30 秒)。
6.6、实际业务缓存策略
- 基础数据变化小的才进行数据缓存:象用户信息、字典类信息可以缓存,象订单这样的业务单据因为都是分页查询,且查询自定义条件复杂,不适合做缓存处理,使用数据库的索引达到高效查询的目标。
- 尽量不要使用自动缓存:自动缓存往往带来一些不可预测的结果,所以一般不要使用自动缓存。
- 对流量集中爆发的业务进行缓存精细化设计:拼团、秒杀等业务在缓存上需要仔细设计,防止缓存击穿、缓存雪崩导致业务卡顿、数量超限等问题,必要的时候可以放宽一些要求,比如秒杀100个,超个10个8个在允许范围内。
7、工具与库推荐
- NestJS 集成:
@nestjs/cache-manager+cache-manager-redis-store。 - Redis 客户端:
ioredis(支持集群、Pipeline、Lua)。 - 监控工具:RedisInsight(可视化监控)、Prometheus + Grafana(指标报警)。
- 性能测试:
redis-benchmark(压测工具)。
8、缓存实践
目标:实现部门表的缓存,缓存策略是:新增、编辑、删除部门时清除所有数据缓存,在单个查询时使用缓存,保证下一次查询单个部门信息时使用缓存的数据,分页查询因为条件复杂就不使用缓存。该策略适合部门这种变化频率不高的数据。 程序设计: 1)缓存是程序基础框架的一部分,所以我们单独把redis的处理放在一个模块中, 目录结构如下:
- src
- core
- redis
- redis.module.ts
- redis.service.ts
- core.module.ts 2)使用注解方式实现缓存清理和缓存。
- redis
- core
步骤1、安装依赖
pnpm i @songkeys/nestjs-redis ioredis
步骤2、新建模块和服务
nest g mo core/redis
nest g s core/redis --no-spec
步骤3、实现redis.service
这是redis相关的操作,直接拷贝下面代码就可以了。
import { InjectRedis } from '@songkeys/nestjs-redis';
import { Injectable } from '@nestjs/common';
import Redis from 'ioredis';
@Injectable()
export class RedisService {
constructor(@InjectRedis() private readonly client: Redis) {}
getClient(): Redis {
return this.client;
}
/**
* redis基本信息
* @returns
*/
async getInfo() {
// 连接到 Redis 服务器
const rawInfo = await this.client.info();
// 按行分割字符串
const lines = rawInfo.split('\r\n');
const parsedInfo = {};
// 遍历每一行并分割键值对
lines.forEach((line) => {
const [key, value] = line.split(':');
parsedInfo[key?.trim()] = value?.trim();
});
return parsedInfo;
}
/**
* 分页查询缓存数据
* @param data
* @returns
*/
async skipFind(data: { key: string; pageSize: number; pageNum: number }) {
const rawInfo = await this.client.lrange(data.key, (data.pageNum - 1) * data.pageSize, data.pageNum * data.pageSize);
return rawInfo;
}
/**
* 缓存Key数量
* @returns
*/
async getDbSize() {
return await this.client.dbsize();
}
/**
* 命令统计
* @returns
*/
async commandStats() {
const rawInfo = await this.client.info('commandstats');
// 按行分割字符串
const lines = rawInfo.split('\r\n');
const commandStats = [];
// 遍历每一行并分割键值对
lines.forEach((line) => {
const [key, value] = line.split(':');
if (key && value) {
commandStats.push({
name: key?.trim()?.replaceAll('cmdstat_', ''),
value: +value?.trim()?.split(',')[0]?.split('=')[1],
});
}
});
return commandStats;
}
/* --------------------- string 相关 -------------------------- */
/**
*
* @param key 存储 key 值
* @param val key 对应的 val
* @param ttl 可选,过期时间,单位 毫秒
*/
async set(key: string, val: any, ttl?: number): Promise<'OK' | null> {
const data = JSON.stringify(val);
if (!ttl) return await this.client.set(key, data);
return await this.client.set(key, data, 'PX', ttl);
}
async mget(keys: string[]): Promise<any[]> {
if (!keys) return null;
const list = await this.client.mget(keys);
return list.map((item) => JSON.parse(item));
}
/**
* 返回对应 value
* @param key
*/
async get(key: string): Promise<any> {
if (!key || key === '*') return null;
const res = await this.client.get(key);
return JSON.parse(res);
}
async del(keys: string | string[]): Promise<number> {
if (!keys || keys === '*') return 0;
if (typeof keys === 'string') keys = [keys];
return await this.client.del(...keys);
}
async ttl(key: string): Promise<number | null> {
if (!key) return null;
return await this.client.ttl(key);
}
/**
* 获取对象keys
* @param key
*/
async keys(key?: string) {
return await this.client.keys(key);
}
/* ----------------------- hash ----------------------- */
/**
* hash 设置 key 下单个 field value
* @param key
* @param field 属性
* @param value 值
*/
async hset(key: string, field: string, value: string): Promise<string | number | null> {
if (!key || !field) return null;
return await this.client.hset(key, field, value);
}
/**
* hash 设置 key 下多个 field value
* @param key
* @param data
* @params expire 单位 秒
*/
async hmset(key: string, data: Record<string, string | number | boolean>, expire?: number): Promise<number | any> {
if (!key || !data) return 0;
const result = await this.client.hmset(key, data);
if (expire) {
await this.client.expire(key, expire);
}
return result;
}
/**
* hash 获取单个 field 的 value
* @param key
* @param field
*/
async hget(key: string, field: string): Promise<number | string | null> {
if (!key || !field) return 0;
return await this.client.hget(key, field);
}
/**
* hash 获取 key 下所有field 的 value
* @param key
*/
async hvals(key: string): Promise<string[]> {
if (!key) return [];
return await this.client.hvals(key);
}
async hGetAll(key: string): Promise<Record<string, string>> {
return await this.client.hgetall(key);
}
/**
* hash 删除 key 下 一个或多个 fields value
* @param key
* @param fields
*/
async hdel(key: string, fields: string | string[]): Promise<string[] | number> {
if (!key || fields.length === 0) return 0;
return await this.client.hdel(key, ...fields);
}
/**
* hash 删除 key 下所有 fields value
* @param key
*/
async hdelAll(key: string): Promise<string[] | number> {
if (!key) return 0;
const fields = await this.client.hkeys(key);
if (fields.length === 0) return 0;
return await this.hdel(key, fields);
}
/* ----------- list 相关操作 ------------------ */
/**
* 获取列表长度
* @param key
*/
async lLength(key: string): Promise<number> {
if (!key) return 0;
return await this.client.llen(key);
}
/**
* 通过索引设置列表元素的值
* @param key
* @param index
* @param val
*/
async lSet(key: string, index: number, val: string): Promise<'OK' | null> {
if (!key || index < 0) return null;
return await this.client.lset(key, index, val);
}
/**
* 通过索引获取 列表中的元素
* @param key
* @param index
*/
async lIndex(key: string, index: number): Promise<string | null> {
if (!key || index < 0) return null;
return await this.client.lindex(key, index);
}
/**
* 获取列表指定范围内的元素
* @param key
* @param start 开始位置, 0 是开始位置
* @param stop 结束位置, -1 返回所有
*/
async lRange(key: string, start: number, stop: number): Promise<string[] | null> {
if (!key) return null;
return await this.client.lrange(key, start, stop);
}
/**
* 将一个或多个值插入到列表头部
* @param key
* @param val
*/
async lLeftPush(key: string, ...val: string[]): Promise<number> {
if (!key) return 0;
return await this.client.lpush(key, ...val);
}
/**
* 将一个值或多个值插入到已存在的列表头部
* @param key
* @param val
*/
async lLeftPushIfPresent(key: string, ...val: string[]): Promise<number> {
if (!key) return 0;
return await this.client.lpushx(key, ...val);
}
/**
* 如果 pivot 存在,则在 pivot 前面添加
* @param key
* @param pivot
* @param val
*/
async lLeftInsert(key: string, pivot: string, val: string): Promise<number> {
if (!key || !pivot) return 0;
return await this.client.linsert(key, 'BEFORE', pivot, val);
}
/**
* 如果 pivot 存在,则在 pivot 后面添加
* @param key
* @param pivot
* @param val
*/
async lRightInsert(key: string, pivot: string, val: string): Promise<number> {
if (!key || !pivot) return 0;
return await this.client.linsert(key, 'AFTER', pivot, val);
}
/**
* 在列表中添加一个或多个值
* @param key
* @param val
*/
async lRightPush(key: string, ...val: string[]): Promise<number> {
if (!key) return 0;
return await this.client.lpush(key, ...val);
}
/**
* 为已存在的列表添加一个或多个值
* @param key
* @param val
*/
async lRightPushIfPresent(key: string, ...val: string[]): Promise<number> {
if (!key) return 0;
return await this.client.rpushx(key, ...val);
}
/**
* 移除并获取列表第一个元素
* @param key
*/
async lLeftPop(key: string): Promise<string> {
if (!key) return null;
const result = await this.client.blpop(key);
return result.length > 0 ? result[0] : null;
}
/**
* 移除并获取列表最后一个元素
* @param key
*/
async lRightPop(key: string): Promise<string> {
if (!key) return null;
const result = await this.client.brpop(key);
return result.length > 0 ? result[0] : null;
}
/**
* 对一个列表进行修剪(trim),就是说,让列表只保留指定区间内的元素,不在指定区间之内的元素都将被删除
* @param key
* @param start
* @param stop
*/
async lTrim(key: string, start: number, stop: number): Promise<'OK' | null> {
if (!key) return null;
return await this.client.ltrim(key, start, stop);
}
/**
* 移除列表元素
* @param key
* @param count
* count > 0 :从表头开始向表尾搜索,移除与 value 相等的元素,数量为 count;
* count < 0 :从表尾开始向表头搜索,移除与 value 相等的元素,数量为 count 的绝对值;
* count = 0 : 移除表中所有与 value 相等的值
* @param val
*/
async lRemove(key: string, count: number, val: string): Promise<number> {
if (!key) return 0;
return await this.client.lrem(key, count, val);
}
/**
* 移除列表最后一个元素,并将该元素添加到另一个裂膏并返回
* 如果列表没有元素会阻塞队列直到等待超时或发现可弹出元素为止
* @param sourceKey
* @param destinationKey
* @param timeout
*/
async lPoplPush(sourceKey: string, destinationKey: string, timeout: number): Promise<string> {
if (!sourceKey || !destinationKey) return null;
return await this.client.brpoplpush(sourceKey, destinationKey, timeout);
}
/**
* 删除全部缓存
* @returns
*/
async reset() {
const keys = await this.client.keys('*');
return this.client.del(keys);
}
}
步骤4、实现redis.module
在模块中实现forRoot、forRootAsync静态方法,方便在core.module中引入。
import { RedisModule as customRedisModule, RedisModuleAsyncOptions } from '@songkeys/nestjs-redis';
import { DynamicModule, Global, Module } from '@nestjs/common';
import { RedisService } from './redis.service';
@Global()
@Module({
providers: [RedisService],
exports: [RedisService],
})
export class RedisModule {
static forRoot(options: RedisModuleAsyncOptions, isGlobal = true): DynamicModule {
return {
module: RedisModule,
imports: [customRedisModule.forRootAsync(options, isGlobal)],
providers: [RedisService],
exports: [RedisService],
};
}
static forRootAsync(options: RedisModuleAsyncOptions, isGlobal = true): DynamicModule {
return {
module: RedisModule,
imports: [customRedisModule.forRootAsync(options, isGlobal)],
providers: [RedisService],
exports: [RedisService],
};
}
}
步骤5、配置core.module,增加如下代码:
// src/core/core.module.ts
import { Global, Module } from '@nestjs/common';
import { WinstonModule, utilities } from 'nest-winston';
import * as winston from 'winston';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { RedisService } from './redis/redis.service';
import { RedisModule } from './redis/redis.module';
import 'winston-daily-rotate-file';
import { RedisClientOptions } from '@songkeys/nestjs-redis';
function createDailyRotateTransport(level: string, filename: string,maxSize:string="20m",maxFiles:string="14d") {
return new winston.transports.DailyRotateFile({
level,
dirname: 'logs', //日志文件夹
auditFile: 'false', // 关键配置:关闭审计文件
filename: `${filename}-%DATE%.log`, //日志名称,占位符 %DATE% 取值为 datePattern 值
datePattern: 'YYYY-MM-DD', //日志轮换的频率,此处表示每天。其他值还有:YYYY-MM、YYYY-MM-DD-HH、YYYY-MM-DD-HH-mm
zippedArchive: true, //是否通过压缩的方式归档被轮换的日志文件
maxSize, // 设置日志文件的最大大小,m 表示 mb 。
maxFiles, // 保留日志文件的最大天数,此处表示自动删除超过 14 天的日志文件
format: winston.format.combine(
winston.format.timestamp({
format: 'YYYY-MM-DD HH:mm:ss',
}),
winston.format.simple(),
),
});
}
@Global()
@Module({
imports: [
RedisModule.forRootAsync(
{
imports: [ConfigModule],
inject: [ConfigService],
useFactory: (config: ConfigService) => {
return {
closeClient: true,
readyLog: true,
errorLog: true,
config: {
host: config.get<string>('REDIS_HOST'),
port: config.get<number>('REDIS_PORT'),
password: config.get<string>('REDIS_PASSWORD'),
db: config.get<number>('REDIS_DB'),
}//config.get<RedisClientOptions>('redis'),
};
},
},
true,
),
// 动态初始化 Winston 异步方式
WinstonModule.forRootAsync({
inject: [ConfigService], // 注入配置服务
useFactory: (configService: ConfigService) => {
//取到配置项 LOG_MAX_SIZE=104857600 # 100MB LOG_MAX_FILES=14 LOG_ON= true LOG_LEVEL=debug
const logOn = configService.get('LOG_ON');
const logFileName= configService.get('LOG_FILE_NAME');
const logLevel = configService.get('LOG_LEVEL');
const logMaxSize = configService.get('LOG_MAX_SIZE');
const logMaxFiles = configService.get('LOG_MAX_FILES');
const nodeEnv=configService.get('NODE_ENV');
return {
// 公共日志格式
format: winston.format.combine(
winston.format.timestamp(),
winston.format.json()
),
// 动态传输配置
transports: [
// 开发环境控制台输出
...(nodeEnv === 'development'
? [new winston.transports.Console({
level: logLevel,
format: winston.format.combine(
winston.format.colorize(),
winston.format.simple() //简单日志格式
)
})]
: []),
// 文件输出
...(logOn
? [
createDailyRotateTransport(logLevel,logFileName,logMaxSize,logMaxFiles),
//错误日志强制输出 默认20兆 保留14天
createDailyRotateTransport("error","error"),
]
: []),
]
}
}
}),
RedisModule
],
providers: [RedisService],
exports: [WinstonModule] // 关键:导出初始化后的Winston模块
})
export class CoreModule {}
步骤6、创建缓存注解
执行nest g d common/decorators/redis创建文件。编写缓存清除和缓存函数:
import { Inject } from '@nestjs/common';
import { paramsKeyFormat } from 'src/common/utils/decorator';
import { RedisService } from 'src/core/redis/redis.service';
export function CacheEvict(CACHE_NAME: string, CACHE_KEY: string) {
const injectRedis = Inject(RedisService);
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
injectRedis(target, 'redis');
const originMethod = descriptor.value;
descriptor.value = async function (...args: any[]) {
const key = paramsKeyFormat(originMethod, CACHE_KEY, args);
if (key === '*') {
const res = await this.redis.keys(`${CACHE_NAME}*`);
if (res.length) {
await this.redis.del(res);
}
} else if (key !== null) {
await this.redis.del(`${CACHE_NAME}${key}`);
} else {
await this.redis.del(`${CACHE_NAME}${CACHE_KEY}`);
}
return await originMethod.apply(this, args);
};
};
}
export function Cacheable(CACHE_NAME: string, CACHE_KEY: string, CACHE_EXPIRESIN?: number) {
const injectRedis = Inject(RedisService);
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
injectRedis(target, 'redis');
const originMethod = descriptor.value;
descriptor.value = async function (...args: any[]) {
const key = paramsKeyFormat(originMethod, CACHE_KEY, args);
if (key === null) {
return await originMethod.apply(this, args);
}
const cacheResult = await this.redis.get(`${CACHE_NAME}${key}`);
if (!cacheResult) {
const result = await originMethod.apply(this, args);
await this.redis.set(`${CACHE_NAME}${key}`, result, CACHE_EXPIRESIN);
return result;
}
return cacheResult;
};
};
}
CacheEvict程序解释: 装饰器函数,用于清除缓存。
函数参数
CACHE_NAME:缓存的名称,用于标识缓存的范围或类别。CACHE_KEY:缓存的键,用于指定要清除的缓存项。
函数主体
-
const injectRedis = Inject(RedisService);:使用Inject函数注入RedisService,以便在目标对象中使用 Redis 客户端。 -
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor):返回一个装饰器函数,该函数接收三个参数:target:目标对象,通常是类的原型。propertyKey:目标方法的名称。descriptor:目标方法的属性描述符,用于描述目标方法的特性。
装饰器函数内部
injectRedis(target, 'redis');:将 Redis 客户端注入到目标对象的redis属性中,这样在目标方法中就可以通过this.redis来访问 Redis 客户端。const originMethod = descriptor.value;:保存目标方法的原始实现,以便在清除缓存后调用原始方法。descriptor.value = async function (...args: any[]):替换目标方法的实现为一个新的异步函数,该函数会在清除缓存后调用原始方法。
新的异步函数内部
const key = paramsKeyFormat(originMethod, CACHE_KEY, args);:调用paramsKeyFormat函数生成缓存键,该函数通常会根据目标方法的参数和CACHE_KEY来生成唯一的缓存键。if (key === '*'):如果生成的缓存键为'*',则清除以CACHE_NAME开头的所有缓存项。else if (key !== null):如果生成的缓存键不为null,则清除以CACHE_NAME和缓存键拼接成的缓存项。else:如果生成的缓存键为null,则使用CACHE_NAME和CACHE_KEY拼接成的缓存键来清除缓存项。return await originMethod.apply(this, args);:调用原始方法并返回结果,确保在清除缓存后仍然执行目标方法的原始逻辑。
总结
该 CacheEvict 装饰器函数的作用是在目标方法执行前,根据指定的 CACHE_NAME 和 CACHE_KEY 生成缓存键,并使用 Redis 客户端清除相应的缓存项,从而实现对缓存的管理。在清除缓存后,它会调用目标方法的原始实现,以确保业务逻辑的正常执行。
步骤7、根据接口参数自动生成缓存key
在common/utils目录下新建一个工具类decorator.ts, 注意需要安装依赖包:
pnpm i lodash
代码如下:
import { get } from 'lodash';
//如果函数是 function example(a, b, c) {},getArgs(example) 将返回 ['a', 'b', 'c']。
function getArgs(func) {
const funcString = func.toString();
return funcString.slice(funcString.indexOf('(') + 1, funcString.indexOf(')')).match(/([^\s,]+)/g);
}
const stringFormat = (str: string, callback: (key: string) => string): string => {
return str.replace(/\{([^}]+)\}/g, (word, key) => callback(key));
};
/**
function getUser(name, id) {}
const cacheKey = paramsKeyFormat(getUser, "user:{name}_{id}", ["Alice", 123]);
console.log(cacheKey); // 输出:user:Alice_123
*/
export function paramsKeyFormat(func: () => any, formatKey: string, args: any[]) {
const originMethodArgs = getArgs(func);
const paramsMap = {};
originMethodArgs?.forEach((arg, index) => {
paramsMap[arg] = args[index];
});
let isNotGet = false;
const key = stringFormat(formatKey, (key) => {
const str = get(paramsMap, key);
if (!str) isNotGet = true;
return str;
});
if (isNotGet) {
return null;
}
return key;
}
export function paramsKeyGetObj(func: () => any, formatKey: string | undefined, args: any[]): any {
const originMethodArgs = getArgs(func);
const paramsMap = {};
originMethodArgs?.forEach((arg, index) => {
paramsMap[arg] = args[index];
});
const obj = get(paramsMap, formatKey);
if (typeof obj === 'object') return obj;
if (args[0] && typeof args[0] === 'object') return args[0];
return null;
}
步骤8、使用缓存
在test.service.ts中处理缓存,代码如下:
import { Injectable } from '@nestjs/common';
import { CreateTestDto } from './dto/create-test.dto';
import { UpdateTestDto } from './dto/update-test.dto';
import { InjectRepository } from '@nestjs/typeorm';
import { DataSource, Repository } from 'typeorm';
import { TestEntity } from './entities/test.entity';
import { CacheEvict, Cacheable } from 'src/common/decorators/redis/redis.decorator';
const cacheKey="test"
@Injectable()
export class TestService {
constructor(
@InjectRepository(TestEntity)
private testRepository: Repository<TestEntity>,
private dataSource: DataSource
){}
@CacheEvict(cacheKey, '*')//新增数据后清理所有该键值相关的缓存
create(createTestDto: CreateTestDto) {
this.testRepository.save(createTestDto);
return '保存成功';
}
@Cacheable(cacheKey, '*')//缓存所有数据
findAll() {
const data=this.testRepository.find();
return data;
}
@Cacheable(cacheKey, 'findOne:{id}')//缓存一行数据
findOne(id: number) {
return this.testRepository.findOneBy({id});
}
@CacheEvict(cacheKey, '*')//新增数据后清理所有该键值相关的缓存
async createPatch() {
console.log('开始事务测试');
//测试使用事务
const queryRunner = this.dataSource.createQueryRunner();
await queryRunner.connect();
await queryRunner.startTransaction();
try {
const updateData1=new TestEntity();
updateData1.username='test1事务测试';
updateData1.password='test123';
await queryRunner.manager.save(updateData1);
const updateData2=new TestEntity();
updateData2.username='test2事务测试';
updateData2.password='test123';
await queryRunner.manager.save(updateData2);
await queryRunner.commitTransaction();
} catch (err) {
console.log('开始事务测试3',err.message);
// since we have errors lets rollback the changes we made
await queryRunner.rollbackTransaction();
} finally {
console.log('开始事务测试4');
// you need to release a queryRunner which was manually instantiated
await queryRunner.release();
}
return '事务测试'
}
@CacheEvict(cacheKey, '*')//删除数据后清理所有该键值相关的缓存
remove(id: number) {
return this.testRepository.delete(id);
}
}
测试: 注意:启动项目前先启动redis服务器,否则连不上服务器系统无法启动。 测试步骤: 1、调用查询接口,此时数据会被缓存 2、从后台直接修改数据库内容,再次调用查询接口,此时缓存内容未刷新,仍然是以前的内容 3、调用创建记录接口,创建一条新记录,此时缓存会被清除 4、再次调用查询接口,此时应该看到数据已刷新为最新数据
第一次调用findone接口:
从后台数据库修改第一行数据内容:
重新调用findone:
调用创建接口:
重新调用findone: