写缓存和写数据,由于不是原子性的,所以会造成数据的不一致。那我们是先写数据库还是先操作缓存呢,缓存操作是更新还是删除呢?
任何脱离业务的设计都是耍流氓。我们试想一个场景,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,只有获取到了锁,才可以操作,这样也可以保证数据的一致性。
当然不管采用哪种方式,数据的一致性和性能的关系是反比的。数据的一致性要求高,性能就会下降;数据的一致性要求不高,性能就会上升。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。