缓存与数据库一致性
问题
如何保证缓存和数据库的数据一致性?
答案
一致性问题
当数据同时存在于数据库和缓存中时,更新操作可能导致两者不一致。
常见策略
| 策略 | 读 | 写 | 一致性 |
|---|---|---|---|
| Cache Aside(旁路缓存) | 缓存命中返回;未命中查 DB 写缓存 | 更新 DB,删除缓存 | 最终一致 |
| Read/Write Through | 读写都通过缓存层 | 缓存层同步更新 DB | 强一致 |
| Write Behind | 同 Cache Aside | 缓存异步批量写 DB | 弱一致 |
Cache Aside(最常用)
cache-aside.ts
class UserService {
// 读:先查缓存 → 未命中查 DB → 写入缓存
async getUser(id: string): Promise<User> {
const cacheKey = `user:${id}`;
const cached = await redis.get(cacheKey);
if (cached) return JSON.parse(cached);
const user = await db.user.findUnique({ where: { id } });
if (user) {
await redis.set(cacheKey, JSON.stringify(user), 'EX', 3600);
}
return user;
}
// 写:先更新 DB → 再删除缓存
async updateUser(id: string, data: UpdateUserDto): Promise<User> {
// 1. 更新数据库
const user = await db.user.update({ where: { id }, data });
// 2. 删除缓存(下次读时重建)
await redis.del(`user:${id}`);
return user;
}
}
为什么是删除缓存而不是更新缓存?
- 更新缓存可能有并发问题(A 在 B 之前更新 DB 但在 B 之后写缓存)
- 缓存可能包含复杂计算结果,更新代价高
- 删除后 lazy load 更简单可靠
并发不一致问题
延迟双删
delayed-double-delete.ts
async function updateWithDoubleDelete(id: string, data: UpdateUserDto) {
const cacheKey = `user:${id}`;
// 1. 先删缓存
await redis.del(cacheKey);
// 2. 更新数据库
await db.user.update({ where: { id }, data });
// 3. 延迟再删一次缓存(覆盖期间可能被写入的脏数据)
setTimeout(async () => {
await redis.del(cacheKey);
}, 500); // 延迟 500ms,大于一次读请求的时间
}
基于消息队列的方案
mq-cache-invalidation.ts
// 写操作
async function updateUser(id: string, data: UpdateUserDto) {
await db.user.update({ where: { id }, data });
// 发消息到队列,由消费者负责删缓存
await messageQueue.publish('cache:invalidate', {
key: `user:${id}`,
retries: 3,
});
}
// 消费者
messageQueue.subscribe('cache:invalidate', async (msg) => {
await redis.del(msg.key);
});
常见面试问题
Q1: 先更新 DB 还是先删缓存?
答案:
推荐先更新 DB,再删缓存。 原因:
- 先删缓存再更新 DB:删缓存后、更新 DB 前,其他请求读到旧数据并写入缓存
- 先更新 DB 再删缓存:即使删缓存失败,下次缓存过期后也会读到新数据
Q2: 缓存过期时间怎么设?
答案:
- 变化频繁的数据:短 TTL(1-5 分钟)
- 变化不频繁:长 TTL(1-24 小时)
- 添加随机偏移防止缓存雪崩:
baseTime + random(0, 300)
Q3: 强一致性场景怎么办?
答案:
- 不用缓存,直接读数据库
- 使用分布式锁保证读写串行
- binlog + CDC 实时同步