先更新数据库,再更新缓存
首先这个逻辑是有问题的。试想,两个更新操作,一个更新数据库为A,但还没来的及更新缓存,另一个更新数据库为B,又更新了缓存值为B,这时候第一个更新操作才更新缓存为A,那样数据库值为B,缓存值为A,数据不一致。
先更新缓存,再更新数据库
这个流程跟上面很类似,出现的问题也很类似。两个更新操作,一个更新缓存为A,但还没来的及更新数据库,另一个更新缓存为B,又更新了数据库为B,这时候第一个更新操作才更新数据库为A,那样数据库值为A,缓存值为B,数据不一致。
先删除缓存,再更新数据库
这个逻辑也是错误的。两个并发操作,一个是更新操作,另一个是查询操作,更新操作删除缓存后,查询操作没有命中缓存,先把老数据读出来后放到缓存中,然后更新操作更新了数据库。于是,在缓存中的数据还是老的数据,导致缓存中的数据是脏的,而且还一直这样脏下去了。
先更新数据库,再删除缓存(Cache-Aside pattern)
来看下有没有问题,一个是查询操作,一个是更新操作的并发,首先,没有了删除cache数据的操作了,而是先更新了数据库中的数据,此时,缓存依然有效,所以,并发的查询操作拿的是没有更新的数据,但是,更新操作马上删除缓存,后续的查询操作再把数据从数据库中拉出来。而不会像上面一样产生的问题,后续的查询操作一直都在取老的数据。
为什么不是写完数据库后更新缓存?主要是怕两个并发的写操作导致脏数据。
那么,是不是这个就不会有并发问题了?不是的,比如,一个是读操作,但是没有命中缓存,然后就到数据库中取数据,此时来了一个写操作,写完数据库后,删除缓存,然后,之前的那个读操作再把老的数据放进去,所以,会造成脏数据。
但,这个case理论上会出现,不过,实际上出现的概率可能非常低,因为这个条件需要发生在读缓存时缓存失效,而且并发着有一个写操作。而实际上数据库的写操作会比读操作慢得多,而且还要锁表,而读操作必需在写操作前进入数据库操作,而又要晚于写操作更新缓存,所有的这些条件都具备的概率基本并不大。
上面先更新数据库,再删除缓存的策略中,因为要删除缓存,但如果缓存删除失败,就会导致数据库与缓存不一致。这个问题怎么办?一般可以用消息队列解决。
如果删除缓存失败,发送消息投递到消息中间件中,进入消息队列。这样就保证了即使删除消息失败,也会重试。
但是,这个方案有个问题,就是和应用服务的业务代码耦合的比较厉害。代码业务不清晰。那有没有别的方案呢,对业务没有侵入呢?可以利用了mysql的底层机制,binlog日志进行删除缓存,这样就不需要和业务关联,删除缓存服务是独立的。可以利用阿里开源的canal去操作。
关于先更新数据库,再删除缓存的策略,再来看一下另一个场景,数据库的读写分离的场景。写请求在一个库,读请求在另一个库。读写分离时,库与库之间会存在数据延迟,因为存在数据同步。
那再回顾一下更新流程,就会发现有问题,因为请求B更新数据在主库上面,请求A去读取数据时是另一个库。比如一个请求更新数据库值为A,删除缓存,另一个请求查询值为旧值B,(读库数据还没有同步),然后更新到缓存为B。这样就导致不一致,这个场景是经常出现的。那如何处理呢?
可以不可以这样?预留数据库主从复制的同步时间,将删缓存的操作改为更新缓存并设置这个缓存的失效时间为一个“经验主从同步时间(500ms?)”,这个超时时间比正常的超时时间要短。
来验证一下,之前提到了先更新数据库,再更新缓存可能存在的问题,那这边更新缓存并重设短的失效时间会导致一样的问题吗?两个更新操作,一个更新数据库为A,但还没来的及操作缓存,另一个更新数据库为B,又更新了缓存值为B,并设置了一个短的超时时间,这时候第一个更新操作才更新缓存为A并设置了一个短的超时时间,而此时数据库值是B,这的确会有短暂的不一致性问题,但是缓存会很快失效后,马上会更新值为B,所以还是可以保证最终一致性。
读取缓存步骤一般没有什么问题,但是一旦涉及到数据更新:数据库和缓存更新,就容易出现缓存和数据库间的数据一致性问题。
不管是先写数据库,再删除缓存;还是先删除缓存,再写库,都有可能出现数据不一致的情况。举个例子:
- 如果删除了缓存Redis,还没有来得及写库MySQL,另一个线程就来读取,发现缓存为空,则去数据库中读取数据写入缓存,此时缓存中为脏数据。
- 如果先写了库,在删除缓存前,写库的线程宕机了,没有删除掉缓存,则也会出现数据不一致情况。
因为写和读是并发的,没法保证顺序,就会出现缓存和数据库的数据不一致的问题。如何解决?
所以结合前面例子的两种删除情况,可以考虑前后双删加懒加载模式。那么什么是懒加载?就是当业务读取数据的时候再从存储层加载的模式,而不是更新后主动刷新。
延迟双删
在写库前后都进行redis.del(key)操作,并且第二次删除通过延迟的方式进行。
方案一(一种思路,不严谨)具体步骤是:
1)先删除缓存;
2)再写数据库;
3)休眠500毫秒(根据具体的业务时间来定);
4)再次删除缓存。
那么,这个500毫秒怎么确定的,具体该休眠多久呢?需要评估自己的项目的读数据业务逻辑的耗时。这么做的目的,就是确保读请求结束,写请求可以删除读请求造成的缓存脏数据。当然,这种策略还要考虑 redis 和数据库主从同步的耗时。最后的写数据的休眠时间:则在读数据业务逻辑的耗时的基础上,加上几百ms即可。比如:休眠1秒。
方案二,异步延迟删除:
1)先删除缓存;
2)再写数据库;
3)触发异步写人串行化mq(也可以采取一种key+version的分布式锁);
4)mq接受再次删除缓存。
异步删除对线上业务无影响,串行化处理保障并发情况下正确删除。
为什么要双删?
db更新分为两个阶段,更新前及更新后,更新前的删除很容易理解,在db更新的过程中由于读取的操作存在并发可能,会出现缓存重新写入数据,这时就需要更新后的删除。
双删失败如何处理?
1、设置缓存过期时间
从理论上来说,给缓存设置过期时间,是保证最终一致性的解决方案。所有的写操作以数据库为准,只要到达缓存过期时间,则后面的读请求自然会从数据库中读取新值然后回填缓存。
结合双删策略+缓存超时设置,这样最差的情况就是在超时时间内数据存在不一致。
2、重试方案
重试方案有两种实现,一种在业务层做,另外一种实现中间件负责处理。