文章目录
- 起因
- 更新缓存的策略问题:当缓存中的内容变化时,是选择修改缓存(update),还是直接淘汰缓存(delete)?
- 淘汰缓存
- 更新缓存
- 若有操作失败情况产生
- 执行顺序的问题:先更新缓存还是先更新数据库?
- 先删缓存,在更数据库
- 先更数据库,再删缓存
- 其他
起因
在高并发的业务场景下,数据库大多数情况都是用户并发访问最薄弱的环节。所以,就需要使用redis做一个缓冲操作,让请求先访问到redis,而不是直接访问MySQL等数据库。读取缓存步骤一般没有什么问题,但是一旦涉及到数据更新:数据库和缓存更新,就容易出现缓存(Redis)和数据库(MySQL)间的数据一致性问题。
更新缓存的策略问题:当缓存中的内容变化时,是选择修改缓存(update),还是直接淘汰缓存(delete)?
一般选择淘汰缓存
淘汰缓存
优点:操作简单
缺点:下次查询需要重新查数据库
更新缓存
优点:命中率高
缺点:更新缓存消耗较大,更新操作如果牵连到其他数据,会很耗时,而且这个缓存以后也不一定会用到
若有操作失败情况产生
方案一、先淘汰缓存,再更新数据库
如果第一步淘汰缓存成功,第二步更新数据库失败,此时再次查询缓存,最多会有一次cache miss
方案二、先更新数据库,再淘汰缓存
如果第一步更新数据库成功,第二部淘汰缓存失败,则会出现数据库中是新数据,缓存中是旧数据,即数据不一致
解决办法:为确保缓存删除成功,需要用到“重试机制”,即当删除缓存失效后,返回一个错误,由业务代码再次重试,直到缓存被删除。
但对于方案一,如果更新数据库失败其实也是一个问题,为了确保数据库中的数据被正常更新,也需要“重试机制”,即当数据库中的数据更新失败后,也需要人工或业务代码再次重试,直到更新成功。
执行顺序的问题:先更新缓存还是先更新数据库?
先删缓存,在更数据库
1、在正常情况下,A、B两个线程先后对同一个数据进行读写操作:
A线程进行写操作,先淘汰缓存,再更新数据库
B线程进行读操作,发现缓存中没有想要的数据,从数据库中读取更新后的新数据
此时没有问题
2、在并发量较大的情况下,采用同步更新缓存的策略:
A线程进行写操作,先成功淘汰缓存,但由于网络或其它原因,还未更新数据库或正在更新
B线程进行读操作,发现缓存中没有想要的数据,从数据库中读取数据,但此时A线程还未完成更新操作,所以读取到的是旧数据,并且B线程将旧数据放入缓存。注意此时是没有问题的,因为数据库中的数据还未完成更新,所以数据库与缓存此时存储的都是旧值,数据没有不一致
在B线程将旧数据读入缓存后,A线程终于将数据更新完成,此时是有问题的,数据库中是更新后的新数据,缓存中是更新前的旧数据,数据不一致。如果在缓存中没有对该值设置过期时间,旧数据将一直保存在缓存中,数据将一直不一致,直到之后再次对该值进行修改时才会在缓存中淘汰该值
此时可能会导致cache与数据库的数据一直或很长时间不一致
3、在并发量较大的情况下,采用异步更新缓存的策略:
A线程进行写操作,先成功淘汰缓存,但由于网络或其它原因,还未更新数据库或正在更新
B线程进行读操作,发现缓存中没有想要的数据,从数据库中读取数据,但B线程只是从数据库中读取想要的数据,并不将这个数据放入缓存中,所以并不会导致缓存与数据库的不一致
A线程更新数据库后,通过订阅binlog来异步更新缓存
此时数据库与缓存的内容将一直都是一致的
进一步分析:
如果采取同步更新缓存的策略,即如果缓存中没有数据,就读取数据库并将数据直接放入缓存,可能会导致数据长时间的不一致
在这种情况下,可以用一些方法来进行优化:
延时双删+设置缓存的超时时间
不一致的原因是,在淘汰缓存之后,旧数据再次被读入缓存,且之后没有淘汰策略,所以解决思路就是,在旧数据再次读入缓存后,再次淘汰缓存,即淘汰缓存两次(延迟双删)
引入延时双删后,执行步骤变为下面这种情形:
A线程进行写操作,先成功淘汰缓存,但由于网络或其它原因,还未更新数据库或正在更新
B线程进行读操作,从数据库中读入旧数据,共耗时N秒
在B线程将旧数据读入缓存后,A线程将数据更新完成,此时数据不一致
A线程将数据库更新完成后,休眠M秒(M比N稍大即可),然后再次淘汰缓存,此时缓存中即使有旧数据也会被淘汰,此时可以保证数据的一致性
其它线程进行读操作时,缓存中无数据,从数据库中读取的是更新后的新数据
利用延迟双删,可以很好的解决数据不一致的问题,其中A线程休眠的M秒,需要根据业务上读取的时间来衡量,只要比正常读取消耗的实际稍大就可以。但是个人感觉实际业务中需要根据场景来设置休眠的时间,这个不好确定。
为什么要延时:
为了在 修改数据库->清空缓存前,其他事务的更改缓存操作已经执行完,所以要M>N
引入延时双删后,存在两个新问题:
1、A线程需要在更新数据库后,还要休眠M秒再次淘汰缓存,等所有操作都执行完,这一个更新操作才真正完成,降低了更新操作的吞吐量
解决办法:用“异步淘汰”的策略,将休眠M秒以及二次淘汰放在另一个线程中,A线程在更新完数据库后,可以直接返回成功而不用等待。
2、如果第二次缓存淘汰失败,则不一致依旧会存在
解决办法:用“重试机制”,即当二次淘汰失败后,报错并继续重试,直到执行成功个人
先更数据库,再删缓存
正常情况
A请求进行写操作,先更新数据库,再淘汰缓存
B请求进行读操作,由于A请求已将缓存淘汰,B请求没有在redis中发现所需数据,因此从数据库中读取数据,并更新缓存到redis中
异常情况1
A请求进行写操作,先更新数据库
B请求进行读操作,由于A请求尚未淘汰缓存,B请求在redis中发现所需数据,因此直接返回老数据,产生了数据不一致的问题
A请求淘汰缓存。
C请求进行读操作,发现redis中没有数据,因此从数据库中读取新数据,并更新至缓存。数据不一致的问题解决。
该场景下,数据最终一致,只是在高并发下产生了一小段时间的数据不一致。
异常情况2
A请求进行读操作,此时redis缓存中没有数据,因此直接从数据库中读取数据
B请求进行写操作,更新数据库,并将redis中缓存进行了淘汰(虽然此时redis中并没有任何的缓存)
A请求将从数据库中读到的老数据,更新到redis。此时产生数据不一致问题。
该种异常情况发生概率极低,一般读操作比写操作要快。如有担心,可以采用上述的延时删除策略
先更新数据库,后更新缓存,有可能导致极短时间内的数据不一致,但是数据最终是一致的
其他
重试机制可以采利用“消息队列MQ”来实现
通过订阅binlog来异步更新缓存,可以通过canal中间件来实现
那如果消息队列和canal挂了怎么办,这是可以用守护线程,守护线程拉起进程,守护进程挂了怎么办,其实这是个无底洞,无法保证绝对的一致。
参考:https://developer.aliyun.com/article/712285https://blog.csdn.net/wwd0501/article/details/106902856/