高并发场景下缓存一致性方案


本文对应知识卡片集:

相关文章

  • [[Go 本地缓存方案选型]]
  • [[高并发场景下缓存污染问题]]
  • [[高并发场景下的多级缓存设计]]
  • [[高并发缓存设计实战-短链接]]

    一致性

在此之前先认清两个一致性

强一致性(Strong Consistency)

强一致性要求系统中的所有数据副本在任何时候都是一致的。这意味着,一旦数据更新操作完成,任何对该数据的读取操作都会立即反映出这次更新。强一致性可以保证所有用户在任何时间都能看到相同的数据版本。这种一致性模型适用于对数据一致性要求非常高的场景,如金融交易系统。

最终一致性(Eventual Consistency)

最终一致性是一种更宽松的一致性模型。它只保证如果系统不再接收新的更新,那么最终所有数据副本将会达到一致的状态。在这种模型中,数据的更新可能会在不同副本之间延迟传播,因此在某些时刻,不同的用户可能会看到不同版本的数据。这种模型适用于对实时性要求不是非常高,但需要高可用性和分区容错性的系统,例如许多大规模的互联网应用。

最终一致性方案

优点 缺点 备注
更新缓存方案 出现脏数据 本方案仅用于讨论,实际场景下并不使用
先删除缓存,再更新数据库 出现脏数据 本方案仅用于讨论,实际场景下并不使用
延迟双删 较先删除缓存,再更新数据库,一定程度解决了脏数据的问题 1. 需要两次删除操作,高并发场景下对 redis 造成了更大的压力。
2. 如果第二次删除失败了,整体的效果会退化至先删除缓存,再更新数据库。
个人感觉,改进效果一般,不实用。
先更新数据库,再删除缓存 1. 写入数据库(操作1)和删缓存(操作2)之间,存在短时间的数据不一致。
2. 如果删缓存失败,则会存在较长时间的数据不一致,这个时间会一直持续到缓存过期。
比较常见的方案
先更新数据库,再基于队列删除缓存 较先更新数据库,再删除缓存,进一步解决了删除缓存失败的问题 1. 如果写操作非常频繁,队列的任务比较多,消费可能会比较慢,需要引入多线程机制,加快消费速度。
2. 程序复杂度成倍上升,引入了消费线程、任务队列,并且还需要不断进行性能优化。
先更新数据库,然后由 binlog+消息队列进行缓存删除 进一步解耦缓存的更新操作,并提高拓展性 除了先更新数据库,再基于队列删除缓存原有的缺点外,再引入了日志监听工具,进一步增加复杂性。

关于缓存的强一致性与最终一致性存在着下列特点:

  1. 性能和一致性不能同时满足,为了性能考虑,通常会采用「最终一致性」的方案

  2. 掌握缓存和数据库一致性问题,核心问题有 3 点:缓存利用率、并发、缓存 + 数据库一起成功问题

  3. 失败场景下要保证一致性,常见手段就是「重试」,同步重试会影响吞吐量,所以通常会采用异步重试的方案

  4. 订阅变更日志的思想,本质是把权威数据源(例如 MySQL)当做 leader 副本,让其它异质系统(例如 Redis / Elasticsearch)成为它的 follower 副本,通过同步变更日志的方式,保证 leader 和 follower 之间保持一致

更新缓存方案

缺点 :出现脏数据

在实际的业务场景中,一种常见的并发场景是:微服务 Provider 实例 A、B 同时进行同一个数据的更新操作。

按照先更新数据库,再更新缓存的策略,微服务 Provider 实例A、B可能会出现下面的执行次序:

具体的原因是:Provider B更新到缓存中的数据被Provider A更新到缓存中的数据覆盖了。数据库的更新次序是先A后B,理论上缓存是Provider B的数据而不是Provider A的数据。所以,在上述流程执行完毕后,缓存中的 Provider A 的数据为脏数据

之所以出现这个问题,是因为以上流程中步骤3与步骤4执行的均为操作缓存,都是高并发的操作,很难保证先后次序,所以缓存出现脏数据的概率很大。

至于先更新缓存再更新数据库的方案也是同理。

除了出现脏数据之外,更新缓存相对于删除缓存还有两点劣势:

  1. 如果写入缓存的值是经过复杂计算才得到的,更新缓存频率高的话,就会大大降低性能。
  2. 及时更新缓存属于饿汉模式,适用于数据读取高频的场景。在写多读少的情况下,数据很多时候还没被读取到就又被更新了,这也浪费了缓存的空间,降低了性能。

先删除缓存,再更新数据库

缺点:出现脏数据

在实际的业务场景中,一种常见的并发场景是:微服务Provider实例A进行数据的写操作,而微服务Provider实例B同时进行同一个数据的读操作。按照先删除缓存,再更新数据库的策略,微服务 Provider 实例 A、B 可能会出现下面的执行次序:

Provider B查询缓存的时候,缓存中的数据被删除,Provider B只能去数据库中查找,然后将数据更新到缓存,而Provider A在Provider B查询完之后更新了数据库,导致了数据库和缓存的不一致。

延迟双删

延迟双删是为了解决先删除缓存,再更新数据库出现的脏数据问题而提出的:

其大致方案如下:先删除缓存,再更新数据库,延迟一段时间后去删除缓存(此处延迟,个人理解是为了让数据成功落地到数据库中,但是在部分同步操作情况下,等待数据库返回响应其实就是一种延迟了)

微服务Provider实例A进行数据的写操作,而微服务Provider实例B同时进行同一个数据的读操作。微服务Provider实例A、B可能会出现下面的执行次序:

存在问题:

  1. 如果写操作比较频繁,可能会对Redis造成一定的压力。

  2. 在极端情况下,第二次延迟删缓存失败,操作的效果退化到先删除缓存,再写数据库:数据库和缓存存在较长时间的数据不一致,这段时间会一直持续到缓存过期,比如4个小时(以项目中的配置时间为准)。

先更新数据库再删除缓存

按照先更新数据库,再删除缓存的策略,微服务Provider实例A、B可能会出现下面的执行次序:

存在问题:

  1. 写入数据库(操作1)和删缓存(操作2)之间,存在短时间的数据不一致。

  2. 如果删缓存失败,则会存在较长时间的数据不一致,这个时间会一直持续到缓存过期。

先更新数据库,再基于队列删除缓存

本策略是对先更新数据库,再删除缓存的策略进行改进后得到的:

  1. 写数据库(操作1)和删缓存(操作2)之间存在短时间的数据不一致。

  2. 如果删缓存失败,则会存在较长时间的数据不一致,这个时间会一直持续到缓存过期。

使用队列删除的好处是:可以解耦删除缓存的操作,进行不断重试(毕竟重试就会造成响应速度的降低),知道成功为止,如此解决了问题 2

本策略存在的问题:

  1. 如果写操作非常频繁,队列的任务比较多,消费可能会比较慢,需要引入多线程机制,加快消费速度。

  2. 程序复杂度成倍上升,引入了消费线程、任务队列,并且还需要不断进行性能优化。

队列的选择与对比

特性 消息队列 内存队列
响应速度 较慢(需网络传输) 较快
效率 高(可部署多消费者) 依实现方式而定
可靠性 高(支持持久化) 低(数据易丢失)
耦合性
扩展性 高(易于增加消费者处理能力) 依实现方式而定
事务性 支持(可保证操作顺序和完整性) 通常不支持
成本 相对较高(需要维护、可能需支付服务费用) 通常较低

适用场景

  • 消息队列
    • 高可靠性需求:需要保证数据不丢失,可以进行持久化存储。
    • 大规模数据处理:需要处理大量数据或请求,可通过增加消费者来扩展处理能力。
    • 事务性要求:需要保证操作的顺序和完整性,例如订单处理流程。
  • 内存队列
    • 快速响应需求:追求最低延迟和最快处理速度,适用于对实时性要求较高的场景。
    • 成本敏感型应用:成本考虑优先,适用于小规模或初创项目。
    • 简单应用场景:应用逻辑简单,数据丢失风险较低或易于恢复的场景。

先更新数据库,然后由 binlog+消息队列进行缓存删除

和单纯的基于消息队列删除缓存的优势是:微服务Provider在执行数据库和缓存双写时,只需要执行写入数据库的操作就可以了,大大简化了微服务Provider的业务逻辑

方案示例

此处使用 MySQL 作为示例

使用 maxwell 去监听 binlog ,然后 maxwell 将 binlog 同步到 RabbitMQ 中,负责删除缓存的组件监听 RabbitMQ ,接受到消息后,删除对应的缓存。

其中有几个注意事项:

  1. 什么 SQL 语句需要被处理
  2. 如何去提高消费速度,降低 redis 的压力
  3. 此方案也是只能保证最终一致性,期间还是存在短时间的数据不一致

强一致性方案

分布式读写锁

分布式读写锁用于控制对共享资源的并发访问。它可以确保在任何时刻,要么只有一个节点可以对资源进行写操作,要么有多个节点可以进行读操作,但两者不能同时发生。这种锁通常通过分布式协调服务(如Zookeeper或Etcd)来实现。分布式读写锁有助于保证数据的一致性,但可能会因为锁的竞争和网络延迟而影响系统的性能和可用性。

分布式事务

分布式事务涉及到多个节点上的操作,这些操作要么全部成功,要么全部失败,以此来维持事务的原子性、一致性、隔离性和持久性(ACID属性)。实现分布式事务的方法有两阶段提交(2PC)、三阶段提交(3PC)和基于时间戳的协议等。虽然分布式事务可以提供强一致性保证,但它们往往会引入较大的延迟和复杂性,并可能影响系统的整体性能。

分布式共识算法

在缓存的强一致性方案中,主要是前两种。

其中 Paxos 和 Raft 是两个著名的共识算法。

参考资料

本文资料与论证来自各种网络资料,加之本人实践理解,由于时间跨度较大,无法一一列出。


如果本文帮助到了你,帮我点个广告可以咩(o′┏▽┓`o)


文章作者: Anubis
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Anubis !
评论
  目录