Version: Next

双写一致性问题

应对缓存击穿的解决方法xusanyao的博客-CSDN博客缓存击穿解决方案

缓存击穿解决方案 - 简书 (jianshu.com)

你一定要掌握这种缓存读写策略,开发必备

你真的懂Redis与MySQL双写一致性如何保证吗?_Linuxhus的博客-CSDN博客

看facebook的3篇paper,scaling memcache at facebook, tao, 还有flighttracker, 看完就知道满足ryw一致性,高可用的缓存咋设计了

先更新数据库,再更新缓存 (×)

线程安全角度

  • 线程 A 更新数据库
  • 线程 B 更新数据库
  • 由于种种原因,线程 B 率先更新了 缓存
  • 线程 A 更新缓存

导致缓存数据与数据库数据不一致


业务场景

  • 更新的数据未必有人会读取
  • 如果缓存更新依赖数据库重复无意义的聚合函数调用,相当于浪费资源

先删除缓存,再更新数据库

  1. 请求A 操作,第一步先 删除缓存
  2. 请求B 查缓存,发现为空
  3. 请求B 查询数据库,得到旧值
  4. 请求B 将旧值写入缓存
  5. 请求A 将新值写入数据库

延时双删策略

  1. 先删除缓存
  2. 再写数据库
  3. 休眠 1 秒,再删除缓存

在休眠期间缓存与数据库不一致

  • 如何确定休眠时长
    • 自行评估自己项目的读数据业务逻辑耗时,写数据的休眠时间应当在读数据业务逻辑的耗时基础上,加几百ms即可

如果第二次删除缓存失败怎么办

  1. 请求A进行写操作,先删除缓存
  2. 请求B查询发现缓存为空
  3. 请求B查询数据库得到旧值
  4. 请求B将旧值写入缓存
  5. 请求A将新值写入数据库
  6. 请求A尝试将新值写入缓存,但失败了

解决:必须使用其他策略 (先更新数据库,再删除缓存)

使用了 MySQL 读写分离架构的场景

发生数据不一致的场景再现

  1. 请求A 进行了写操作,删除缓存
  2. 请求A 将数据写入数据库
  3. 请求B 查询缓存,发现缓存为空
  4. 请求B 去 从库读 ,这时,还没有完成主从同步,因此读取到 旧值
  5. 请求B 将旧值写入缓存
  6. 数据库完成主从同步,从库变为新值

解决:依然采用 延迟双删

  • 睡眠时间:在 主从同步 的延时时间基础上加几百 ms

采用延时双删,降低了吞吐量,怎么办(异步延时双删)

  • 将第二次删除操作改为 异步,自己创建一个线程,异步删除,这样就不需要睡眠了

先更新数据库,再删除缓存(Cache-Aside Pattern)

Cache-Aside Pattern 中指出

  • 失效:应用程序先从 cache 中取数据,没有得到,则从数据库中取数据,成功后,放到缓存中
  • 命中:程序从 cache 中取数据,取到后返回
  • 更新:先把数据存到数据库中,成功后,再让缓存失效

在 FaceBook 论文 Scaling Memcache at Facebook 中提出,使用 先更新数据库后删除缓存 的策略


是否存在并发问题

不存在,情景再现:

  1. 缓存刚好失效
  2. 请求A查询数据库,得到一个旧值
  3. 请求B将新值写入数据库
  4. 请求B删除缓存
  5. 请求A将查到的旧值写入缓存

上述情况会产生脏数据,但是发生概率很低

  • 因为步骤3写数据库比步骤2读数据库更快,才有可能让步骤4先于步骤5,但是读肯定比写快
  • 如果一定要解决:还是用延时双删,可以用异步的

如果删除缓存操作失败,如何处理

方案一

  • 将删除失败的 缓存键,存入 消息队列,编写业务代码处理消息队列中的内容,在后续的逻辑中尝试再次删除
  • 缺点:与业务逻辑代码耦合度高

方案二

  • 使用中间件 阿里 canal
  • 按照先更新数据库,再删除缓存来做
    1. 先执行更新数据库操作,产生对应的 MySQL Binlog
    2. 基于 canal 的 binlog 订阅程序,提取出操作的数据以及 key
    3. 尝试删除缓存
    4. 基于 canal 将数据与 key 存入消息队列
    5. 基于 canal 处理消息队列中的内容,尝试再次删除