前言:
在读取与写入缓存方面大家都是这么做的:判断是否有缓存数据,无数据的话从数据库加载,若查出数据不为null,则写入缓存,再把数据返回调用方。
但是这里有一个问题需要分析,缓存与数据库的同步,在更新完数据库后,是更新缓存还是删除缓存,还是先删缓存,再更新数据库。从理论上来说,设置过期时间是最终保持一致的解决方案。但是这不是最好的办法,在缓存有效期内或者高并发情况下,会很可能出现读取到的数据与缓存不一致的情况。
三种策略:
1.先更新数据库,再更新缓存
2.先删除缓存,再更新数据库
3.先更新数据库,再删除缓存
不会出现先更新缓存再更新数据库,如果更新数据库失败了,那就完了
1.先更新数据库,再更新缓存
这套方案不太好
a.线程安全角度
(1) 线程A更新了数据库
(2) 线程B更新了数据库
(3) 线程B更新了缓存
(4) 线程A更新了缓存
这样缓存中就出现了错误的数据库
b.业务场景
如果数据库写的场景比较多,读的场景比较少,就会出现数据库频繁更新,缓存频繁更新可能缓存根本就没有读,这样浪费性能
如果写入数据库的值,并不是直接写入缓存的,而是要经过一系列复杂的计算再写入缓存,那么每次写入数据库后,再次计算写入缓存,也是浪费性能的。删除缓存显得更为合适。
2.先删缓存,再更新数据库
高并发操作下,依旧会出现问题
(1) A线程进行写操作,先删除缓存
(2) B线程发现没有缓存,去数据库读取旧值
(3) B线程将旧值写入缓存
(4) A线程更新数据库
这样,数据库中的数据,又是脏数据了,还有在数据库主从分离情况下,中从没来的及同步,结果把未同步的数据写入到了缓存。这怎么解决
采用延时双删策略
redis.delKey(key);
db.update(Data);
new Thread(()->{Thread.sleep(1000);redis.delKey(key);}).start();
在不影响程序响应的情况下,开一个线程去删除缓存, 至于是多少时间后删除,可以自己评估,不一定是1s。
但是这种情况下,如果第二次删除缓存失败了怎么办?
3.先更新数据库,再删缓存
这样也有问题
(1) 缓存刚好失效
(2) A查数据库得到一个旧值
(3) B将新值写入数据库
(4) B删除缓存
(5) A将旧值写入缓存
因为数据库的读比写快,所以这种概率比较低正常顺序应该是1 2 5 3 4,但是也是有可能发生的,也可能是查询出来值后,经过一系列计算,又写入缓存的。解决办法还是延时缓存双删,但是和2一样,删除失败了怎么办?
解决方案1
1.更新数据库
2.删除缓存失败
3.将要删除的key发送到消息队列
4.消费消息删除key,重试直到成功
解决方案2
(1)更新数据库数据
(2)数据库会将操作信息写入binlog日志当中
(3)订阅程序提取出所需要的数据以及key
(4)另起一段非业务代码,获得该信息
(5)尝试删除缓存操作,发现删除失败
(6)将这些信息发送至消息队列
(7)重新从消息队列中获得该数据,重试操作。
备注说明:上述的订阅binlog程序在mysql中有现成的中间件叫canal,可以完成订阅binlog日志的功能。至于oracle中,博主目前不知道有没有现成中间件可以使用。另外,重试机制,博主是采用的是消息队列的方式。如果对一致性要求不是很高,直接在程序中另起一个线程,每隔一段时间去重试即可,这些大家可以灵活自由发挥,只是提供一个思路。
参考文献:
分布式之数据库和缓存双写一致性方案解析
实时内容请关注微信公众号,公众号与博客同时更新:程序员星星