提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
通常情况下,我们使用缓存的主要目的是为了提升查询的性能。大多数情况下,是这样使用缓存的:
当数据库有数据更新时,在很长的一段时间内(决定于缓存的过期时间),用户请求从缓存中获取到的都可能是旧值,而非数据库的最新值。那么,该如何更新缓存呢?目前有以下四种解决方案:
先更新缓存,再更新数据库(差)
先更新数据库,再更新缓存(差)
先删除缓存,后更新数据库(一般)
先更新数据库,后删除缓存(推荐)
讨论四种方案前先统一两个认知,以便更好理解四种方案:
缺点:如果刚写完缓存,突然网络出现了异常,导致写数据库失败了。这样缓存中的数据就变成脏数据,这个问题非常严重,也是最差的一种解决
缺点一:问题又来了,写数据库成功,但写缓存失败了,依然会造成缓存脏数据的问题。但写缓存失败比写数据库失败的概率要小很多了(因为数据库可能有加锁、外键约束、超时等机制限制),所以此方案要比第一种方案好一点。
很显然,在这个过程当中,请求b在缓存中的新数据,被请求a的旧数据覆盖了。
也就是说:在高并发场景中,如果多个线程同时执行先写数据库,再写缓存的操作,可能会出现数据库是新值,而缓存中是旧值,两边数据不一致的情况。
缺点三:浪费系统资源
删除缓存类
嗯,看起来还不错。即使写数据库失败了,下个请求也会重新触发写缓存操作,基本上避免更新缓存的所有弊端,然而也不是十全十美。
缺点:
这种极端情况下依然会导致写入的缓存为旧值。
推荐1:延迟双删
该方案有个非常关键的地方是:第二次删除缓存,并非立马就删,而是要在一定的时间间隔之后。
sleep的时间要对业务读写缓存的时间做出评估,sleep时间大于读写缓存的时间即可。
那么,为什么一定要间隔一段时间之后,才能删除缓存呢?
请求d卡顿结束,把新值写入数据库后,请求c将数据库中的旧值,更新到缓存中。此时,如果请求d删除太快,在请求c将数据库中的旧值更新到缓存之前,就已经把缓存删除了,这次删除就没任何意义。必须要在请求c更新缓存之后,再删除缓存,才能把旧值及时删除了。
这时,缓存和数据库的数据同样出现不一致的情况了。但这种情况还是比较少的,需要同时满足以下条件:
缓存失效。只有可能是查询比删除慢的情况,而这种情况相对来说会少很多。同时结合延时双删的处理,可以有效的避免缓存不一致的情况。
其实先写数据库,再删缓存的方案,跟缓存双删的方案一样,有一个共同的风险点,即:如果缓存删除失败了怎么办?
缓存设置一个过期时间,比如5分钟。当然这种方案只适合数据更新不是太频繁的业务。
在接口中判断是否删除成功,如果失败就重试,直到成功或超过最大重试次数为止,返回数据。当然,这种方案的缺点就是可能影响接口性能。
将删除缓存任务写入mq等消息中间件中,在mq的consumer中处理。但问题也很多:
引入消息中间件之后,问题更复杂了,对业务代码有一定侵入性、消息丢失怎么办
消息本身的延迟也会带来短暂的不一致性,不过这个延迟相对来说还是可以接受的
比如基于 RocketMQ 的可靠性消息通信,来实现最终一致性。
当一条数据发生修改时,MySQL 就会产生一条变更日志(binlog),我们可以订阅这个日志,拿到具体操作的数据,然后再根据这条数据,去删除对应的缓存。
意思就是我们的业务应用在修改数据时,「只需」修改数据库,无需操作缓存。步骤如下:
缓存删除比更新效果更好
举个例子:如果数据库1小时内更新了1000次,那么缓存也要更新1000次,但是这个缓存可能只在最后一次更新后被读取了1次,那么前999次的更新有必要吗?
反过来,如果是删除的话,就算数据库更新了1000次,那么也只是做了1次缓存删除(删除前判断key是否存在),只有当缓存真正被读取的时候才去数据库加载
删除缓存有两种方式:
针对缓存一致性要求不是很高的场景,那么只通过设置超时时间就可以了。