保证数据库与缓存一致性 你是怎么做的

最近建了一个技术交流群,欢迎志同道合的同学加入,群里主要讨论:分享业务解决方案、深度分析面试题并解答工作中遇到的问题,同时也能为我提供写作的素材。

欢迎加Q:312519302,进群讨论

前言

在工作中,大多数的系统都在使用缓存,那你有没有想过为什么要使用缓存?使用缓存后,数据与缓存的一致性如何保证?

带着上面的问题,我们一起探索。

我们刚开始做一个项目的时候,刚起步,流量很小,直接读写数据库即可,性能不错,系统稳定,架构如下图:

随时时间推移,系统运行一段时间,老板说,要推广我们的系统,给用户赋能,接着搞一波营销,流量激增,结果系统报警了,系统都快挂了,赶紧排查,发现性能瓶颈在数据库

这好办,给服务器加上 Redis,让其作为数据库的缓存。

这样,在客户端请求数据时,如果能在缓存中命中数据,那就查询缓存,不用在去查询数据库,从而减轻数据库的压力,提高服务器的性能。

那我们的架构变为:

看起来非常美好,但是有个问提挡在我们面前,如果数据库的数据有修改的时候,我们是先更新数据库,还是先更新缓存呢

由于引入了缓存,那么在数据更新时,不仅要更新数据库,而且要更新缓存,这两个更新操作存在前后的问题

  • 先更新数据库,再更新缓存
  • 先更新缓存,再更新数据库

想了想,为了保证数据是最新的数据,那我们选择 先更新数据库,再更新缓存, 一顿操作猛如虎,系统优化完成后,上线,数据库压力也下来,吞吐量得到明显的提高,心里那个激动啊,我TM真是牛逼,等着老板的嘉奖。

过了一段时间后,有人反馈,我修改了数据后,查看详情,发现数据没变,但是我修改数据的时候,提示我修改成功。擦,难道是系统出bug了?

登录服务器排查,没发现有更新数据库、缓存的失败的信息,但是发现个问题,缓存是修改前的数据,数据是修改后的数据,那这种问题是怎么产生的呢?

进一步分析,发现缓存与数据库不一致的原因是并发导致的

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

并发为什么能导致数据与缓存不一致呢,我们接着分析,我举个例子,用户A与用户B同时修改一条数据

执行步骤:

  1. 用户A更新数据库为1
  2. 用户B更新数据库为2****
  3. 用户B更新缓存2
  4. 用户A更新缓存为1

从上面可以看出,数据库为2,缓存也应该为1,这就造成了缓存与数据库不一致的现象

如果我们先更新缓存,再更新数据库呢?

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

执行步骤:

  1. 用户A更新缓存为1
  2. 用户B更新缓存2
  3. 用户B更新数据库为2
  4. 用户A更新数据库为1

从上面可以看出,数据库为1,缓存也应该为2,还是造成了缓存与数据库不一致的现象


结论: 无论是先更新数据库,再更新缓存,还是先更新缓存,再更新数据库,在并发访问修改一条数据的时候,都会出现不一致的情况

想了想,那不更新缓存了,直接删除缓存。也就是数据库后更新后,直接删除缓存。 在读取数据的时候,查看缓存中有没有数据,没有数据查询数据库,然后再更新缓存。

这个策略有个名字:Cache Aside 策略, 中文:旁路缓存策略

旁路缓存策略

Cache Aside(旁路缓存)策略是最常用的,应用程序直接与「数据库、缓存」交互,并负责对缓存的维护,该策略又可以细分为「读策略」和「写策略」。

写策略的步骤:

  • 先更新数据库中的数据,再删除缓存中的数据。

读策略的步骤:

  • 如果读取的数据命中了缓存,则直接返回数据;
  • 如果读取的数据没有命中缓存,则从数据库中读取数据,然后将数据写入到缓存,并且返回给用户。

问题来了,那我们是先更新数据库,再删除缓存还是先删除缓存,再更新数据库?

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

执行步骤:

  1. 用户A删除缓存
  2. 用户B查询缓存,缓存中没有数据
  3. 用户B查询数据库,得到数据
  4. 用户B更新缓存数据
  5. 用户A更新数据库为最新的值

从结果看,缓存的是旧值:1,数据库是最新值:2,在并发读+写的场景下,依然数据库跟缓存不一致。

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

执行步骤

  1. 用户A查询缓存,缓存未命中
  2. 用户A查询数据库,得到值为:1
  3. 用户B更新数据库,值更新为:2
  4. 用户B删除缓存
  5. 用户A,把查询到的值1,回写到缓存

从结果看,缓存的是旧值:1,数据库是最新值:2,在并发读+写的场景下,依然数据库跟缓存不一致。

从理论上来分析,先更新数据,再删除缓存,依然存在数据库与缓存不一致的情况,但在实际中,出现不一致的情况概率非常低。

因为缓存的写入速度远远大于写入数据库的速度 ,为了以防万一,再给缓存数据加一个过期时间,如果真出现不一致的情况,也最多在过期时间的这个区间不一致,过期时间到了,重新更新缓存,也能到达最终一致性

方案选择

以下四种方案,我们都分析了,那我们到底用哪一种呢?

  1. 先更新数据库,再更新缓存
  2. 先更新缓存,再更新数据库
  3. 先更新数据库,再删除缓存
  4. 先删除缓存,再更新数据库

先说我的观点,也是大厂的方案,强烈推荐:先更新数据库,再删除缓存

原因如下

  1. 缓存的写入速度远远大于写入数据库的速度,出现不一致的的概率很低
  2. 设置过期时间,如果真出现不一致的情况,过期时间到期,重新刷新缓存,能达到最终一致性
  3. 正常的情况,缓存与数据强一致性,也没那么高的要求,如果真要达到强一致性,系统吞吐量必要下降

题外话:对于一致性的解决方案,我对大厂的方案很感兴趣,想了解他他们是怎么解决的。当进去后,深入了解下,使用的方案也是:先更新数据库,再删除缓存 ,最终一致性,没必要强一致性,原因跟上面三条差不多。

这样是不是很完美了嘛,等等,这种方案还有没有问题,想想................,看下图:

更新数据库,删除缓存是两个操作,如果删除缓存失败了呢(这种概率虽然很低),那缓存中依然是旧值,有没有什么方案解决这个问题呢?

上面的四种方案,无论是先操作数据库,还是操作缓存,都存在这种问题

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

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

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

重试机制

引入消息中间件,比如:rabbitmq

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

订阅 MySQL binlog,再操作缓存

引入canal中间件

canal,译意为水道/管道/沟渠,主要用途是基于 MySQL 数据库增量日志解析,提供增量数据订阅和消费。

canal 工作原理

  • canal 模拟 MySQL slave 的交互协议,伪装自己为 MySQL slave ,向 MySQL master 发送dump 协议
  • MySQL master 收到 dump 请求,开始推送 binary log 给 slave (即 canal )
  • canal 解析 binary log 对象(原始为 byte 流)

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

总结

如何保证数据库与缓存一致性?先更新数据库,再删缓存,缓存设置一个过期时间, 这种方案能适用95%以上的场景。

不知道你们在实际工作中,你们的项目,用的那种方案,踩过哪些坑,欢迎留言一起谈论。

相关推荐
苏打水com1 分钟前
数据库进阶实战:从性能优化到分布式架构的核心突破
数据库·后端
shan~~44 分钟前
linux达梦数据库操作
linux·数据库·chrome
间彧1 小时前
Spring Cloud Gateway与Kong或Nginx等API网关相比有哪些优劣势?
后端
间彧1 小时前
如何基于Spring Cloud Gateway实现灰度发布的具体配置示例?
后端
间彧1 小时前
在实际项目中如何设计一个高可用的Spring Cloud Gateway集群?
后端
间彧1 小时前
如何为Spring Cloud Gateway配置具体的负载均衡策略?
后端
间彧1 小时前
Spring Cloud Gateway详解与应用实战
后端
武文斌771 小时前
项目学习总结:LVGL图形参数动态变化、开发板的GDB调试、sqlite3移植、MQTT协议、心跳包
linux·开发语言·网络·arm开发·数据库·嵌入式硬件·学习
CoderIsArt1 小时前
SQLite架构
数据库·sqlite
lixora2 小时前
银河麒麟高级服务器操作系统(ADM64 版)V10(SP1)搭建 Oracle 19c RAC
数据库