共计 1942 个字符,预计需要花费 5 分钟才能阅读完成。
一、前言
最近在线上遇到了因缓存和数据库数据不统一而引发的问题。经排查后,发现是因为咱们写操作采纳的策略是 先写数据库,再删缓存 。如果写操作后马上去读的话,因为缓存被删,会去数据库查数据。又因为数据库主从提早,会加载从库的旧数据到缓存。于是产生了 数据库已被批改为新数据,但缓存仍然是旧数据的状况。
这次线上的事件,引申出了一个陈词滥调的话题,如何保障数据库与缓存一致性?明天咱们就来谈谈这个事。
二、不统一场景剖析
1、无并发时
首先看一下最简略的状况。当一个写申请心愿把数据从 D1 改成 D2。
因为咱们写申请须要进行两个操作:1、写数据库;2、更新 / 删除缓存。这两个操作并非是一个事务,所以必然有可能产生其中一个胜利一个失败的状况(个别是后一个,因为如果前一个操作失败,个别不会再做第二个操作)。这样会引申出以下两种状况。
- 先将 DB 数据由 D1 批改为 D2
- 再删除或更新缓存(该步骤产生异样,即失败了)
最终,数据库数据是 D2,缓存数据仍旧为 D1。
- 先更新缓存数据由 D1 到 D2
- 更新数据库产生异样
最终,数据库数据是 D1,缓存数据已被批改为 D2。
2、并发时
接下来探讨更简单的状况,也就是在读写并发时可能产生的不统一。
首先明确的是,在读未命中缓存的时候,咱们的做法个别是去查数据库,而后把查到的值写入到缓存中。但在写数据时,策略往往不尽相同。咱们经常会思考两个问题。1)先操作缓存还是先操作数据库;2)删缓存还是更新缓存。
1)先 DB 后缓存
1. 写数据库后更新缓存
- 首先是未命中读 + 写操作并发的场景。
- 线程 A 读缓存,未命中
- 线程 A 读 DB,失去 Data1
- 线程 B 写 DB,将数据从 Data1 更新至 Data2
- 线程 B 写缓存,更新为 Data2
- 线程 A 写缓存,更新为之前读到的 Data1
最终,DB 值为 Data2,但缓存中值为 Data1。
- 其次是写操作并发的场景。
- 线程 A 写 DB,写入 Data1
- 线程 B 写 DB,写入 Data2
- 线程 A 更新缓存,写入 Data1
- 线程 B 更新缓存,写入 Data2
最终,DB 值为 Data2,缓存值为 Data1.
2. 写数据库后删除缓存
写数据库之后删除缓存,仿佛能够解决以上的问题,如下图所示。
但这不是万能的,就例如我在开篇提到的线上问题,采纳的正是写 DB+ 删缓存策略。因为咱们我的项目读 QPS 十分大,但写 QPS 不高。故采纳了读写拆散的主从架构。写申请都在主库上进行,读申请则拜访从库,并依赖主从同步保证数据统一。因为主从同步须要工夫,就可能产生以下的状况导致 DB 与缓存数据不统一。
2)先缓存后 DB
1. 删缓存后写数据库
- 线程 A 写申请,先删缓存
- 线程 B 读缓存,未命中
- 线程 B 读 DB,失去 D1
- 线程 A 写数据库,D1 更新为 D2
- 线程 B 写缓存 D1
最终,DB 的数据是 D2,而缓存是 D1。
2. 更新缓存后写数据库
- 线程 B 读缓存,未命中
- 线程 A 写申请,缓存由 D1 更新至 D2
- 线程 B 读 DB,失去 D1
- 线程 A 写 DB,有 D1 更新至 D2
- 线程 B 写缓存 D1
最终,DB 的数据是 D2,缓存的数据是 D1。
三、不统一的解决形式
在缓存与数据库不统一之后,若过期工夫十分长,且期间没有写操作,会造成读的时候有很长一段时间数据是谬误的。那么如何去修改或者说尽量保障统一呢?
1、提早双删
顾名思义,提早双删就是在写完数据库之后,隔一小段时间 \(\Delta(T) \),再删一次缓存。当然第二次删缓存是异步进行的。
对以下两种状况,采纳提早双删策略后,都能保障在一段时间后,缓存中的脏数据被删除。也就是达到了最终一致性。但期间可能有申请读到的是脏数据。
这一小段时间 \(\Delta(T) \)该怎么取值呢?首先晓得,\(\Delta(T) \)之后再删一次的目标是为了删除并发的未命中读产生的脏数据。所以个别要略大于一次读的申请,且略大于主从同步的提早。
2、删除缓存重试机制
删缓存这一步可能会产生异样,为了保障删缓存胜利,能够引入重试机制。对于删缓存失败的操作,进入重试队列。重试队列选型能够是 Kafka,也能够是 Redis 中的列表。对于一致性要求没那么高的,甚至能够在单机内存中寄存队列。
3、读取 binlog 校对缓存
应用组件 / 中间件获取数据库的 binlog。binlog 若采纳 Row 模式,解析后个别会有数据行最新数据的信息。通过这个信息去查缓存,若发现不统一则删除缓存;若统一,则不作解决。
四、总结
其实最终应用哪种策略去写数据,都要根据本人服务的个性来做取舍,并没有万能的策略(除非应用串行化或者做很多限度保证数据强统一,这时会升高零碎可用性)。
业界常常应用的 Cache Aside
策略,也就是对于写申请 先更新数据库再删缓存 的这种做法,在咱们的服务中会遇到不少问题。所以最终改成了 先更新数据库再更新缓存。
对于线上的状况,能够尝试不同的策略,并在后盾做数据库与缓存的一致性统计,联合业务特点抉择最合适的计划。