缓存更新策略
[toc]
先更新数据库,再更新缓存
首先这个逻辑是有问题的。
试想,两个更新操作,一个更新数据库为A,但还没来的及更新缓存,另一个更新数据库为B,又更新了缓存值为B,这时候第一个更新操作才更新缓存为A,那样数据库值为B,缓存值为A,数据不一致。
先更新缓存,再更新数据库
这个流程跟上面很类似,出现的问题也很类似。
两个更新操作,一个更新缓存为A,但还没来的及更新数据库,另一个更新缓存为B,又更新了数据库为B,这时候第一个更新操作才更新数据库为A,那样数据库值为A,缓存值为B,数据不一致。
先删除缓存,再更新数据库
这个逻辑也是错误的。
两个并发操作,一个是更新操作,另一个是查询操作,更新操作删除缓存后,查询操作没有命中缓存,先把老数据读出来后放到缓存中,然后更新操作更新了数据库。于是,在缓存中的数据还是老的数据,导致缓存中的数据是脏的,而且还一直这样脏下去了。
先更新数据库,再删除缓存
来看下有没有问题,一个是查询操作,一个是更新操作的并发,首先,没有了删除cache数据的操作了,而是先更新了数据库中的数据,此时,缓存依然有效,所以,并发的查询操作拿的是没有更新的数据,但是,更新操作马上删除缓存,后续的查询操作再把数据从数据库中拉出来。而不会像上面一样产生的问题,后续的查询操作一直都在取老的数据。
为什么不是写完数据库后更新缓存?主要是怕两个并发的写操作导致脏数据。
那么,是不是这个就不会有并发问题了?不是的,比如,一个是读操作,但是没有命中缓存,然后就到数据库中取数据,此时来了一个写操作,写完数据库后,删除缓存,然后,之前的那个读操作再把老的数据放进去,所以,会造成脏数据。
但,这个case理论上会出现,不过,实际上出现的概率可能非常低,因为这个条件需要发生在读缓存时缓存失效,而且并发着有一个写操作。而实际上数据库的写操作会比读操作慢得多,而且还要锁表,而读操作必需在写操作前进入数据库操作,而又要晚于写操作更新缓存,所有的这些条件都具备的概率基本并不大。
缓存删除失败,导致不一致
上面先更新数据库,再删除缓存的策略中,因为要删除缓存,但如果缓存删除失败,就会导致数据库与缓存不一致。这个问题怎么办?一般可以用消息队列解决。
如果删除缓存失败,发送消息投递到消息中间件中,进入消息队列。这样就保证了即使删除消息失败,也会重试。
但是,这个方案有个问题,就是和应用服务的业务代码耦合的比较厉害。代码业务不清晰。那有没有别的方案呢,对业务没有侵入呢?可以利用了mysql的底层机制,binlog日志进行删除缓存,这样就不需要和业务关联,删除缓存服务是独立的。可以利用阿里开源的canal去操作。
读写分离,导致不一致
关于先更新数据库,再删除缓存的策略,我们来看一下另一个场景,数据库的读写分离的场景。写请求在一个库,读请求在另一个库。读写分离时,库与库之间会存在数据延迟,因为存在数据同步。
那再回顾一下流程,就会发现有问题,因为请求B更新数据 在一个库上面,请求A去读取数据时是另一个库。
1)请求B更新值99,删除缓存
2)请求A查询值100(读库数据还没有同步),在更新到缓存中(值为100)
这样就导致不一致,这个场景是经常出现的,不是小概率事件。那我们如何处理呢?
可以不可以这样?预留数据库主从复制的同步时间,将删缓存的操作改为更新缓存并设置这个缓存的失效时间为一个“经验主从同步时间(500ms?)”,这个超时时间比正常的超时时间要短。
来验证一下,之前提到了先更新数据库,再更新缓存可能存在的问题,那这边更新缓存并重设短的失效时间会导致一样的问题吗?两个更新操作,一个更新数据库为A,但还没来的及操作缓存,另一个更新数据库为B,又更新了缓存值为B,并设置了一个短的超时时间,这时候第一个更新操作才更新缓存为A并设置了一个短的超时时间,而此时数据库值是B,这的确会有短暂的不一致性问题,但是缓存会很快失效后,马上会更新值为B,所以还是可以保证最终一致性。