[redis]保证redis和数据库db的数据一致性

107 阅读6分钟

保证Redis与数据库之间的数据一致性,是分布式系统中的一个经典挑战。没有“银弹”方案,需要根据业务场景(对一致性要求、数据读写比例、性能需求)进行权衡和设计。

核心思想是:放弃强一致性,追求最终一致性

在分布式环境下,同时保证Redis和数据库的强一致(CP)代价极高,会严重损害性能,通常业务上可以接受短暂的数据不一致

以下从设计模式、常见方案、高级实践和选择建议几个方面来系统阐述。

一、核心设计模式 (Cache-Aside Pattern)

这是最常用、最基础的缓存模式,也称为“懒加载”。流程如下:

  1. 读操作
    • 先读Redis,命中则返回。
    • 未命中则读数据库,取出数据后写入Redis,再返回。
  2. 写操作
    • 更新数据库
    • 删除Redis中的对应缓存(而非更新)。

优点:逻辑简单,能覆盖大部分场景。删除缓存而非更新,避免了并发写操作导致的缓存数据错乱问题。

关键:该模式的一致性问题,就集中在 “更新数据库” 和 “删除缓存” 这两个操作的执行顺序和原子性上。

二、常见一致性方案与权衡

主要围绕两大核心问题:1. 先操作数据库还是先操作缓存? 2. 操作失败怎么办?

方案一:先更新数据库,再删除缓存 (Cache-Aside 标准做法)

这是最推荐的通用做法。

  • 流程
    1. 更新数据库。
    2. 删除Redis缓存。
  • 不一致场景分析
    • A线程更新数据库后,删除缓存前:B线程读取到的是旧缓存(短暂不一致,等A删除后即恢复)。
    • A线程更新数据库后,删除缓存失败:缓存永远为旧数据(严重问题)。
  • 优点:出现不一致的时间窗口通常极短(因为数据库写操作一般比Redis删除慢,下一个读请求会在缓存缺失后从数据库加载新值)。
  • 缺点:缓存删除失败会导致永久不一致。必须要有重试或补偿机制

方案二:先删除缓存,再更新数据库

  • 流程
    1. 删除Redis缓存。
    2. 更新数据库。
  • 不一致场景分析(概率较高)
    1. A线程删除缓存。
    2. B线程发现缓存缺失,读取数据库旧值,并写入缓存。
    3. A线程更新数据库。
    • 结果:缓存中是旧数据,数据库是新数据,且直到该缓存下次被删除或过期前,都会不一致。
  • 缺点:不一致发生的概率和窗口比方案一更大,因为读操作(步骤2)非常快。

针对“先删后更”的优化:延迟双删

为了解决上述问题,在方案二基础上增加一个延迟删除步骤。

  1. 删除Redis缓存。
  2. 更新数据库。
  3. 等待一小段时间(如几百毫秒,根据主从同步和业务读耗时估算)
  4. 再次删除Redis缓存。
  • 作用:第4步可以清除掉在步骤1和步骤2之间被其他线程写入的旧值缓存。
  • 缺点:引入了延迟,降低了吞吐量,且时间难以精确估计。

三、保障可靠性的高级实践

无论采用哪种顺序,关键是要保证两个操作最终都能成功。以下是保障措施:

1. 缓存删除重试机制

  • 同步重试:在应用代码中重试几次,简单但不推荐,会阻塞用户请求。
  • 异步重试 - 消息队列
    • 更新数据库后,向消息队列发送一条删除缓存的消息。
    • 消费者消费消息,执行删除。若失败,消息会自动重试,直到成功。
    • 优点:解耦,可靠。需维护消息队列。
  • 异步重试 - 订阅数据库Binlog
    • 使用 Canal、Debezium 等工具,订阅数据库的变更日志(Binlog)。
    • 程序解析Binlog,识别出数据更新,然后触发缓存删除。
    • 优点:完全解耦,对业务代码零侵入。可以统一处理所有缓存失效逻辑。
    • 缺点:系统复杂度高。

2. 设置合理的缓存过期时间

  • 在所有缓存Key上设置一个不太长的过期时间(如30秒到几分钟)。
  • 这是最终一致性的兜底方案。即使之前的删除操作全部失败,缓存数据也会在一段时间后自动过期,下一个读请求会从数据库加载最新值。
  • 作用:防止在补偿机制也失效的情况下,产生永久性不一致。

四、不同业务场景的选择建议

场景一致性要求推荐方案关键点
读多写少,容忍秒级不一致(如用户信息、商品详情)最终一致性先更新DB,再删除缓存 + 缓存过期实施简单,配合短过期时间,可靠性足够。
写多或一致性要求稍高(如库存、优惠券)最终一致性(尽量短)先更新DB,再删除缓存 + 异步重试(MQ/Binlog)通过重试保证删除成功,不一致窗口极短。可考虑延迟双删。
金融、交易核心数据(如账户余额)强一致性放弃读缓存,直接读DB使用分布式锁强一致性代价高。可直接读主库,或使用读写锁确保“读-写-读”顺序,但性能骤降。
极少变化的配置类数据最终一致性先更新DB,再删除缓存设置较长过期时间,定时刷新不一致影响小,甚至可以直接用过期时间驱动更新。

五、总结与最佳实践清单

  1. 首选标准模式:对于大多数互联网应用,采用 Cache-Aside(先更新数据库,再删除缓存)
  2. 删除而非更新:写操作时,总是 删除(Invalidate) 缓存,而不是更新它。
  3. 必须设置过期时间:为缓存Key设置一个合理的、不算太长的过期时间,作为最后的安全网。
  4. 建立补偿机制:通过 消息队列订阅Binlog 实现异步重试,确保缓存删除操作最终成功。
  5. 考虑并发场景:在高并发写入场景下,可以评估 延迟双删,但要权衡复杂度。
  6. 强一致性场景慎用缓存:对于必须强一致的业务,要么直接读数据库(可能从库有延迟,需读主库),要么引入分布式读写锁(性能代价大)。
  7. 监控与告警:监控缓存与数据库的差异(例如通过定期抽样对比),并设置告警。这是发现和解决问题的最后防线。

记住,一致性、可用性、分区容错性(CAP)无法同时完美满足。与产品、业务方明确“可接受的不一致时间”是技术方案设计的前提。通过技术手段将这个不一致窗口缩短到业务可接受的范围内,是工程实践的目标。