前言

Github:https://github.com/HealerJean

博客:http://blog.healerjean.com

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、重新从消息队列中获得该数据,重试操作。

ContactAuthor