缓存一致性的常见方案

Cache Aside Pattern - 旁路缓存模式


  • 读请求:如果未命中缓存则查询数据库并更新至缓存,否则返回缓存中数据
  • 写请求:先更新数据库,再删除缓存(非延迟双删)
Cache Aside 模式

写请求为啥不更完DB直接更缓存?

Cache Aside 模式的读请求处理流程应该很好理解,但对于写请求大家或许会有疑问,为何写完库不直接更缓存?从直觉上而言直觉更缓存似乎更容易被理解,但实际上要从两个方面考虑:性能与安全

  • 从性能方面考虑,当写请求较多时,可能出现一个线程刚更完缓存就被重新更新了(此现象业界称之为缓存扰动),那么机器的性能会被白白浪费,缓存利用率也不高,而等到读请求读完再更新缓存,也符合懒加载的思想。
  • 从安全性上考虑,当出现写写线程并发执行时,缓存容易出现脏数据,下面会逐步踩坑。

开始踩坑


1、先更数据库,再更缓存?(写写情况下易出问题)

假如执行过程为:

1、线程1更新数据库

2、线程2更新数据库

3、线程2更新缓存

4、线程1更新缓存

很明显执行完毕后,缓存中的数据为脏数据,出现数据不一致问题。

而之所以大概率出现这个问题,是因为过程3与4的执行均为操作缓存,速度差不多,所以缓存出现脏数据的概率很大。

双写.png

2、先删缓存,再更新数据库?(写读情况下易出问题)

假设执行过程为:

1、写请求先删除缓存

2、读请求查询缓存,发现不存在,则查询数据库

3、读请求写入缓存

3、写请求更新数据库

先删后写.png

3、先更数据库,再删缓存(推荐)

假设执行过程为:

1、读请求先查询缓存,未击中,查询数据库返回18

2、写请求更新数据库,删除缓存(删了个寂寞)

3、读请求才回写缓存

同理也有可能出现数据不一致问题,但实际上出现概率会很小,因为数据库的更新操作要比内存操作慢几个数量级,所以理论上过程3回写入缓存速度会快于过程2更新数据库的操作。如何避免这种问题?通常我们会有一个兜底方案,就是设置缓存过期时间,允许一定时间内的数据库与缓存的数据不一致。

但加上过期时间并非就是完美的,假设过程2写完库后,删除缓存失败,那么也会导致数据不一致问题的出现,而考虑到这种场景,第一种方案是直接对更新数据库与删除缓存的操作加一把分布式锁,但加锁必然带来吞吐量的下降,需综合考虑,另外一种则是对删除操作做一些补偿的措施,下面谈及延迟删除策略会对删除缓存的补偿方案做进一步的解析。

先更后删(Cache Aside Pattern).png

4、结合2和3,做延迟双删(推荐)

1、先删除缓存
2、更新数据库
3、休眠一会儿(比如1s),之后再删一次缓存数据

延迟双删.png

上述方案在极端情况下,如果第三步删除失败仍然可能导致数据一致性问题,解决方案有:

1、引入MQ重试机制。假设更完库后删除失败,则把失败的key丢到MQ中,由mq消费端拉出来进行删除重试。这种方案的弊端是对于删除失败的处理逻辑需要基于业务代码的 trigger 来触发,对业务代码侵入性较为严重。

2、基于数据库binlog的方式增量解析、订阅和消费。为了保证删除成功,可以利用阿里巴巴开源中间件canal订阅binlog发送到MQ中,再利用MQ的ACK机制来保证删除成功,最终保证数据缓存一致性(比如更新了uid=2这个用户信息,那么可以读取binlog中uid=2的log,然后删除缓存中key={user:2}这个key)。

具体架构如下图所示:


基于数据库binlog的方式增量解析、订阅和消费.png

延伸1:Alibaba开源组件canal

  • canal的原理:

模仿MySQL Slave发送dump请求到MySQL服务,MySQL服务接受到该请求后,会将binary log推送给canal server,由Canal Server解析binary log对象(byte流),也就是我们常说的binlog。再由canal client拉取进行消费,且canal server也支持投递到如Kafka、RocketMQ这样的MQ系统中,让其他系统可以消费到,在其ACK机制的加持下,无论推送或拉取都可以保证数据按预期被消费到。

  • canal依赖于zookeeper来实现HA,并且为了减轻MySQL dump的压力,canal同一时刻只允许一个server instance处于运行状态,其他instance则为standby状态。处理之外,为了保证消费的有序性,对于一个instance同一时刻只能由一个canal client进行get/ack等操作。

延伸2:MySQL主从部署,如何解决主从同步延迟?

监听从库的binlog,保证最终删除的操作一定发生在更新数据库之后。

如下图,就是A先删除缓存(不管成功或失败都不影响,因为失败了最终通过binlog会删除的),再更新DB,而此时因为主从可能存在延迟,所以B在cache miss之后从从库可能读取到旧数据写入缓存(脏数据)或者还有一种情况就是A删除缓存失败并且同步有延迟,那么B读取旧缓存,但是不影响,因为最后主从同步成功之后通过canal将binlog数据写入MQ,消费者可以根据更新的log数据删除缓存,并且通过ACK机制确认处理这条更新log,以保证数据缓存一致性。

MySQL主从部署下,监听从节点的binlog

Read Though Pattern - 读穿透模式


简单来说,区别于Cache Aside的是应用程序不需再管理缓存和数据库,只需从独立的缓存提供程序Cache Provider中获取即可。

好处:是独立管理可以减少数据源上的负载,也让应用端的容错性更佳(如果缓存服务挂了,Cache Provider仍然可以通过直接转到数据源上进行操作,不影响应用端的使用)

适用场景:多次请求相同数据的场景。(guavacache采用该模式)

读穿透.png

Write Trough Pattern - 直写模式


与Read Aside类似,增加了一个Cache Provider代理层来处理更新底层数据源和缓存。与Cache Aside不同的是,在写请求更完库之后Write Through是直接写入缓存,而不是删除缓存。

适用场景:写操作较多,且一致性要求高的场景,并且为了避免前面提到并发双写导致的一致性问题,需要给更新数据库和更新缓存的操作加一把分布式锁,牺牲掉一部分的性能。

直写模式.png

Write Behind Pattern - 异步回写模式


简单来说就是先写缓存,再由Cache Provider定时在数据库负载较低的时候写入数据库。可以看出,此方案的缓存与数据库为弱一致性,且有丢数据的风险,需做好缓存的高可用,此方案对于一致性要求高的系统应慎用。

适用场景:写入速度快,适用于大量写入的场景(实际上在大型互联网应用中大多都是读多写少的场景),电商秒杀场景中库存的扣减常用该模式。

Tip:InnoDB Buffer Pool 也用此机制

异步回写模式.png

Write Around


对于一些一致性要求较低的业务,也可以选择Wrtie Aound模式。在该模式下,读请求对于缓存的写入都需要设一个expired time,而写请求在更新数据库后不会对缓存有任何操作。这种方案的优点很明显,实现简单,缺点是数据的弱一致性。

总结


四种策略:

  • Cache Aside Pattern 首选先写入数据库后删除缓存的策略,再增加缓存时间兜底。对于一致性要求更高的场景,考虑用订阅MySQL binlog+MQ的方式做延迟双删。
  • Read/Write through Pattern 增加一个Cache Provider 对外提供读写操作,解耦且避免缓存服务挂掉给应用系统带来的影响。
  • Write Behind Pattern 应用只写缓存,再由Cache Provider定时入库,数据一致性差。
  • Write Around 由读请求写缓存并设置一个过期时间,写请求只负责写库

参考文献

https://github.com/CoderLeixiaoshuai/java-eight-part/blob/master/docs/redis/高并发场景下,到底先更新缓存还是先更新数据库?.md#cache-aside

https://www.modb.pro/db/237472

https://xie.infoq.cn/article/1322475e05c11bd2aacd8bc73

https://cdmana.com/2022/01/202201191744387996.html

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,294评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,493评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,790评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,595评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,718评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,906评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,053评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,797评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,250评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,570评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,711评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,388评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,018评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,796评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,023评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,461评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,595评论 2 350

推荐阅读更多精彩内容