缓存双写一致性
缓存双写一致性,谈谈你的理解
如果redis中有数据
需要和数据库中的值相同
如果redis中无数据
数据库中的值要是最新值,且准备回写redis
缓存按照操作来分,细分2种
一、只读缓存
二、读写缓存
- 同步直写策略
(1)写数据库后也同步写redis缓存,缓存和数据库中的数据一致;
(2)对于读写缓存来说,要想保证缓存和数据库中的数据一致,就要采用同步直写策略 - 异步缓写策略
(1)正常业务运行中,mysql数据变动了,但是可以在业务上容许出现一定时间后才作用于redis,比如仓库、物流系统
(2)异常情况出现了,不得不将失败的动作重新修补,有可能需要借助kafka或者RabbitMQ等消息中间件,实现重试重写
数据库和缓存一致性的几种更新策略
可以停机的情况
- 挂牌报错,凌晨升级,温馨提示,服务降级
- 单线程,这样重量级的数据操作最好不要多线程
我们讨论4中更新策略
- 先更新数据库,再更新缓存
- 异常问题1
(1)先更新mysql的某商品的库存,当前商品的库存是100,更新为99个
(2)先更新mysql修改为99成功,然后更新redis
(3)此时假设异常出现,更新redis失败了,这导致mysql里面的库存是99而redis里面的还是100
(4)上述发生,会让数据库里面和缓存redis里面数据不一致,读到redis脏数据 - 异常问题2
# 正常逻辑
1 A update mysql 100
2 A update redis 100
3 B update mysql 80
4 B update mysql 80
# 异常逻辑:多线程环境下,A、B两个线程有快有慢,有前有后有并行
1 A update mysql 100
3 B update mysql 80
4 B update redis 80
2 A update redis 100
最终结果,mysql和redis数据不一致
mysql 80,redis 100
- 先更新缓存,再更新数据库
不太推荐:业务上一般把mysql作为底单数据库,保证最后解释
- 异常问题
# 正常逻辑
1 A update redis 100
2 A update mysql 100
3 B update redis 80
4 B update redis 80
# 异常逻辑:多线程环境下,A、B两个线程有快有慢并行
1 A update redis 100
2 B update redis 80
3 B update mysql 80
4 A update mysql 100
- 先删除缓存,再更新数据库
- 异常问题
(1)步骤分析1,先删除缓存,再更新数据库
1 A线程先成功删除了redis里面的数据,然后去更新Mysql,此时mysql正在更新中,还没有结束。(比如网络延时)B 突然出现要来读取缓存数据
2 此时redis里面的数据是空的,B线程来读取,先去读redis里数据(已经被A线程delete掉了),此处出来2个问题:
2.1 B从mysql获得了旧值
B线程发现redis里没有(缓存缺失)马上去mysql里面读取,从数据库里面读取来的是旧值。
2.2 B会把获得的旧值写回redis
获得旧值数据后返回前台并回写进redis(刚被A线程删除的旧数据有极大可能又被写回了)
3 A线程更新完mysql,发现redis里面的缓存是脏数据,A线程直接懵逼了
两个并发操作,一个是更新操作,另一个是查询操作,
A删除缓存后,B查询操作没有命中缓存,B先把laoshuju 读出来后放到缓存中,然后A更新操作更新了数据库。于是,在缓存中的数据还是老的数据,导致缓存中的数据是脏的,而且还一直这样脏下去了。
4 总结流程:
(1)请求A进行写操作,删除redis缓存后,工作正在进行中,更新mysql.....A还没有彻底更新完mysql,还没commit
(2)请求B开工查询,查询redis发现缓存不存在(被A从redis中删除了)
(3)请求B继续,去数据库查询得到了mysql中的旧值(A还没有更新完)
(4)请求B将旧值写回redis缓存
(5)请求A将新值写入mysql数据库 - 解决方案:采用延时双删策略
- 先更新数据库,再删除缓存(
目前主流方案)
-
异常问题
假如缓存删除失败或者来不及,导致请求再次访问redis时缓存命中,读取到的是缓存旧值。 -
解决方案

流程如下图:
(1)更新数据库数据
(2)数据库会将操作信息写入binlog日志当中
(3)订阅程序提取出所需要的数据以及key
(4)另起一段非业务代码,获得该信息
(5)尝试删除缓存操作,发现删除失败
(6)将这些信息发送至消息队列
(7)重新从消息队列中获得该数据,重试操作
- 可以把要删除的缓存值或者是要更新的数据库值暂存到消息队列中(例如使用Kafka/RabbotMQ等)。
- 当程序没有能够成功地删除缓存值或者是更新数据库值时,可以从消息队列中重新读取这些值,然后再次进行删除或更新
- 如果能够成功地删除或更新,我们就要把这些值从消息队列中去除,以免重复操作,此时,我们也可以保证数据库和缓存的数据一致了,否则还需要再次进行重试
- 如果重试超过的一定次数后还是没有成功,我们就需要向业务层发送报错信息了,通知运维人员
- 类似经典的分布式事务问题,只有一个权威答案
最终一致性:
(1)流量充值,先下发短信实际充值可能滞后5分钟,可以接受
(2)电商发货,短信下发但是物流明天见
小总结
在大多数业务场景下,个人建议是(仅代表个人观点),优先`使用先更新数据库,再删除缓存的方案(先更库-后删除)。理由如下:
- 先删除缓存值再更新数据库,有可能导致请求因缓存缺失而访问数据库,给数据库带来压力导致打满mysql
- 如果业务应用中读取数据库和写缓存的时间不好估算,那么,延迟双删中的等待时间就不好设置。
多补充一句:如果使用先更新数据库,再删除缓存的方案
如果业务层要求必须读取一致性的数据,那么我们就需要在更新数据库时,先在Redis缓存客户端暂停并发请求,等数据库更新完,缓存值删除后,再读取数据,从而保证数据一致性,这是理论可以达到的效果,但实际,不推荐,因为真实生产环境中,分布式下很难做到实时一致性,
一般都是最终一致性,请大家参考
| 策略 | 高并发多线程条件下 | 问题 | 现象 | 解决方案 |
| 先删除redis缓存,再更新mysql| 无 | 缓存删除成功但数据库更新失败 | Java程序从数据库中读到旧值 | 再次更新数据库,重试 |
| | 有 | 缓存删除成功但数据库更新中……有并发读请求 | 并发请求从数据库读到旧值并回写到redis,导致后续都是从redis读取到旧值 | 延迟双删 |
| 先更新mysql,再删除redis缓存 | 无 | 数据库更新成功,但缓存删除失败 | Java程序从redis中读到旧值 | 再次删除缓存,重试 |
| | 有 | 数据库更新成功但缓存删除中……有并发读请求 | 并发请求从缓存读到旧值 | 等待redis删除完成,这段时间有数据不一致,短暂存在 |
案例落地实战bitmap/hyperloglog/GEO
UV: Unique Visitor,独立访客,一般理解为客户端IP (去重考虑)
PV:Page View,页面浏览量 (不用去重)
DAU: Daily Active User,日活跃用户量登录或者使用了某个产品的用户数(去重复登录的用户),常用于反映网站、互联网应用或者网络游戏的运营情况
MAU: Monthly Active User,月活跃用户量
| 缓存问题 | 产生原因 | 解决方案 |
|---|---|---|
| 缓存更新方式 | 数据变更、缓存时效性 | 同步更新、失效更新、异步更新、定时更新 |
| 缓存不一致 | 同步更新失败、异步更新 | 增加重试、补偿任务、最终一致 |
| 缓存穿透 | 恶意攻击 | 空对象缓存、bloomfilter过滤器 |
| 缓存击穿 | 热点key失效 | 互斥更新、随机退避、差异失效时间 |
| 缓存雪崩 | 缓存挂掉 | 快速失败熔断、主从模式、集群模式 |

浙公网安备 33010602011771号