前言
在开发中为了减轻数据库的访问压力,使用Redis作为缓存基本是业内共识,但引入新的中间件,必然也会带来更高的复杂度,也就是数据一致性问题。如果Redis和数据库数据不一致,那么就会导致业务上拿到的数据是脏数据,导致系统异常。那么我们应该如何去保证数据的一致性呢?这就是本文要探讨的课题。
首先我们应该理解,Redis作为缓存,数据应该以数据库为准。为了保证一致性,我们更新数据库的同时就应该去更新缓存,这也没有争议,两者更新之间有时间间隔,那么这段时间内数据显然不一致。但是业务代码中完全同时更新是做不到的,因为Redis和数据库是相互独立的系统,一定会有一个先后顺序。因此,我们需要考虑的就是谁先谁后的问题了。
不过在讲写流程之前,我们先把简单的解决,那就是读流程, 读流程是没有争议的。
步骤
1.收到读请求
2.1如果缓存有数据,从缓存中读数据,直接返回数据
2.2如果缓存没有数据,读取数据库中的数据到内存
3.将数据库的数据放入缓存
4.返回数据
按这个流程,无论缓存有没有数据,都不影响正常流程。
接下来来看写流程
写流程有4种方案
1)先更新数据库,再更新缓存
2)先更新缓存,再更新数据库
3)先更新数据库,再删除缓存
4)先删除缓存,再更新数据库
这四种方案大致讨论的是,先处理数据库还是先处理缓存,是直接更新缓存好还是删除缓存好。
如何判断好不好,就看这个方案在高并发下保证数据一致性的能力如何
我们来看看写写并发 的情况
1)先更新数据库,再更新缓存
1线程A更新数据库为1
2线程B更新数据库为2
3线程B更新缓存为2
4线程A更新缓存为1
最终数据库数据为2,缓存数据为1,数据不一致
2)先更新缓存,再更新数据库
1线程A更新缓存为1
2线程B更新缓存为2
3线程B更新数据库为2
4线程A更新数据库为1
最终数据库数据为1,缓存为2,数据不一致
3)先更新数据库,再删除缓存
1线程A更新数据库为1
2线程B更新数据库为2
3线程B删除缓存
4线程A删除缓存
最终数据库值为2,没有缓存,下次查询时,会将最新的数据放到缓存中,数据一致
4)先删除缓存,再更新数据库
1线程A删除缓存
2线程B删除缓存
3线程B更新数据库为2
4线程A更新数据库为1
最终数据库值为1,没有缓存,下次查询时,会将最新的数据放到缓存中,数据一致
可以看到写写并发时,只有方案三和方案四能保证数据一致
那么读写并发时呢
1)先更新数据库,再更新缓存
1线程A更新数据库为1
2线程B读取数据,缓存中有数据,从缓存中获取旧数据
3线程A更新缓存为1
最终数据库数据为1,缓存数据为1,数据一致
2)先更新缓存,再更新数据库
1线程A更新缓存为1
2线程B读取数据,缓存中有数据,从缓存中获取新数据
4线程A更新数据库为1
最终数据库数据为1,缓存为2,数据一致
3)先更新数据库,再删除缓存
0.数据库初始值为1
1.线程A更新数据库为2
2.线程B获取数据,缓存有数据,从缓存中获取旧数据
3.线程A删除缓存
最终数据库为2,没有缓存,下次查询时,缓存获取最新值。数据一致
4)先删除缓存,再更新数据库
0.数据库初始值为1
1.线程A删除缓存
2.线程B获取数据,缓存无数据,从数据库获取值为1
3.线程B将数据1加载到缓存中
4.线程A更新数据库为2
最终数据库为2,缓存中为1。数据不一致
读写并发时,方案一,方案二,方案三可以保证数据一致性
可以看到无论是写写并发还是读写并发,方案三都能保证最终一致性。
这就是是现在最常用,最经典,开发成本最低的方案------Cache-Aside Pattern(旁路缓存模式)
Cache-Aside Pattern(旁路缓存策略)
最常用、最经典的缓存模式。
读流程
1.读请求
2.如果缓存有数据,从缓存中读数据,直接返回数据
3.如果缓存没有数据,读取数据库中的数据
4.将数据库的数据放入缓存,返回数据
写流程
1.写请求
2.更新数据库中的数据
3.删除缓存
那么,这个方案真的没问题吗?非也。
其实上述四种方案在读写并发时,都有一个共同的弊端。我们来看下面这个场景
如果缓存中无数据,发生读写并发时
0.数据库初始值为1
1.线程A获取数据,缓存无数据,从数据库获取值为1
2.线程B更新数据库和缓存为2(无论先后,无论删除或更新),线程B更新操作执行完毕
3.线程A将数据库取到的值1放入缓存中
最终数据库值为2,缓存值为1。导致脏数据,没有保证最终一致性。
可以看到,这种情况下,无论使用哪种方案,都无法完全保证最终一致性。
不可否认,这种情况概率比较低,必须在线程A从数据库拿到旧数据,和将旧数据保存到缓存的中间,完成另一个线程的写操作,才会导致数据不一致。所以对数据一致性要求没那么高的系统使用旁路缓存模式就够了。
但如果一致性要求真的更高的话,该怎么办呢?
延时双删策略
缓存双删策略是一种保证最终一致性的方案,读流程不变,写流程增加了一次删除操作
写流程
1.写请求
2.删除缓存
3.更新数据库
4.休眠一段时间(一次读取操作消耗的时间,比如500毫秒)
5.删除缓存
在旁路缓存策略中,无法保证数据一致性的场景是线程A从数据库获取旧数据,在将旧数据放入缓存之前,一个新的写操作已经完成,此时再将旧数据放入缓存,就会导致数据不一致。
那么我们在更新数据库之后,将线程休眠一段时间,保证其他线程可以完成读取操作就可以了。那么这个休眠时间是多久呢?也很简单,我们只要保证其他线程可以完成读取操作即可,因此休眠时间就应该是一次读操作的耗时。
我们来看看加入休眠操作后的情况
步骤
1.线程A读取旧数据
2.线程B更新数据库
3.线程B休眠500ms
4.线程A将旧数据保存到缓存中
5.线程B删除缓存
可以看到,因为线程B休眠了500ms,所以删除操作几乎一定会在最后执行。也因为是删除操作,所以下次读取一定是最新值,因此保证了最终一致性。
但由于500ms里一直没有删除缓存,那么如果缓存里有旧数据,就会导致这500ms里一直都是脏数据。因此,我们可以再加入一次删除缓存的操作,尽量保证这500ms里缓存里的数据也是最新的。
总结
大部分普通场景下,旁路缓存策略已经够用了,对数据一致性要求更高的场景下,可以使用延时双删策略。