过期时间与 Cache-Aside 缓存
TTL 是缓存设计的核心。没有过期时间的缓存可能永久占用内存,也可能长期返回旧数据。
设置过期时间
await client.set("nloop:verification:13800138000", "482913", {
EX: 300,
});
EX: 300 表示 300 秒后过期。也可以使用毫秒级 PX。
只在键不存在时写入:
const result: string | null = await client.set(
"nloop:idempotency:request-123",
"processing",
{
EX: 60,
NX: true,
},
);
const acquired: boolean = result === "OK";
查看 TTL
const ttlSeconds: number = await client.ttl("nloop:verification:13800138000");
常见返回值:
- 正数:剩余秒数。
-1:键存在,但没有过期时间。-2:键不存在。
Cache-Aside 模式
Cache-Aside 也叫旁路缓存,是最常见的缓存模式:
- 先查 Redis。
- 命中则返回。
- 未命中则查数据库。
- 将结果写入 Redis,并设置 TTL。
- 返回结果。
interface UserDto {
id: number;
name: string;
email: string;
}
async function findUserFromDatabase(id: number): Promise<UserDto | null> {
// 示例占位:真实项目在这里查询 MySQL。
return { id, name: "Alice", email: "alice@example.com" };
}
async function getUser(id: number): Promise<UserDto | null> {
const key: string = `nloop:user-cache:${id}`;
const cached: string | null = await client.get(key);
if (cached !== null) {
return JSON.parse(cached) as UserDto;
}
const user: UserDto | null = await findUserFromDatabase(id);
if (user === null) {
return null;
}
await client.set(key, JSON.stringify(user), {
EX: 300,
});
return user;
}
as UserDto 只能帮助 TypeScript 编译,不能验证 Redis 中的真实数据。重要业务应使用运行时校验函数或 schema 库。
数据更新时怎么办
常见策略是“先更新数据库,再删除缓存”:
async function updateUserName(id: number, name: string): Promise<void> {
await updateUserNameInDatabase(id, name);
await client.del(`nloop:user-cache:${id}`);
}
下一个读取请求会从数据库重新构建缓存。
为什么通常删除而不是直接更新缓存?因为同一个数据库记录可能对应多个缓存视图,直接更新容易漏字段或漏键。
缓存穿透
大量请求查询不存在的数据,每次都落到数据库。可以短暂缓存“空结果”:
const notFoundValue: string = "__NOT_FOUND__";
await client.set(key, notFoundValue, { EX: 30 });
空值 TTL 应比正常数据短,避免真实数据创建后仍被误判不存在。
缓存击穿
一个热点键失效时,大量请求同时查询数据库。常见缓解方式:
- TTL 加随机抖动。
- 使用短期互斥标记,只允许一个请求重建。
- 逻辑过期并后台刷新。
TTL 抖动示例:
const baseTtl: number = 300;
const jitter: number = Math.floor(Math.random() * 60);
await client.set(key, value, { EX: baseTtl + jitter });
缓存雪崩
大量键在同一时刻过期,导致请求集中落到数据库。避免给同一批键设置完全相同的过期时间,并为数据库准备限流和降级措施。
最佳实践
- 缓存不是权威数据源,必须支持重建。
- TTL 应根据业务允许的陈旧时间决定,而不是统一写成一个数字。
- 不缓存含敏感信息的完整数据库对象。
- 记录缓存命中、未命中、错误和重建耗时。
- Redis 故障时是否回源数据库,要考虑数据库能否承受流量。