写缓存和写数据,因为不是原子性的,所以会造成数据的不统一。那咱们是先写数据库还是先操作缓存呢,缓存操作是更新还是删除呢?
任何脱离业务的设计都是耍流氓。咱们试想一个场景,A 服务更新缓存后,很长一段时间内没有人拜访,B 服务也更新了缓存,那 A 更新的缓存不就做了无用功了?更甚的是,如果这个缓存的计算,是极其简单的、耗时的、耗资源的,多来几个操作数据库的操作,这个开销就很大了。所以咱们选的是做删除,只有用到的时候,才去计算缓存,没有用到缓存的时候,不做计算,毛病就是第一次拜访可能会比较慢。
那当初剩下两种状况:
- 先写数据库,再删除缓存。
- 先删除缓存,再写数据库。
另外,咱们也从并发和故障两种状况来思考。
先数据库
并发
如下图所示:
- 服务 A 更新数据,把 100 更改为 99,而后删除缓存。
- 服务 B 读取缓存,此时缓存为空,从数据库读取 99。
- 服务 C 更新数据,把 99 更改为 98,而后删除缓存。
- 服务 B 赋值缓存为 99。
此时,数据库为 98,然而缓存为 99,数据不统一。
故障
如下图所示:
- 服务 A 更新数据,把 100 更改为 99。
- 服务 A 删除缓存失败。
- 服务 B 读取缓存,为 100。
此时缓存数据和数据库数据不统一
先缓存
并发
如下图所示:
- 服务 A 删除缓存。
- 服务 B 读取缓存。
- 服务 B 读取不到缓存,就读取数据库,此时为 100。
- 服务 A 更新数据库。
此时缓存数据和数据库数据不统一
故障
如下图所示:
- 服务 A 更新缓存。
- 服务 A 更新数据库失败。
- 服务 B 读取缓存。
- 服务 B 读取数据库。
因为数据库未做更改,所以缓存数据和数据库数据统一。
解决方案
业务
任何脱离业务的设计都是耍流氓。如果业务能够承受这种不统一,那就采纳 Cache Aside Pattern。如果业务上不承受,那能够让强一致性需要的业务间接读数据库,然而这个性能就会很差,所以咱们有以下的串行计划。
串行
在 java 中,多线程会有数据安全问题,咱们能够用 synchronized 和 cas 来解决,次要的思路就是把并行改串行。
思路一:通过音讯队列
把所有的申请压入队列,因为申请的 key 太多,能够依照 hash 取模的形式,把 key 扩散到无限的队列中。比方有 3 个队列,hash(key)=0,存入第一个队列。
存入的是读、读、写、读、读、写,那执行的时候,跟存入的程序是一样的,因为对同一个 key 的操作是串行的,所以保障了的数据的一致性。
思路二:通过分布式锁
在下面的计划中,如果队列太多,则不好治理,如果队列太少,则那么多的 key 在排队影响了性能,所以咱们能够采纳分布式锁来做。此时的办法相似于 synchronized 和 cas,只有获取到了锁,才能够操作,这样也能够保证数据的一致性。
当然不论采纳哪种形式,数据的一致性和性能的关系是正比的。数据的一致性要求高,性能就会降落;数据的一致性要求不高,性能就会回升。