Redis缓存与数据库数据一致性
Redis由于将数据保存在内存中,访问速度远远大于基于磁盘的数据库(如MySQL)。但是由于其容量有限,不开启持久化时断电数据容易丢失(开启持久化影响性能)等特点, 因此常常作为数据库的缓存来使用。Redis也主要适合于读多写少,且对一致性要求不是特别高的场景,这是使用Redis的前提。
缓存更新策略
由于引入缓存,数据就会分散在缓存和数据库两处不同的数据源,当数据更新时,事实上很难做到数据一致,除非采用强一致性方案,这里不在进行讨论。关于数据的更新,主要有以下4种模式, 其中Read Through和Write Through放在一起讨论:
- Cache Aside
- Read Through
- Write Through
- Write Back
这四种模式的主要区别在于最新数据在缓存中还是数据库中,由谁进行更新
模式 | 最新数据在哪里 | 由谁更新 |
---|---|---|
Cache Aside | 数据库 | 应用程序更新缓存 |
Read Through/Write Through | 缓存服务更新数据库 | |
Write Back | 缓存 | 缓存服务更新数据库 |
下面分别对这四种模式进行阐述,为保证行文连贯,先假设更新数据库以及缓存都会事务成功,由于某一种更新导致的不一致性在后续章节讨论
1. Cache Aside
Cache Aside顾名思义,就是缓存“靠边站”,只是在访问数据库的主流程上帮个忙,最新的数据还是以数据库为主![](https://img.haomeiwen.com/i13878677/9e4f38b3ddaff9c1.png)
Cache Aside主要有三点:
- 命中:应用程序从cache中取数据,取到后返回。
- 失效:应用程序先从cache取数据,没有得到,则从数据库中取数据,成功后,放到缓存中。
- 更新:先把数据存到数据库中,成功后,应用程序再让缓存失效。
至于为什么是先更新数据库,再让缓存失效, 而不是直接更新缓存,主要是为了保证在并发的情况下,尽可能降低数据不一致出现的概率,具体参考附录,在大概率的情况下先更新数据库再失效缓存能够保证数据一致,也是业界推荐的处理方式,包括Facebook的论文《Scaling Memcache at Facebook》也使用了这个策略。
![](https://img.haomeiwen.com/i13878677/8005821ccb4d271b.png)
![](https://img.haomeiwen.com/i13878677/61f0a31f57c33e2a.png)
2. Read Through与Write Through
Cache Aside 对缓存以及数据库的更新逻辑是由应用程序去控制的,很显然这是一个很复杂的过程。Write/Read Through对调用方而言,缓存是作为整个的数据存储,而不用关心缓存后面的数据库,数据库的更新则是由缓存统一进行管理,对调用方而言只需要和缓存进行交互,整体过程是透明的。可以理解为,应用认为后端就是一个单一的存储,而存储自己维护自己的Cache。如下图,图片来源于Cache (computing)
![](https://img.haomeiwen.com/i13878677/b609f39c64de9135.png)
-
Read Through
查询操作中,当缓存失效的时(过期或LRU换出),由缓存服务负责从数据库中加载数据到缓存中,对应用方是透明的。 -
Write Through
更新数据时,如果没有命中缓存,直接更新数据库,然后返回。
如果命中了缓存,则更新缓存,然后再由缓存服务自己更新数据库(这是一个同步操作)
3. Write Back
这种模式是当数据更新的时候直接更新缓存数据,然后建立异步任务去更新数据库。这种异步方式请求响应会很快,系统的吞吐量会明显提升。但是,因为是异步更新数据库,数据一致性的保障就会变弱,如果更新数据库失败则会永远的造成系统脏数据,需要很精细设计系统重试的策略,另外如果异步服务宕机的话,还要考虑更新的数据如何持久化,服务重启后能够迅速恢复。在更新数据库时,由于并发多任务的存在,还需要考虑并发写是否会造成脏数据的问题,就需要追溯每次更新数据的时序。使用这种模式需要考虑的细节会有很多,设计出一套好的方案是件很不容易的事情。
![](https://img.haomeiwen.com/i13878677/6ea98484e78d7188.png)
数据不一致的原因
1. 数据不一致的原因
- 逻辑失败造成的数据不一致
因为异步读写请求在并发情况下的操作时序导致的数据不一致,称之为”逻辑失败“。解决这种因为并发时序导致的问题,核心的解决思想是将异步操作进行串行化。 - 物理失败造成的数据不一致
在Cache Aside 模式中先更新数据库再删除缓存以及异步双删策略等等,如果删除缓存失败时都出现数据不一致的情况。出于性能的考虑,数据库及缓存的操作不会放在一个事务中,因为缓存操作失败,导致的数据不一致称之为“物理失败”。大多数情况物理失败的情况会重用重试的方式进行解决。
2. 数据最终一致性解决方案:
在绝大部分业务场景中,追求的是最终一致性,针对物理失败造成的数据不一致常用的方案有:消费消息异步删除缓存以及订阅Binlog的方式,针对逻辑失败造成的数据不一致常用的方案有:队列异步操作同步化。
消费消息异步删除缓存
流程如下图所示,主要包括:
- 应用程序更新数据库数据;
- 删除缓存
- 如果缓存删除失败,将删除失败的key 发送到消息队列
- 应用程序自己消费消息,获得需要删除的key
- 继续重试删除操作,直到成功
![](https://img.haomeiwen.com/i13878677/786d961ec7f752bf.png)
订阅binlog
消费消息异步删除缓存有一个缺点,对业务线代码有大量的侵入,所以引出了本方案, 主要流程如下:
- 应用程序更新数据库
- 通过canal 订阅数据库的binlog
- 数据更新服务解析binlog
- 根据解析的binlog更新缓存
- 对于更新失败的,将失败的key发送到消息队列
- 缓存服务订阅消息队列, 重试
![](https://img.haomeiwen.com/i13878677/77a2cf352a59eaa8.png)
总结:
无论是4种更新策略,还是缓存的最终一致性方案,Redis缓存与数据库都会有短时间或者小概率的不一致的风险, 这又回到了开篇Redis适合使用的场景,读多写少, 对一致性要求不高,如果是一致性要求特别高的情况,比如交易场景,则只能使用数据库了。
附录
1. 先更新数据库再失效缓存
- 数据库读请求比写请求快,出现数据不一致概率极低
- 读请求时,缓存刚好失效(图中1、2)
- 读请求从数据库中读取数据并更新缓存期间(图中3、4、9、10),正好有写请求更新数据库并使缓存失效(图中5、6、7、8), 且读数据库比写数据库慢
- 可以通过异步双删的策略以及过期失效的方式来避免这种不一致
![](https://img.haomeiwen.com/i13878677/4bb9e20752673628.png)
2. 先失效缓存再更新数据库
- 数据库读请求比写请求快,容易出现数据不一致
写请求失效缓存并更新数据库期间(图中1、2、7、8),读请求读取数据库旧值并更新缓存(图中3、4、5、6) - 通过延时双删(影响吞吐率),异步双删降低不一致的影响
![](https://img.haomeiwen.com/i13878677/519deae6b5fb2dd5.png)
3. 先更新缓存再更新数据库与先更新数据库再更新缓存
- 并发写容易写覆盖造成脏数据问题
具体如如下两图所示 - 双写不同数据源容易造成数据不一致
同时写数据库以及缓存数据,任何一个更新失败都会造成数据不一致,即物理失败。另外事务都成功,无论是先更新缓存还是再更新数据库,还是先更新数据库再更新缓存,这两种情况在并发的情况下也很容易出现双写不成功。 -
违背数据懒加载,避免不必要的计算消耗(这点还好)
如果有些缓存值是需要经过复杂的计算才能得出,所以如果每次更新数据的时候都更新缓存,但是后续在一段时间内并没有读取该缓存数据,这样就白白浪费了大量的计算性能,完全可以后续由读请求的时候,再去计算即可,这样更符合数据懒加载,降低计算开销。
先更新数据库再更新缓存数据不一致情况(概率大)
![](https://img.haomeiwen.com/i13878677/b43a26e448b2019e.png)