Version: Next
双写一致性问题
应对缓存击穿的解决方法xusanyao的博客-CSDN博客缓存击穿解决方案
你真的懂Redis与MySQL双写一致性如何保证吗?_Linuxhus的博客-CSDN博客
看facebook的3篇paper,scaling memcache at facebook, tao, 还有flighttracker, 看完就知道满足ryw一致性,高可用的缓存咋设计了
先更新数据库,再更新缓存 (×)
线程安全角度
- 线程 A 更新数据库
- 线程 B 更新数据库
- 由于种种原因,线程 B 率先更新了 缓存
- 线程 A 更新缓存
导致缓存数据与数据库数据不一致
业务场景
- 更新的数据未必有人会读取
- 如果缓存更新依赖数据库重复无意义的聚合函数调用,相当于浪费资源
先删除缓存,再更新数据库
- 请求A
写
操作,第一步先删除缓存
- 请求B 查缓存,发现为空
- 请求B 查询数据库,得到旧值
- 请求B 将
旧值
写入缓存
- 请求A 将
新值
写入数据库
延时双删策略
- 先删除缓存
- 再写数据库
- 休眠 1 秒,再删除缓存
在休眠期间缓存与数据库不一致
- 如何确定休眠时长
- 自行评估自己项目的读数据业务逻辑耗时,写数据的休眠时间应当在读数据业务逻辑的耗时基础上,加几百ms即可
如果第二次删除缓存失败怎么办
- 请求A进行写操作,先删除缓存
- 请求B查询发现缓存为空
- 请求B查询数据库得到旧值
- 请求B将旧值写入缓存
- 请求A将新值写入数据库
- 请求A尝试将新值写入缓存,但失败了
解决:必须使用其他策略 (先更新数据库,再删除缓存)
使用了 MySQL 读写分离架构的场景
发生数据不一致的场景再现
- 请求A 进行了写操作,删除缓存
- 请求A 将数据写入数据库
- 请求B 查询缓存,发现缓存为空
- 请求B 去
从库读
,这时,还没有完成主从同步,因此读取到旧值
- 请求B 将旧值写入缓存
- 数据库完成主从同步,从库变为新值
解决:依然采用
延迟双删
- 睡眠时间:在
主从同步
的延时时间基础上加几百 ms采用延时双删,降低了吞吐量,怎么办(异步延时双删)
- 将第二次删除操作改为
异步
,自己创建一个线程,异步删除,这样就不需要睡眠了
先更新数据库,再删除缓存(Cache-Aside Pattern)
在
Cache-Aside Pattern
中指出
- 失效:应用程序先从 cache 中取数据,没有得到,则从数据库中取数据,成功后,放到缓存中
- 命中:程序从 cache 中取数据,取到后返回
- 更新:先把数据存到数据库中,成功后,再让缓存失效
在 FaceBook 论文
Scaling Memcache at Facebook
中提出,使用先更新数据库后删除缓存
的策略是否存在并发问题
不存在,情景再现:
- 缓存刚好失效
- 请求A查询数据库,得到一个旧值
- 请求B将新值写入数据库
- 请求B删除缓存
- 请求A将查到的旧值写入缓存
上述情况会产生脏数据,但是发生概率很低
- 因为步骤3写数据库比步骤2读数据库更快,才有可能让步骤4先于步骤5,但是读肯定比写快
- 如果一定要解决:还是用延时双删,可以用异步的
如果删除缓存操作失败,如何处理
方案一
- 将删除失败的
缓存键
,存入消息队列
,编写业务代码处理消息队列中的内容,在后续的逻辑中尝试再次删除- 缺点:与业务逻辑代码耦合度高
方案二
- 使用中间件 阿里
canal
- 按照先更新数据库,再删除缓存来做
- 先执行更新数据库操作,产生对应的 MySQL Binlog
- 基于 canal 的 binlog 订阅程序,提取出操作的数据以及 key
- 尝试删除缓存
- 基于 canal 将数据与 key 存入消息队列
- 基于 canal 处理消息队列中的内容,尝试再次删除