(以两个更新年龄为20和为21的并发请求为案例)
1.更新数据库,然后更新缓存
请求A更新数据库为20,接着准备更新缓存为20。在更新缓存前,请求B进来,更新数据库为21,然后更新缓存为21。这时,请求A的更新缓存操作才开始,更新了缓存为20。这就导致数据库里是新数据21,而缓存里是旧数据20。客户只能查到旧数据。
2.更新缓存,然后更新数据库
请求A更新缓存为20,接着准备更新数据库为20。在更新数据库为20前,请求B进来,更新缓存为21,更新数据库为21,然后请求A才更新数据库为20。这时,缓存里是新数据21,数据库里是旧数据20。
更新数据时,将更新缓存换为删除缓存,等到查询数据时,如果缓存未命中,再去查询数据库,写入缓存。
(为什么将更新缓存换为删除缓存?)
①用户一更新数据就去更新缓存,如果用户一直不去查,更新频率又很高,就会有很多无效写操作。
②删除缓存是一种懒思想的应用。删除一个数据,相较于更新一个数据,更加轻量级,出错概率更小。
那么,删除缓存和更新数据库,谁先呢?
(以一个更新年龄的请求和一个查询年龄的请求为案例)
1.删除缓存,然后更新数据库
请求A要将数据20改为21,先删除缓存20,准备更新数据库为21。在这之前,请求B进来,查询,未命中缓存,去数据库查到20,写入缓存为20。然后,请求A才更新数据库为21。这时,缓存中为20,数据库为21。
2.更新数据库,再删除缓存
查询请求A进来,未命中缓存,读取数据库数据为20,准备写入缓存数据为20。在这之前,请求B进来更新数据20为21,更新了数据库为21,删除了缓存(但这时缓存里没东西,所以是个空删除),然后请求A才将20写入缓存。这时,缓存中为20,数据库中为21。
仍然是两种方案都不行,但数据库操作一般比较耗时,所以写入缓存在数据库操作完成之后才进行的概率很低,即第二种失误发生的概率较低,故选用先更新数据库,再删除缓存的方案。
但如果业务对缓存命中率要求较高的话,我们可以采用更新缓存+更新数据库的方案,因为这样缓存命中率才会比较高。但双更新的方案之前分析过会出现并发问题,因此要对其加锁或者加较短的过期时间来应对。
对于【先删除缓存,再更新数据库】的方案,在高并发状况下的缓存不一致问题,有“延迟双删”的解决方案。
具体操作是:①删除缓存;②更新数据库;③线程休眠一段时间;④再次删除缓存
之前就是因为先删,在更新前另一个请求进来查询,未命中缓存,然后去数据库查,又写入缓存,然后数据库才更新完成。才导致缓存是旧数据,数据库是新数据。线程休眠一段时间,是为了留时间给查询的并发请求将旧数据尽管写到缓存中,然后我再次删除缓存,这样缓存就不会是旧数据了,下次别的请求来查,查不到,就会去数据库查新数据。
但延迟双删要让线程休眠时间大于查询请求查询又写入缓存的时间,这个时间不好估计。
在【更新数据库,然后删除缓存】方案中,我们要保证删除缓存成功。因为如果删除缓存失败,缓存中是旧数据,数据库中是新数据。下次查询请求过来,因为命中了缓存,会一直查到旧数据。就算设置了过期时间,也有一定延时。
为了保证删除缓存成功,有两种解决方案:
①使用消息队列的重试机制:将删除缓存要操作的数据加入消息队列,如果删除失败,从消息队列重新读取数据,然后再次删除。成功删除后,要把数据从消息队列中移除,避免重复操作。(这种方案对代码侵入性较强)
②使用MySQL的binlog日志