1、Cache Aside Pattern
CAP 是分布式系统的指导理论,它指出:一个分布式系统不可能同时满足一致性( C:Consistency )、可用性( A:Availability )和分区容错性( P:Partition Tolerance )这 3 个需求,最多只能满足其中两项。
C、A、P 这 3 个要素的关系如图:
- 一致性( Consistency ),指 “all nodes see the same data at the same time” ,即,在更新操作成功并返回客户端完成后,所有节点在同一个时间的数据完全一致。
- 提示:这里的一致性是指强一致性。一般关系型数据库就具有强一致性特性。
- 可用性( Availability ),指 “Reads and writes always succeed” ,即,服务一直可用,而且是正常响应时间。
- 分区容错性( Partition Tolerance ),指 “the system continues to operate despite arbirary message loss or failure of part of the system” ,即,分布式系统在遇到某节点或网络分区故障时,仍然能够对外提供满足一致性和可用性的服务。
- 由于 CAP 三需求无法同时满足,因此在设计分布式系统时就必须有所取舍。由于分区容错性是最基本的要求,所以系统架构师权衡取舍的也就只有 C( 一致性 )和 A( 可用性 ),即CP 还是 AP 。
- CP ,即实现一致性和分区容错性。此组合为数据强一致性模式,即,要求在多服务之间数据一定要一致,弱化了可用性。一些对数据要求比较高的场景( 比如金融业务 )常使用此模式。这种模式性能较低。Seata AT 模式的 “读已提交” 级别就是这种模式。
- AP,即实现可用性和分区容错性。此组合为数据最终一致性,即,要求所有服务器都可用,弱化了一致性。互联网分布式服务多数基于 AP 。这种模式性能较高,可满足高并发业务需求。基于消息的最终一致性就是这种模式。
- CAP是标准的方案,facebook 就是使用这种方式。
2、双写读写并发问题
Cache Aside Pattern 方案能解决 双写并发 问题:结论:即写完操作,就删除对应的redis缓存
a、先更新数据库,再更新缓存场景【不推荐】
当有两个线程A、B,同时对一条数据进行操作,一开始数据库和redis的数据都为1,当线程A去修改数据库,将1改为2,然后线程A在修改缓存中的数据,可能因为网络原因出现延迟,这个时候线程B将数据库的2修改成了3、然后将redis中的1也改成了3,然后线程A恢复正常,将redis中的缓存改成了2,此时就出现了缓存数据和数据库数据不一致情况。不推荐
b、先更新缓存,再更新数据库场景【不推荐】
当有两个线程A、B,同时对一条数据进行操作,线程A先将redis中的数据修改为了2,然后CPU切换到了线程B,将redis中的数据修改为了3,然后将数据库中的信息也修改了3,然后线程A获得CPU执行,将数据库中的信息改为了2,此时出现缓存和数据库数据不一致情况。不推荐
c、先删除缓存,再更新数据库的场景【不推荐】
先删除缓存,再更新数据库能解决双写并发问题,不能解决读写并发问题。
读写问题:
当有两个线程A、B,同时对一条数据进行操作,当线程A进行修改缓存操作时,先删除掉缓存中的数据,然后去修改数据库,因为网络问题出现延迟,这时线程B查新redis没有值,因此去数据库中查询数据为1,然后将数据1更新到缓存中,线程A网络恢复,又将数据库数据修改为了2,此时出现数据不一致。不推荐,这种情况在读写并发情况有问题
d、先更新数据库,再删除缓存场景【可以接受】
FaceBook 是采用这种方式,简称CAP
- 两次修改(双写)场景
- 当有两个线程A、B,线程A去修改数据库中的值改为2,然后出现网络波动,线程B将数库中的值修改为了3,然后两个线程都会删除缓存,保证数据一致性。无非是线程A多删了一次
- 一改一查(读写)场景
- 场景1:
当有两个线程A、B,线程A先去将数据库的值修改为2,然后需要去删除redis中的缓存,当线程B去读取缓存时,线程A已经完成delete操作时,缓存不命中,需要去查询数据库,然后在更新缓存,数据一致性;
如果线程A没有完成delete操作(图中案例),线程B直接命中,返回的数据与数据库中的数据不一致,可能会短暂出现数据不一致情况,但最终都会一致
- 场景2:
(1)缓存刚好失效,(2)线程A查询数据库,得一个旧值 ,(3)线程B将新值写入数据库,(4)线程B删除缓存(其实缓存这个时候没有东西,不过也无所谓,只不过删除的返回值为0而已),(5)最后线程A将查到的旧值写入缓存
然而,发生这种情况的概率又有多少呢?**发生上述情况有一个先天性条件,就是步骤(2)的写数据库操作比步骤(2)的读数据库操作耗时更短,才有可能使得步骤(4)先于步骤(5)。
可是,大家想想,数据库的读操作的速度远快于写操作的(不然做读写分离干嘛,做读写分离的意义就是因为读操作比较快,耗资源少),因此步骤(3)耗时比步骤(2)更短,这一情形很难出现。 假设,有人非要抬杠,有强迫症,一定要解决怎么办?
3、Cache Aside Pattern 方案的改进
a、 延期删除
将写操作的『删除 Redis』操作改为异步的延迟删除。例如:更新完数据库,1 秒钟之后再删除缓存,这种情况下,读写并发造成的数据不一致问题最多也就存在 1 秒
这个改进方案的问题在于:你要延迟多久?延迟的时间短了没有解决读写并发问题;延迟的时间越长不一致隐患就越大。
延期的好处:
正常情况下,A线程读操作,B线程写操作,假如说B先更新数据库,然后删除redis缓存,在B删除redis缓存期间,A没有读操作,然后等B删除redis完成,A再查,发现缓存没有,查数据库,然后再更新缓存,这是正常的。
如果是A先读的呢?A先找redis缓存,发现没有,然后查数据库,完了之后,当它将数据库数据写到redis缓存之前,这个时候B更新了数据库,然后删除了缓存,然后A在更新redis缓存,那么redis缓存就是旧数据库了。所以这个时候延期删除的话,目的就是等到A放到缓存之后再删除。那么延期多久呢?这也是个问题
b、借助消息队列,将删存缓存的工作委托给第三方
- 读数据的人,先查redis缓存,发现没有数据时,它去查数据库拿到结果,但是把这个结果放到redis缓存不再由它自己来完成,而是发消息给消息队列,队列监听到数据,然后将数据存redis,不再由他自己来刷新缓存,而是由『别人』来刷新;
- 写数据的人,在更新完数据库之后,同样发送消息给消息队列,表示要删除缓存,队列存该消息,有监听器接收到该消息,去执行删除缓存操作,不再由他自己来删除缓存,而是由『别人』来删除;
- 好处:读和写操作,送到队列这是有先后的,将并行化产生的问题,由串行化顺序执行
- 坏处:代价很大,一旦消息队列宕机,整个过程结束
说明: 这些改建方案不一定用得上。原因在于: 有数据不一致的窗口期,这是可接受的。 改进方案虽然改进了问题,但是同时带来了复杂性