返回文章列表
面试考点

Redis-缓存一致性

Redis-缓存一致性

KKiana
2025年10月26日29 次阅读

Redis缓存一致性策略

系统化学习Redis与MySQL数据一致性解决方案

技术栈: Redis、MySQL、Canal、消息队列


📚 目录


缓存一致性策略

策略概览

📊 五种策略快速对比

策略操作方式一致性性能适用场景推荐度
Cache-Aside应用直接操作通用业务(用户信息、商品详情、文章内容)⭐⭐⭐⭐⭐
Read-Through缓存层自动读读快读密集(配置中心、字典数据、热门榜单)⭐⭐⭐⭐
Write-Through缓存层同步写写慢强一致(账户余额、库存、订单状态)⭐⭐⭐
Write-Behind缓存层异步写写快高并发写(点赞数、浏览量、在线人数)⭐⭐⭐
Write-Around跳过缓存冷数据(操作日志、历史归档、批量导入)⭐⭐

🎯 推荐方案

通用场景(90%):

Cache-Aside + 延迟双删 + Canal监听从库binlog

多层防护:

  1. 应用DELETE cache(覆盖70%)
  2. 延迟双删500ms(+25%)
  3. Canal监听从库(+4.9%)
  4. TTL自动过期(兜底100%)

特殊场景:

  • 强一致性 → Write-Through
  • 极致性能 → Write-Behind
  • 冷数据 → Write-Around

策略详解

1. Cache-Aside(旁路缓存)⭐ 最常用

定义: 应用程序直接操作缓存和数据库

操作流程:

  • 读: 查缓存 → 未命中查DB → 写缓存 → 返回
  • 写: 更新DB → 删除缓存

特点:

  • ✅ 应用完全控制
  • ✅ 实现简单灵活
  • ⚠️ 需处理并发问题

适用: 读多写少的通用业务


2. Read-Through(读穿)

定义: 缓存层自动负责数据加载

操作流程:

  • 读: 应用调用缓存层 → 缓存层自动查DB并写缓存
  • 写: 直接写DB(需配合其他策略)

特点:

  • ✅ 应用逻辑简单
  • ✅ 缓存加载统一管理
  • ⚠️ 需防止缓存击穿

适用: 读密集型应用,使用缓存库(如go-cache、groupcache)


3. Write-Through(写穿)

定义: 同步更新缓存和数据库

操作流程:

  • 读: 同Read-Through
  • 写: 缓存层同步写DB → 成功后更新缓存 → 返回

特点:

  • ✅ 强一致性
  • ✅ 数据不丢失
  • ❌ 写性能差(同步阻塞)
  • ⚠️ 需要加锁(防止并发更新缓存乱序)

适用: 强一致性要求,写操作不频繁

三种加锁方案:

方案原理优点缺点适用场景推荐度
分布式锁Redis锁保护整个写流程跨服务器生效性能开销大金融账户、库存⭐⭐⭐⭐⭐
数据库行锁SELECT FOR UPDATE利用DB机制长事务影响并发中等并发⭐⭐⭐⭐
乐观锁版本号CAS更新无锁高性能冲突时需重试读多写少⭐⭐⭐

💡 为什么Cache-Aside不需要加锁?

  • Cache-Aside是删除缓存(幂等操作,重复删除无影响)
  • Write-Through是更新缓存(顺序敏感,旧值可能覆盖新值)

4. Write-Behind(写回)

定义: 异步批量写数据库

操作流程:

  • 读: 同Read-Through
  • 写: 立即更新缓存 → 加入队列 → 立即返回 → 后台批量写DB

特点:

  • ✅ 写性能极高
  • ✅ 减少DB压力
  • ❌ 可能丢数据

适用: 高并发写入,可容忍数据丢失(点赞、统计)


5. Write-Around(绕写)

定义: 写操作跳过缓存,依赖TTL过期

操作流程:

  • 读: 查缓存 → 未命中/过期查DB → 写缓存
  • 写: 只写DB,不管缓存

特点:

  • ✅ 避免缓存污染
  • ✅ 写操作简单
  • ❌ 短期数据不一致

适用: 冷数据、批量导入、写多读少


补偿机制

📋 三种补偿方式对比

方式流程延迟可靠性复杂度成本推荐
延迟双删DELETE → UPDATE DB → 延迟500ms → DELETE⭐⭐⭐
Canal监听binlogCanal订阅binlog → 解析变更 → DELETE cache⭐⭐⭐⭐⭐
MQ异步UPDATE DB → 发MQ → 消费者DELETE cache⭐⭐⭐⭐

1. 延迟双删

实现:

  1. 第1次删除缓存
  2. 更新数据库
  3. 延迟N毫秒(通常500ms)
  4. 第2次删除缓存

关键: 延迟时间 > 读请求最大耗时 + 主从延迟


2. Canal监听binlog ⭐ 推荐

实现:

  1. Canal订阅MySQL binlog(推荐从库
  2. 解析数据变更事件
  3. 配合版本号判断:if (cacheVersion < dbVersion) { DELETE cache }

关键配置:

  • 监听从库binlog(利用主从延迟作为保护窗口)
  • 只需监听一个从库(选延迟最大的最保守)

优势:

  • 从库同步后才删除,避免"Canal删除后又有旧值写入"

3. MQ异步

实现:

  1. 更新数据库后发送MQ消息
  2. 消费者异步删除缓存
  3. 失败重试机制

适用: 已有MQ基础设施,需要解耦


并发问题

🔴 Cache-Aside并发场景

问题:

T1: 读请求查DB(旧值)
T2: 写请求更新DB
T3: 写请求DELETE cache
T4: 读请求SET cache(旧值)← 在DELETE之后!

发生条件:

  1. 时序:T1 < T2 < T3 < T4
  2. 网络延迟导致Redis命令到达顺序不确定
  3. 主从延迟导致读从库获取旧值

发生概率: 1-5%(正常)/ 10-30%(主从延迟大/高并发)


✅ 多层防护方案

方案原理覆盖率成本
延迟双删500ms后再删一次90%
Canal监听从库从库同步后删除95%
版本号机制Canal判断版本号99%
Lua脚本写缓存时拒绝旧版本99.9%
TTL自动过期100%

💡 版本号机制(推荐)

1. 数据库设计

ALTER TABLE user ADD COLUMN version BIGINT DEFAULT 0;
UPDATE user SET name=?, version=version+1 WHERE id=?;

2. 缓存数据

{
  "id": 1,
  "name": "张三",
  "version": 15
}

3. Canal判断

if (cacheVersion < dbVersion) {
    DELETE cache;  // 旧值,删除
}

4. Lua脚本防护

-- 写缓存时拒绝旧版本
if existingVersion >= newVersion then
    return 0  -- 拒绝写入
end
SET key value

常见问题Q&A

❓ Write-Around vs Cache-Aside 区别?

策略写操作一致性
Write-Around只写DB,不管缓存依赖TTL过期
Cache-Aside写DB + 删除缓存立即失效

❓ 先更新DB还是先删Cache?

推荐:先更新DB,再删Cache

顺序优点缺点
先DB后删Cache并发窗口小,DB优先持久化主从延迟可能导致旧值覆盖
先删Cache后DB缓存先失效并发窗口大,DB失败导致穿透

❓ 事务与缓存删除时机?

错误:

func (s *Service) Update(user *User) error {
    tx, _ := s.db.Begin()
    defer tx.Rollback() // 回滚保护
    
    if err := updateDB(tx, user); err != nil {
        return err
    }
    
    deleteCache(user.ID)  // ❌ 事务可能回滚,但缓存已删
    
    return tx.Commit()
}

正确方案1:事务提交后删除

func (s *Service) Update(user *User) error {
    tx, _ := s.db.Begin()
    defer tx.Rollback()
    
    if err := updateDB(tx, user); err != nil {
        return err
    }
    
    if err := tx.Commit(); err != nil {
        return err
    }
    
    // ✅ 事务提交成功后再删除缓存
    deleteCache(user.ID)
    return nil
}

正确方案2:defer + 检查error

func (s *Service) Update(user *User) (err error) {
    tx, _ := s.db.Begin()
    defer tx.Rollback()
    
    // ✅ 使用defer,只在成功时删除
    defer func() {
        if err == nil {
            deleteCache(user.ID)
        }
    }()
    
    if err = updateDB(tx, user); err != nil {
        return err
    }
    
    return tx.Commit()
}

❓ Canal监听主库还是从库?

监听对象优点缺点推荐
从库利用主从延迟保护,Canal删除时从库已同步延迟稍长
主库延迟短可能在从库同步前删除,读从库仍写旧值

❓ Canal需要监听多个从库吗?

答案:否,只需监听一个

原因:

  • 所有从库同步相同的binlog
  • 监听任一从库都能获取完整变更
  • 配合版本号机制保护其他从库

选择策略:

  • 小型系统:任意从库
  • 中型系统:延迟最大的从库(最保守)
  • 大型系统:专用Canal从库(不接业务流量)

❓ 为什么DELETE在SET之前发送,却可能后到达Redis?

原因:

  1. 网络延迟差异 - 不同TCP连接延迟不同
  2. IO多路复用 - Redis epoll随机处理就绪连接
  3. TCP重传/拥塞 - 导致顺序变化

结论: 分布式系统中网络不可靠,消息顺序不确定


❓ 如何选择缓存策略?

决策树:

业务场景?
├─ 读多写少 → Cache-Aside(最常用)
├─ 读密集   → Read-Through(go-cache/groupcache)
├─ 强一致   → Write-Through(金融、账户)
├─ 高并发写 → Write-Behind(点赞、统计)
└─ 冷数据   → Write-Around(日志、归档)

是否需要补偿?
├─ 是 → 延迟双删 + Canal + 版本号
└─ 否 → 依赖TTL(可容忍不一致)

📝 知识库持续更新中...

面试考点

评论 (0)

后参与评论

// 暂无评论,来说点什么吧

Redis-缓存一致性 | 博客