跳到主要内容

缓存与数据库一致性

问题

如何保证缓存和数据库的数据一致性?

面试速答版

如何保证缓存和数据库的数据一致性? 互联网场景 99% 选 Cache Aside(旁路缓存),追求最终一致:

  • :先查缓存 → 未命中查 DB → 回写缓存并设 TTL。
  • 先更新 DB、再删除缓存,不建议「更新缓存」(并发下会出现老值覆盖新值)。
  • 为什么是删除不是更新:删除是幂等、代价低,且避免了并发写的 lost update。
  • 进阶场景还有 Read/Write Through(应用只请求缓存层,缓存层同步 DB,强一致但缓存层实现复杂)和 Write Behind(异步批量刷 DB,高吞吐低一致)。

主要问题与解决方案

  • 并发不一致:A 读(未命中)查 DB → B 写 DB + 删缓存 → A 回填老值 → 缓存存老值。延迟双删:写后删一次,睡会儿之后再删一次,克服这个窗口。
  • 缓存穿透:查不存在的 key 总是打到 DB → 空值也缓存一下,或加布隆过滤器。
  • 缓存击穿:热点 key 过期瞬间大量请求压到 DB → 互斥锁 + 后台刷缓存。
  • 缓存雪崩:大量 key 同时失效 → TTL 加随机拖动、多级缓存、限流降级。
  • 推荐额外动作:设合理 TTL(充当底库)、重要缓存变更走消息队列同步避免丢失。

答案

一致性问题

当数据同时存在于数据库和缓存中时,更新操作可能导致两者不一致。

常见策略

策略一致性
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;
}
}
为什么是删除缓存而不是更新缓存?
  1. 更新缓存可能有并发问题(A 在 B 之前更新 DB 但在 B 之后写缓存)
  2. 缓存可能包含复杂计算结果,更新代价高
  3. 删除后 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: 强一致性场景怎么办?

答案

  1. 不用缓存,直接读数据库
  2. 使用分布式锁保证读写串行
  3. binlog + CDC 实时同步

相关链接