了解双写一致性方案
解决方案
- 读请求写请求串行化执行
- 先更新数据库,再更新缓存
- 先删除缓存,再更新数据库
- Cache Aside Pattern
一、读写请求串行化
串行化执行,保证数据一致性,但是会大幅降低系统吞吐量,不采用
二、先更新数据库,再更新缓存
当数据库数据更新时,先更新数据库中数据,再更新对应的缓存数据。这个方案主要有两个问题。
1.问题1
如果同时有A、B两个请求进行更新操作,那么该方案可能会有以下情况:
- A请求更新数据库
- B请求更新数据库
- B请求更新缓存
- A请求更新缓存
本来B请求更新后的数据时最新数据,但是A更改请求却晚于B请求,导致了缓存中是旧数据,数据库与缓存的数据不一致。而且如果缓存没有设置过期时间,那么缓存中的永远是脏数据。
2.问题2
缓存中缓存的数据有时候并不一定是从数据库中读取出来的,也有可能是多张表进行计算得出的结果。这个计算过程本身就是耗时的,而且这个缓存数据并不一定是热点数据(访问次数不多、频率不高),这时如果每次修改数据库值,就要重新进行一次耗时的计算,这会使得更新缓存所花费的代价得不偿失(花费时间、资源多,访问次数却很少)
三、先删除缓存,再更新数据库
因为更新缓存的代价可能会很大,所以又引出了删除缓存的策略
1.为什么先删除缓存
如果先跟新数据库,在删除缓存,那么可能会造成以下问题:
- 数据库更新成功,但是缓存删除失败,从而造成数据不一致
2.问题1
有两个请求,A请求更新,B请求查询,那么该方案可能会出现以下情况:
- A请求更新,所以先删除缓存
- B请求查询,缓存已经被删除
- B请求查询数据库,得到旧的值
- B请求将旧的值返回并写入缓存
- A请求将新的值写入数据库
可以看到,这种情况下,缓存中出现了脏数据。而且如果缓存没有过期时间,那么脏数据将一直存在。
问题1解决方案——延时双删策略
操作流程:
- 先删除缓存
- 写入数据库
- 休眠1秒,再次删除缓存。这样就可以将在1秒内所造成的脏数据再次删除。
休眠时间的确定: 应该评估项目的读数据业务逻辑的耗时。然后写数据的休眠时间则在读数据业务逻辑的耗时基础上,加几百ms即可。这么做的目的,就是确保读请求结束,写请求可以删除读请求造成的缓存脏数据。
3.问题2
延时双删第二次的删除失败怎么办(引出Cache Aside Pattern)
四、Cache Aside Pattern
思想: 先更新数据库,再删除缓存
对于读写请求,定义了三种情况
情况 | 描述 |
---|---|
失效 | 先从cache中读数据,缓存中不存在,则从数据库中取数据,成功后写入缓存并返回 |
命中 | cache存在数据,直接从缓存读取返回 |
更新 | 先把数据更新到数据库,然后删除缓存 |
1.问题1
假设A请求查询,B请求更新:
- 缓存刚好失效
- 请求A查询数据库,得一个旧值
- 请求B将新值写入数据库
- 请求B删除缓存
- 请求A将查到的旧值写入缓存
这种情况要发生,必须是B写入数据库的耗时小于A请求查询的耗时(步骤3耗时小于步骤2),才有可能使得步骤4优先发生于步骤5,但是读操作是远远快鱼写操作的,所以这种情况非常难发生
解决方案
方案1: 设置缓存过期时间
方案2: 采用异步延时双删策略,保证读请求完成后,再次进行缓存删除操作
2.问题2
第二次删除缓存失败
解决方案
提供一个保障的重试机制
方案1:
- 更新数据库数据
- 缓存因为种种问题删除失败
- 将需要删除的key发送至消息队列
- 自己消费消息,获得需要删除的key
- 继续重试删除操作,直到成功
(网图侵删)
弊端:对业务代码造成大量入侵
方案2:
启动一个订阅程序去订阅数据库的binlog,获得需要操作的数据。在应用程序中,另起一段程序,获得这个订阅程序传来的信息,进行删除缓存操作。
- 更新数据库数据
- 数据库会将操作信息写入binlog日志当中
- 订阅程序提取出所需要的数据以及key
- 另起一段非业务代码,获得该信息
- 尝试删除缓存操作,发现删除失败
- 将这些信息发送至消息队列
- 重新从消息队列中获得该数据,重试操作
(网图侵删)