Redis畅通之_缓存一致性
前言
Github:https://github.com/HealerJean
1、缓存和数据库谁先动手?
对于一个不能保证事务性的操作,一定涉及“哪个任务先做,哪个任务后做”的问题,解决这个问题的方向是:如果出现不一致,谁先做对业务的影响较小,就谁先执行。
先淘汰缓存,再写数据库:第一步淘汰缓存成功,第二步写数据库失败,则只会引发一次
Cache miss
先写数据库,再淘汰缓存:第一步写数据库操作成功,第二步淘汰缓存失败,则会出现
DB
中是新数据,Cache
中是旧数据,数据不一致。
1.1、先删缓存,再更新数据库
不使用:分析:试想,两个并发操作,一个是更新操作,另一个是查询操作,更新操作删除缓存后,查询操作没有命中缓存,先把老数据读出来后放到缓存中,然后更新操作更新了数据库。于是,在缓存中的数据还是老的数据,导致缓存中的数据是脏的,而且还一直这样脏下去了。
写流程(更新策略)
1、先淘汰 cache
(删除缓存);
2、再写 DB
(更新数据库)。
读流程
1、先读 cache
,如果数据命中 hit
则返回;
2、如果数据未命中 miss
则读 DB
3、将 DB
中读取出来的数据入缓存。
1.1.1、并发错误流程
1、请求A
进行写操作,删除缓存
2、请求B
查询发现缓存不存在
3、请求B
去数据库查询得到旧值
4、请求B
将旧值写入缓存
5、请求A
将新值写入数据库
1.2、先更新数据库,后删除缓存
不使用:先更新数据库,再删缓存依然会有问题,不过,问题出现的可能性会比较低!
那么在大多数情况下,在不想做过多设计,增加太大工作量的情况下,请先更新数据库,再删缓存
先天条件:步骤3的写数据库操作比步骤2的读数据库操作耗时更短,才有可能使得步骤4先于步骤5。
推理:数据库的读操作的速度远快于写操作的(不然做读写分离干嘛,做读写分离的意义就是因为读操作比较快,耗资源少),因此步骤3耗时比步骤2更短,这一情形很难出现。
1.2.1、错误流程
1、缓存刚好失效
2、请求A
查询数据库,得一个旧值
3、请求B
将新值写入数据库
4、请求B
删除缓存
5、请求A
将查到的旧值写入缓存
1.3、先更新数据库,再更新缓存
不使用,分析:
1、
A
更新缓存应该比请求B
更新缓存早才对,但是因为网络等原因,B
却比A
更早更新了缓存。这就导致了缓存不一致2、数据库更新成功了,缓存更新失败,会出现数据不一致问题
业务场景排斥原因:
1、如果你是一个写数据库场景比较多,而读数据场景比较少的业务需求,采用这种方案就会导致,数据压根还没读到,缓存就被频繁的更新,浪费性能。
业界使用原因:
1、数据库和缓存强一致性,在一个事务里面,如果缓存更新失败,则抛出异常。事务回滚,但是一定要保证添加失败了还会再次添加。否则因为网络原因导致的缓存更新失败(缓存实际有值),会出现不一致的情况。
1.3.1、错误流程
1、线程A更新了数据库;
2、线程B更新了数据库;
3、线程B更新了缓存;
4、线程A更新了缓存;
1.4、先更新缓存,再更新数据库
不使用,分析:
1、更新缓存成功,更新数据库出现异常了,导致缓存数据与数据库数据完全不一致
2、非要数据库和缓存数据强一致
不可能做到,缓存系统适用的场景就是非强一致性的场景,所以它属于CAP中的AP
3、采纳方案
3.1、延迟双删(MT)
3.1.1、流程
1、先淘汰缓存
3、再写数据库(这两步和原来一样)
3、发送延迟队列,延迟N
秒,查缓存和查数据库,进行对比,如果不一致,则淘汰缓存
3.1.2、问题归纳
问题1:为什么是先淘汰缓存,再更新数据库呢?
答案:网上看到的都是先淘汰缓存,个人理解,使用延迟双删,其实也可以先写数据库,再删除缓存。
问题2:这个N
秒,怎么确定呢?
答案:这个N
秒,取决于业务代码执行时间,保证数据库已经修改完成
问题3:如果两次都没删除怎么办?
答案:消息积压,直到删除
3.2、方案2:binlog
订阅
binlog
程序在mysql
中有现成的中间件叫canal
,可以完成订阅binlog
日志的功能
3.1.2.1、流程
1、更新数据库数据
2、数据库会将操作信息写入binlog日志当中
3、订阅程序Canal
中间提取出所需要的数据以及key
4、将这些信息发送至消息队列
5、尝试删除缓存操作,发现删除失败
7、重新从消息队列中获得该数据,重试操作。