Skip to content

05.2 - 缓存一致性方案

定位: 缓存与数据库之间的数据同步策略——从强一致到最终一致的权衡 面试高频度: ⭐⭐⭐⭐⭐ 考查方式: 更新缓存的策略选择、先删缓存还是后删、延迟双删


一、这是什么?为什么需要它?

是什么

缓存一致性是指 Redis 缓存中的数据与数据库(MySQL)中的数据保持一致的程度。

为什么需要解决一致性问题?


二、方案对比

2.1 四种更新策略

策略 1:先更新数据库,再删除缓存(Cache Aside ★推荐)写操作: ① UPDATE MySQL(先写库)→ ② DEL Redis(再删缓存)

读操作: ① GET Redis → 未命中 → ② SELECT MySQL → ③ SET Redis

为什么是删缓存不是更新缓存?

  • 删除简单(不需计算)
  • 下次读时自然构建新缓存
  • 避免"写-写"竞争(同一 key 两次写)

策略 2:先删除缓存,再更新数据库写操作: ① DEL Redis(先删缓存)→ ② UPDATE MySQL(后写库)

问题(并发读导致的脏数据): 线程A: DEL Redis 线程B: 读 → Redis 无 → MySQL(旧) → 设缓存 线程A: UPDATE MySQL(新) → 缓存中是旧数据!

策略 3:延迟双删写操作: ① DEL Redis(第一次删) ② UPDATE MySQL ③ sleep(500ms) # 等读请求可能设旧缓存 ④ DEL Redis(第二次删)

双删的目的: 第一次删 → 让读请求查 DB;等读请求完成 → 第二次删 → 清掉可能的旧缓存

问题: sleep 时间不好确定(50ms? 500ms? 5s?)

策略 4:异步更新(Canal ★高可靠) 应用只操作数据库,缓存由 Canal 同步更新: ① 应用 UPDATE MySQL → ② MySQL 记录 binlog → ③ Canal 监听 binlog 变更 → ④ Canal 更新 Redis 缓存

✓ 应用不需要关注缓存逻辑 ✓ binlog 保证不会丢 ✗ 引入消息队列 / Canal 额外组件

2.2 方案对比

方案一致性复杂度延迟适用场景
先删缓存后写库低(脏数据风险)容忍不一致
先写库后删缓存中(短暂不一致)大部分场景 ★
延迟双删中高中(sleep)对一致性要求略高
Canal 同步高(最终一致)低(秒级)高一致性要求
读写锁高(准强一致)关键数据

三、实战验证

3.1 Cache Aside 模式代码

python
import redis
import time

r = redis.Redis()

def get_user(id):
    """读:Cache Aside"""
    key = f"user:{id}"
    
    # 1. 查缓存
    user = r.get(key)
    if user is not None:
        return user
    
    # 2. 缓存未命中 → 查数据库
    user = db.query(f"SELECT * FROM user WHERE id={id}")
    
    if user is None:
        return None
    
    # 3. 写入缓存(带过期时间)
    r.setex(key, 3600, user)
    return user

def update_user(id, data):
    """写:先写库 → 后删缓存"""
    key = f"user:{id}"
    
    # 1. 更新数据库
    db.execute("UPDATE user SET name=%s WHERE id=%s", (data['name'], id))
    
    # 2. 删除缓存(下次读重新构建)
    r.delete(key)

def update_user_delayed_delete(id, data):
    """写:延迟双删版本"""
    key = f"user:{id}"
    
    # 1. 第一次删缓存
    r.delete(key)
    
    # 2. 更新数据库
    db.execute("UPDATE user SET name=%s WHERE id=%s", (data['name'], id))
    
    # 3. 延迟(确保并发读已完成)
    time.sleep(0.5)
    
    # 4. 第二次删缓存
    r.delete(key)

3.2 验证一致性方案效果

bash
# 模拟:先写库后删缓存
# 终端 1:监控 key
redis-cli MONITOR

# 终端 2:写入测试
redis-cli SET user:1 "old_value"

# 执行写流程
echo "模拟先写库后删缓存"
redis-cli DEL user:1  # 真实的业务这里在写入 DB 之后执行

# 验证缓存已删除
redis-cli GET user:1
# (nil) ← 缓存已清空,下次读会重新构建

# 模拟并发读引起的脏数据
echo "模拟:先删缓存后写库的问题"
# 步骤 1:删缓存
redis-cli DEL user:1
# 步骤 2(模拟并发读):缓存没命中→读 DB→写旧值到缓存
redis-cli SET user:1 "old_db_value"  # 并发读操作
# 步骤 3:写库(更新为最新值)
redis-cli SET user:1 "new_db_value"  # 这里应该是 DB 操作
# → 缓存中是 "old_db_value"(脏数据),DB 中是 "new_db_value"
redis-cli GET user:1
# "old_db_value"  ← 不一致!

3.3 使用 Lua 保证原子性

lua
-- 保证"查缓存+设缓存"原子性
local key = KEYS[1]
local ttl = ARGV[1]
local value = redis.call('GET', key)
if value then
    return value  -- 返回缓存值
end
-- 未命中,返回 nil(让调用者查 DB)
return nil

四、面试视角

追问答案要点
为什么 Cache Aside 建议删缓存而不是更新?删除更简单(原子操作),不会出现"写-写"竞争。更新需要知道缓存结构(如 Hash 对象)
先写库后删缓存,删缓存失败怎么办?重试机制(异步队列重试)、延迟双删(第二次覆盖)、Canal(不依赖删缓存)
先删缓存后写库有什么问题?并发读可能写旧值到缓存(脏数据),延迟双删可以缓解但不能根治
延迟双删的 sleep 时间怎么确定?取决于"读请求从查 DB 到设缓存"的时间,需要在业务高峰期实测(通常 500ms-1s)
Canal 同步方案有什么缺点?引入 Canal + 消息队列,运维复杂度增加;binlog 可能会有较大延迟(秒级)
最终一致性和强一致性的选择?大部分场景最终一致性足够(Cache Aside);关键数据(库存/余额)需额外保障
能不能做到强一致?Redis + MySQL 不能(无分布式事务)。方案:读写全部走 DB(不用缓存)或分布式锁

📚 相关链接

  • **缓存三大问题** — 缓存的其他常见问题
  • **Lua脚本** — 用 Lua 保证缓存操作的原子性
  • ← 返回 **缓存设计与应用索引**

Knowledge4J — Java 知识库