缓存一致性方案

面试官问:"更新数据时,Redis 和数据库的更新顺序是什么?"

小陈说:"先更新数据库,再更新缓存。"

面试官追问:"如果更新数据库后 Redis 挂了,更新缓存失败了,怎么办?"

小陈说:"...重试?"

面试官继续追问:"重试失败呢?缓存和数据库不就数据不一致了?"

小陈答不上来。

缓存一致性是 Redis 面试中的高频问题。这道题能说清楚 Cache Aside 模式、延迟双删的候选人,对 Redis 和数据库的协同有实战理解。

一、三种缓存模式 🔴

1.1 Cache Aside(旁路缓存)

读流程:
请求 → 查缓存(hit) → 返回数据 ✅
请求 → 查缓存(miss) → 查数据库 → 回填缓存 → 返回数据 ✅

写流程:
更新数据库 → 删除缓存(不是更新缓存)→ 返回

为什么写时删除而不是更新?

# ❌ 更新缓存的问题:
# T1: 更新数据库 age=26
# T2: 更新缓存 age=26(但 T1 和 T2 之间可能有其他操作)
# T3: 查询:缓存 age=26 ← 正确

# 但如果顺序错了:
# T1: 更新缓存 age=26
# T2: 更新数据库 age=26(延迟或其他原因)
# T3: 查询:缓存 age=26,数据库 age=25 ← 数据不一致!

# ✅ 删除缓存:
# T1: 更新数据库 age=26
# T2: 删除缓存
# T3: 查询:缓存 miss → 查数据库 → 回填缓存 age=26 ← 正确

1.2 Read/Write Through

模式说明
Read Through缓存负责读取,miss 时缓存自动查数据库
Write Through写操作同时写缓存和数据库

1.3 ❌ 错误示范

候选人原话:"先更新 Redis,再更新数据库。"

问题诊断:如果 Redis 更新成功但数据库更新失败,就会导致缓存是脏数据,且无法通过重新查询来修复。

候选人原话 2:"更新时同时更新 Redis 和数据库。"

问题诊断:并发场景下,如果 Redis 和数据库更新顺序不同步,可能导致数据不一致。

【面试官心理】 这道题我会从"为什么删除而不是更新"追问。能说清楚删除缓存的"安全窗口"概念的候选人,说明他理解了并发场景下的一致性问题。

二、延迟双删 🟡

2.1 延迟双删的流程

# 延迟双删:解决删除缓存后、数据库更新前,其他请求回填了旧缓存的问题
def update_user(user_id, new_data):
    # Step 1: 删除缓存
    redis.delete(f'user:{user_id}')

    # Step 2: 更新数据库
    db.execute(f'UPDATE users SET ... WHERE id={user_id}')

    # Step 3: 延迟一段时间后,再次删除缓存
    time.sleep(0.5)  # 等待所有正在读的操作完成
    redis.delete(f'user:{user_id}')

2.2 延迟双删的时序

问题场景(不用延迟双删):
T1: 更新数据库 age=26
T2: 删除缓存
T3: 请求 A 查缓存 miss → 查数据库 age=25 → 回填缓存 age=25 ❌
    (此时缓存是旧数据)

延迟双删解决:
T1: 更新数据库 age=26
T2: 删除缓存
T3: 请求 A 查缓存 miss → 查数据库 age=25 → 回填缓存 age=25
T4: 延迟 0.5s 后删除缓存
T5: 请求 B 查缓存 miss → 查数据库 age=26 → 回填缓存 age=26 ✅

2.3 延迟时间的设置

# 延迟时间 = 最大业务读取耗时
# 如果一个读操作最多 200ms,就延迟 300ms~500ms
time.sleep(0.3)  # 300ms

三、删除 vs 更新 🟡

3.1 为什么缓存通常删除而不是更新?

-- 删除缓存的优点:
-- 1. 删除操作是幂等的,多次删除没问题
-- 2. 即使删除失败,下次查询会重新从数据库加载
-- 3. 避免并发下的数据不一致

-- 更新缓存的问题:
-- 1. 非幂等,更新多次可能结果不同
-- 2. 如果更新缓存成功但更新数据库失败,缓存就是脏数据
-- 3. 缓存更新和数据库更新不是原子的

3.2 异步更新方案

# 异步更新:使用消息队列
def update_user(user_id, new_data):
    # Step 1: 更新数据库
    db.execute(f'UPDATE users SET ... WHERE id={user_id}')

    # Step 2: 发送消息到队列
    mq.send('user_update', {'user_id': user_id})

    # Step 3: 消费者异步更新缓存
    # consumer:
    # msg = mq.receive()
    # redis.setex(f'user:{msg.user_id}', ttl, msg.data)

四、生产避坑 🟢

4.1 删除缓存失败的补偿

# 缓存删除失败后的补偿机制:
# 1. 重试机制(消息队列)
def update_user(user_id, new_data):
    db.execute(f'UPDATE users SET ...')
    # 发送删除消息到队列
    mq.send('cache_delete', {'key': f'user:{user_id}'})

# consumer:
def consume():
    msg = mq.receive()
    redis.delete(msg['key'])
    # 如果删除失败,重新放回队列
    if failed:
        mq.send('cache_delete', msg)

4.2 缓存与数据库的超时设置

# 缓存 TTL 不要设置过长
# 如果 TTL 过长,数据库更新后缓存需要很久才能同步
# 建议:TTL = 正常业务缓存时间 + 一定余量

# 短 TTL 的好处:
# 1. 即使不一致,也能快速自动修复
# 2. 节省内存
# 3. 配合延迟双删,基本不会不一致

【面试官心理】 缓存一致性是 Redis 面试中的高级话题。能说清楚 Cache Aside、延迟双删的时序问题的候选人,说明他对分布式系统的一致性问题有深入理解。


级别考察重点期望回答
P5基本模式Cache Aside 读/写流程
P6深入方案延迟双删、为什么删除而不是更新
P7工程实践删除失败补偿、异步更新