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

Redis缓存与数据库的数据一致性是分布式系统中的一个经典挑战。没有完美的通用方案,只有针对特定场景的权衡选择。以下是主要的策略、场景分析和实践建议:

一、核心策略模式

1. Cache-Aside(旁路缓存)

最常见的模式,由应用层控制缓存:

// 读流程
1. 读缓存,命中则返回
2. 未命中则读数据库
3. 写入缓存后返回

// 写流程
1. 更新数据库
2. 删除缓存(非更新)

优点:简单、可控

风险:并发写时可能短暂不一致

2. Write-Through(直写)

所有写操作同步更新缓存和数据库:

写请求 → 更新缓存 → 同步更新数据库 → 返回

优点:强一致性保证

缺点:写延迟高,缓存污染可能

3. Write-Behind(后写)

异步批量更新,先更新缓存,异步持久化到数据库:

写请求 → 更新缓存 → 异步队列 → 批量写数据库

优点:高性能

缺点:可能丢失数据,实现复杂

二、高并发场景下的经典问题与解决方案

1. 缓存删除延迟导致的不一致

问题场景

线程A更新数据库 → 线程B读缓存(旧值) → 线程A删除缓存

解决方案:延迟双删

public void updateData(Data data) {
    // 1. 先删除缓存
    redis.del(key);
    
    // 2. 更新数据库
    db.update(data);
    
    // 3. 延迟再次删除
    Thread.sleep(delayTime);
    redis.del(key);
}

2. 先更新数据库还是先删除缓存?

  • 先删缓存,再更新数据库(更常用)
    • 风险:删缓存后、更新DB前,其他线程可能读到旧DB数据并重建旧缓存
    • 缓解:延迟双删
  • 先更新数据库,再删缓存
    • 风险:更新DB后、删缓存前,缓存仍是旧数据
    • 概率较低(通常删缓存很快)

三、强一致性方案

1. 订阅数据库变更日志(推荐)

通过Canal、Debezium等工具监听MySQL binlog:

数据库变更 → 解析binlog → 消息队列 → 更新/删除缓存

优点:解耦、可靠

缺点:架构复杂,有延迟

2. 分布式事务

使用Seata等框架,但性能代价高:

BEGIN;
UPDATE db_table ...;
DELETE FROM redis_cache ...;
COMMIT;

四、工程实践建议

分级策略

场景一致性要求推荐方案
用户会话、配置中等Cache-Aside + 较短TTL
商品详情页中等延迟双删 + 版本号控制
库存、余额直接读库 或 分布式锁+Cache-Aside
计数、统计Write-Behind + 容忍延迟

关键实践

  1. 设置合理的过期时间 SET key value EX 300 # 5分钟自动过期即使更新失败,数据最终也会一致
  2. 使用版本号或时间戳 { "data": {...}, "version": 1672531200, "timestamp": 1672531200 }
  3. 降级与熔断
    • 缓存失败时直接读库
    • 数据库压力大时返回缓存旧数据
  4. 监控与告警
    • 缓存命中率监控
    • 数据不一致告警(通过对比采样)

五、选择建议

  1. 优先考虑最终一致性:大部分业务场景可接受秒级不一致
  2. 读多写少场景:Cache-Aside + 较短过期时间
  3. 写多读少场景:考虑不缓存或Write-Through
  4. 强一致性要求:直接读数据库,或使用分布式锁+Cache-Aside
  5. 架构演进:从简单开始,先使用Cache-Aside,随着业务复杂再引入binlog同步

六、常见误区

  • ❌ 先更新缓存,再更新数据库(崩溃时数据永久不一致)
  • ❌ 过度追求强一致性,牺牲可用性
  • ❌ 忽略缓存雪崩、穿透、击穿问题
  • ❌ 没有设置缓存过期时间

最终建议:根据业务容忍度选择方案。对于电商详情页,500ms内一致通常可接受;对于金融余额,可能需要强一致或短暂读库。在实际工程中,”Cache-Aside + 合理TTL + 延迟双删”组合能满足80%的场景。


作 者:南烛
链 接:https://www.itnotes.top/archives/907
来 源:IT笔记
文章版权归作者所有,转载请注明出处!


上一篇
下一篇