前情回顾

上回程序员阿旺为了提升数据访问的性能,引入 Redis 作为 MySQL 缓存层,但是这件事情并不是那么简单,因为还要考虑 Redis 和 MySQL 双写一致性的问题。

阿旺经过一番周折,最终选用了「先更新数据库,再删缓存」的策略,原因是这个策略即使在并发读写时,也能最大程度保证数据一致性。

聪明的阿旺还搞了个兜底的方案,就是给缓存加上了过期时间。

本以为就这样不会再出现数据一致性的问题,结果将功能上线后,老板还是收到用户的投诉「说自己明明更新了数据,但是数据要过一段时间才生效」,客户接受不了。

老板转告给了阿旺,阿旺得知又有 Bug 就更慌了,立马就登录服务器去排查问题,查看日志后得知了原因。

「先更新数据库, 再删除缓存」其实是两个操作,这次客户投诉的问题就在于,在删除缓存(第二个操作)的时候失败了,导致缓存中的数据是旧值,而数据库是最新值

好在之前给缓存加上了过期时间,所以才会出现客户说得过一段时间才更新生效的现象,假设如果没有这个过期时间的兜底,那后续的请求读到的就会一直是缓存中的旧数据,这样问题就更大了。

所以新的问题来了,如何保证「先更新数据库 ,再删除缓存」这两个操作能执行成功?

阿旺分析出问题后,慌慌张张地向老板汇报了问题。

老板知道事情后,又给了阿旺几天来解决这个问题,画饼的事情这次没有再提了。

  • 阿旺会用什么方式来解决这个问题呢?
  • 老板画的饼事情,能否兑现给阿旺呢?

如何保证两个操作都能执行成功?

这次用户的投诉是因为在删除缓存(第二个操作)的时候失败了,导致缓存还是旧值,而数据库是最新值,造成数据库和缓存数据不一致的问题,会对敏感业务造成影响。

举个例子,来说明下。

应用要把数据 X 的值从 1 更新为 2,先成功更新了数据库,然后在 Redis 缓存中删除 X 的缓存,但是这个操作却失败了,这个时候数据库中 X 的新值为 2,Redis 中的 X 的缓存值为 1,出现了数据库和缓存数据不一致的问题。

那么,后续有访问数据 X 的请求,会先在 Redis 中查询,因为缓存并没有 诶删除,所以会缓存命中,但是读到的却是旧值 1。

其实不管是先操作数据库,还是先操作缓存,只要第二个操作失败都会出现数据一致的问题。

问题原因知道了,该怎么解决呢?有两种方法:

  • 重试机制。
  • 订阅 MySQL binlog,再操作缓存。

先来说第一种。

重试机制

我们可以引入消息队列,将第二个操作(删除缓存)要操作的数据加入到消息队列,由消费者来操作数据。

  • 如果应用删除缓存失败,可以从消息队列中重新读取数据,然后再次删除缓存,这个就是重试机制。当然,如果重试超过的一定次数,还是没有成功,我们就需要向业务层发送报错信息了。
  • 如果删除缓存成功,就要把数据从消息队列中移除,避免重复操作,否则就继续重试。

举个例子,来说明重试机制的过程。

订阅 MySQL binlog,再操作缓存

先更新数据库,再删缓存」的策略的第一步是更新数据库,那么更新数据库成功,就会产生一条变更日志,记录在 binlog 里。

于是我们就可以通过订阅 binlog 日志,拿到具体要操作的数据,然后再执行缓存删除,阿里巴巴开源的 Canal 中间件就是基于这个实现的。

Canal 模拟 MySQL 主从复制的交互协议,把自己伪装成一个 MySQL 的从节点,向 MySQL 主节点发送 dump 请求,MySQL 收到请求后,就会开始推送 Binlog 给 Canal,Canal 解析 Binlog 字节流之后,转换为便于读取的结构化数据,供下游程序订阅使用。

下图是 Canal 的工作原理:

所以,如果要想保证「先更新数据库,再删缓存」策略第二个操作能执行成功,我们可以使用「消息队列来重试缓存的删除」,或者「订阅 MySQL binlog 再操作缓存」,这两种方法有一个共同的特点,都是采用异步操作缓存。

老板发饼啦

阿旺由于对消息队列比较熟悉,所以他决定采用「消息队列来重试缓存的删除」的方案,来解决这次的用户问题。

经过几天几夜的操作,服务器搞定了,立马向老板汇报工作。

老板让阿旺再观察些时间,如果没问题,到中秋节就商量“饼”的事情。

时间过得很快,中秋佳节到了,这期间一直都没有用户反馈数据不一致的问题。

老板见这次阿旺表现很好,没有再出现任何差错,服务器的访问性能也上来了,于是给阿旺发了这个超级大的月饼,你看这个饼又大又圆,就像你的代码又长又多。

阿旺看到这个月饼,哭笑不得,没想到这就是老板画的饼,是真的很大饼。。。。

以上故事纯属虚拟,如有巧合,以你为准。

更多技术交流,关注公众号

 

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐