项目中用到其他服务一些数据,每次调用接口时候都需要发起请求到另外一个服务获取数据。跨机房和地区的调用,一些网络波动引起的间发性响应缓慢。其实类似于服务树这种较大的数据,每个人的权限变化不会太大,获取的服务树节点可以考虑用缓存,给定一个过期时间,这样极大的解决获取权限树时转圈圈的情况。
使用缓存造成的问题:
使用缓存不得不提到的一点就是缓存和数据库的一致性。是先更新数据库,再操作缓存,还是先操作缓存再更新数据库;另外,缓存是删除还是更新。在高并发场景下,数据不一致的问题暴露的可能性非常大。
几种处理方式:
一、先更新数据库,再更新缓存
正常情况下,一个查询请求过来,先查缓存,缓存没有再查数据库,并将查询到的数据存入缓存;每次修改数据库的同时,修改缓存。流程图如下
出现的问题:
-
多线程问题
请求A更新数据库
请求B更新数据库
请求B更新缓存
请求A更新缓存
很明显可以看出,最后数据库数据为B,而最后更新的缓存数据为A,这时就出现了不一致问题; -
性能问题
在写操作远大于读操作时,会频繁的更新数据库,而读只需要读取最新的一次,导致了很多不必要的更新操作,尤其是缓存是需要经过大量计算的数据时,性能消耗非常严重。
二、先删除缓存,再更新数据库
出现的问题:
- 多线程问题
请求A删除缓存,准备更新数据库,注意此时并未更新数据库
请求B查询缓存为空,查询数据库获得旧数据,填入缓存
请求A更新数据库
这时出现的情况为,缓存为旧数据,数据库为新数据。在数据采用读写分离的情况下,从库同步主库存在延时,也会造成这种脏数据。所以在使用redis作为缓存时,一定要给key一个过期时间(这里还有别的好处)。 - 解决办法
双删策略: 删除缓存,更新数据库,异步或者等待一定时间再次删除缓存。这种解决办法我认为并不能很好的解决此类问题,应为异步或者等待的时间很难把控。
过期时间: 必要手段,能解决最终一致性问题,但是过程中还是会产生脏数据。
三、先更新数据库,再删除缓存
该方式在并发情况下一样会出现问题:
首先缓存失效,
线程A查询缓存为空,从数据库获得一个旧值
线程B更新数据库,此时缓存为空,删不删都无所谓
线程B执行完删除操作后,线程A这时才将旧数据填会缓存
此时可以看到,缓存依旧是旧数据,数据库已经被B更新了。有看到其他博主对此类问题的分析,首先相比于写操作,读操作的执行速度是比较快的,出现上述问题的情况会大大降低。另外,双删策略和过期时间也可以很好的减少此类问题的发生
总结:
上面都只是提到了并发情况下的不一致问题,不管哪一种方式,当出现缓存删除失败的时候,一样会有不一致情况发生。采用异步重试机制,或者是在mysql读写分离场景下使用binlog订阅机制,可以参看链接中的解决方案(https://www.cnblogs.com/rjzheng/p/9041659.html)。以上为个人对缓存和数据库一致性问题的学习。