缓存一致性
谈谈一致性
一致性就是数据保持一致,在分布式系统中,可以理解为多个节点中数据的值是一致的。
强一致性:这种一致性级别是最符合用户直觉的,它要求系统写入什么,读出来的也会是什么,用户体验好,但实现起来往往对系统的性能影响大
弱一致性:这种一致性级别约束了系统在写入成功后,不承诺立即可以读到写入的值,也不承诺多久之后数据能够达到一致,但会尽可能地保证到某个时间级别(比如秒级别)后,数据能够达到一致状态
最终一致性:最终一致性是弱一致性的一个特例,系统会保证在一定时间内,能够达到一个数据一致的状态。这里之所以将最终一致性单独提出来,是因为它是弱一致性中非常推崇的一种一致性模型,也是业界在大型分布式系统的数据一致性上比较推崇的模型
场景:秒杀
跳过分布式复制,直接就在单节点完成读写,这是保证强一致性最好的办法。
性能上需要垂直切分数据,分散各节点压力。也就是每个
Redis
都是作为主节点存在,将需要秒杀的商品,做数据上的切分,由每个Redis
承担一部分商品的读写访问,那么这个过程就是分布式计算中无共享的并行化模式了,适合使用Redis cluster
架构
场景:数据库 + 缓存
理论上讲,给缓存设置过期时间,就能保证最终一致性。
1. 先更缓存,再更数据库
如果更数据库失败,需要自行实现缓存的回滚逻辑,很麻烦
和下面一样的并发问题
2. 先更数据库,再更缓存
2.1. 更新失败后的回滚
如果更缓存失败,会导致缓存和数据库数据不一致。需要业务增加重试逻辑
2.2. 并发场景下的顺序问题
如果同时有请求A和请求B进行更新操作,那么以下场景会出现脏数据:
线程A更新了数据库
线程B更新了数据库
线程B更新了缓存
线程A更新了缓存
2.3 业务场景角度
如果读较集中于部分热点数据,则大量缓存不会被读到,更新这些缓存浪费资源
2.4 解决
要解决 2.2 并发下的顺序问题,想办法保证操作的顺序就可以了。像上面的场景里,起个线程 C
通过阿里的 canal
中间件从 DB binlog
同步数据到缓存,就可以保证顺序。
另一种思路是在更新数据时从缓冲中删除数据,这种方案叫【Cache-Aside】。设计如下:
失效:应用程序先从cache取数据,没有得到,则从数据库中取数据,成功后,放到缓存中。
命中:应用程序从cache中取数据,取到后返回。
更新:先把数据存到数据库中,成功后,再让缓存失效。
但先删缓存,再更新数据库。还是先更新数据库,再删缓存?
3. 先删缓存,再更数据库
该方案会导致不一致的原因是:同时有一个请求A进行更新操作,另一个请求B进行查询操作。那么会出现如下情形:
A删缓存
B读不到缓存,从
DB
加载旧数据到缓存A更新
DB
由于读比写的速度快,所以步骤2一般比步骤3快
可以采用延时双删策略,即第一步删完缓存后,睡眠过一会儿再删一次
采用这种同步淘汰策略,吞吐量降低怎么办?
睡眠和第二次删除新起线程来做
4. 先更数据库,再删缓存
facebook 在论文《Scaling Memcache at Facebook》中提出,他们用的也是先更新数据库,再删缓存的策略。
该方案也有不一致问题:
A查缓存,缓存刚好失效;A从DB查出旧数据
B更数据库并删除缓存
A加载旧数据进缓存
不过一般A读的速度原高于B写的速度,即步骤3通常早于步骤2,所以这种情况很难出现。
假设,有人非要抬杠,有强迫症,一定要解决怎么办?
首先,给缓存设有效时间是一种方案。其次,采用异步延时删除策略,保证读请求完成以后,再进行删除操作。
这个方案还有一种做法:单起一个线程C,通过阿里 canal
中间件消费 DB binlog
,消费后不是更新缓存,而是删除缓存。B站的评论就是这么做的。
也有问题:1. A读取旧数据(但还没写到缓存)、2. C从缓存删除数据、3. B读取新数据到缓存、4. A写旧数据到缓存。不过同样几率很小,步骤4 一般都早于3
删缓存失败的解决方案
这是3和4都存在的一个问题。要解决,提供一个保障的重试机制即可。
写压力不大的场景,将删除失败的数据直接放入内存里,单起一个线程不断重试即可
删除失败的数据,放入消息队列里进行重试
删除失败属于很少见的
case
,消息队列里不会有太多数据为此引入消息队列,会对业务线代码造成大量的侵入
接数据库
binlog
mysql
有个阿里中间件叫canal
,可以完成订阅binlog
日志的功能另起一段非业务代码,同步
binlog
和redis
参考
Last updated