目录
前言
更新数据库+更新缓存:
1.在更新缓存前先加一个分布式锁
2.在更新完缓存时,给缓存加上较短的过期时间
Cache Aside策略
1.先删除缓存,再更新数据库
延迟双删
2.先更新数据库,再删除缓存
保证两个操作都能执行成功
重试机制
订阅MySQLbinlog,再操作缓存
总结
前言
由于引入了缓存,那么在数据更新时,不仅要更新数据库,而且要更新缓存,这两个更新操作存在前后的问题:
- 先更新数据库,再更新缓存;
- 先更新缓存,再更新数据库;
但是会因为并发问题造成缓存和数据库的数据不一致的现象。
如果先更新缓存,在更新数据库:可能会出现下图
缓存和数据库中的数据依然可能不一致。
出现并发问题的时候,当两个请求并发更新同一条数据时,可能会出现缓存和数据库中的数据不一致的现象。
更新数据库+更新缓存:
如果先更新数据库再更新缓存:可能会出现下图情况
此时,数据库中的数据是 2,而缓存中的数据却是 1,出现了缓存和数据库中的数据不一致的现象。
如果业务对缓存命中率有很高的要求,我们也可以采用这个方案。
如何解决这个方式的数据不一致的问题:
1.在更新缓存前先加一个分布式锁
保证同一时间只运行一个请求更新缓存,就不会产生并发问题了,当然引入了锁后,对于写入的性能就会带来影响。
2.在更新完缓存时,给缓存加上较短的过期时间
这样出现缓存不一致的时候,缓存数据也能很快过期,对业务还是能接受的。
Cache Aside策略
不更新缓存,而是删除缓存中的数据,然后到读取数据时,发现缓存中没数据之后,再从数据库中读取数据并更新到缓存中。
写策略与读策略:
1.先删除缓存,再更新数据库
依然在读写并发时,还是会出现缓存和数据库的数据不一致的问题。
针对读写的并发请求而造成不一致的解决办法是
延迟双删
伪代码:
#删除缓存
redis.delKey(X)
#更新数据库
db.update(X)
#睡眠
Thread.sleep(N)
#再删除缓存
redis.delKey(X)
加了个睡眠时间,主要是为了确保请求 A 在睡眠的时候,请求 B 能够在这这一段时间完成「从数据库读取数据,再把缺失的缓存写入缓存」的操作,然后请求 A 睡眠完,再删除缓存。
所以,请求 A 的睡眠时间就需要大于请求 B 「从数据库读取数据 + 写入缓存」的时间。
但是具体睡眠多久其实是个玄学,很难评估出来,所以这个方案也只是尽可能保证一致性而已,极端情况下,依然也会出现缓存不一致的现象。
因此,还是比较建议用「先更新数据库,再删除缓存」的方案。
2.先更新数据库,再删除缓存
还是会出现不一致的问题,但这个概率不高,因为缓存的写入通常要远远快于数据库的写入,所以,先更新数据库再删除缓存是可以保证数据一致性的。
还可以给缓存数据加上过期时间,就算在这期间存在缓存数据不一致,经过过期时间,最终还是能达到一致。
但是这个依然会遇到问题:当删除缓存失败时,缓存中的数据依然是旧数据,没有更新。再给缓存加上过期时间后,还要等一段时间才能更新生效的现象。
保证两个操作都能执行成功
通过重试机制和订阅MySQL binlog,再操作缓存的方式都可以保证两个操作都能成功。
重试机制
引入消息队列,将第二个操作(删除缓存)要操作的数据加入到消息队列,有消费者来操作数据。
如果应用删除缓存失败,可以从消息队列中重新读取数据,然后再次删除缓存。如果重试超过一定次数还没有成功,就需要向业务层发送报错信息。
如果删除缓存成功,就要把数据从消息队列中移除,避免重复操作。
订阅MySQLbinlog,再操作缓存
「先更新数据库,再删缓存」的策略的第一步是更新数据库,那么更新数据库成功,就会产生一条变更日志,记录在 binlog 里。
于是我们就可以通过订阅 binlog 日志,拿到具体要操作的数据,然后再执行缓存删除,阿里巴巴开源的 Canal 中间件就是基于这个实现的。
Canal 模拟 MySQL 主从复制的交互协议,把自己伪装成一个 MySQL 的从节点,向 MySQL 主节点发送 dump 请求,MySQL 收到请求后,就会开始推送 Binlog 给 Canal,Canal 解析 Binlog 字节流之后,转换为便于读取的结构化数据,供下游程序订阅使用。
所以,如果要想保证「先更新数据库,再删缓存」策略第二个操作能执行成功,我们可以使用「消息队列来重试缓存的删除」,或者「订阅 MySQL binlog 再操作缓存」,这两种方法有一个共同的特点,都是采用异步操作缓存。
总结
数据库和缓存保持一致性:
方式 | 问题 | 解决 |
先更新缓存, 再更新数据库 | 数据不一致(并发问题) | 暂无 |
先更新数据库, 再更新缓存 | 同上 | 1.在更新缓存前加上分布式锁; 2.给缓存加上较短的过期时间。 |
先删除缓存, 再更新数据库 | 数据不一致(读写并发) | 延迟双删 |
先更新数据库, 再删除缓存 | 同上 | 1.重试机制; 2.订阅MySQL binlog,再操作缓存。 |
啊,这个要记好多,回头再来背一遍。