Redis学习总结(7)——怎么保持缓存与数据库一致性?

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/u012562943/article/details/81482914

一、不一致原因分析

将不一致分为三种情况:
1. 数据库有数据,缓存没有数据;
2. 数据库有数据,缓存也有数据,数据不相等;
3. 数据库没有数据,缓存有数据。

在讨论这三种情况之前,先说明一下我使用缓存的策略,也是大多数人使用的策略,叫做 Cache Aside Pattern。简而言之,就是
1. 首先尝试从缓存读取,读到数据则直接返回;如果读不到,就读数据库,并将数据会写到缓存,并返回。
2. 需要更新数据时,先更新数据库,然后把缓存里对应的数据失效掉(删掉)。

读的逻辑大家都很容易理解,谈谈更新。如果不采取我提到的这种更新方法,你还能想到什么更新方法呢?大概会是:先删除缓存,然后再更新数据库。这么做引发的问题是,如果A,B两个线程同时要更新数据,并且A,B已经都做完了删除缓存这一步,接下来,A先更新了数据库,C线程读取数据,由于缓存没有,则查数据库,并把A更新的数据,写入了缓存,最后B更新数据库。那么缓存和数据库的值就不一致了。另外有人会问,如果采用你提到的方法,为什么最后是把缓存的数据删掉,而不是把更新的数据写到缓存里。这么做引发的问题是,如果A,B两个线程同时做数据更新,A先更新了数据库,B后更新数据库,则此时数据库里存的是B的数据。而更新缓存的时候,是B先更新了缓存,而A后更新了缓存,则缓存里是A的数据。这样缓存和数据库的数据也不一致。按照我提到的这种更新缓存的策略,理论上也是有不一致的风险的,之前在其他的博客文章有看到过,只不过概率很小,我们暂时可以不考虑,后面我们有其他手段来补救。讨论完使用缓存的策略,我们再来看这三种不一致的情况。
1. 对于第一种,在读数据的时候,会自动把数据库的数据写到缓存,因此不一致自动消除.
2. 对于第二种,数据最终变成了不相等,但他们之前在某一个时间点一定是相等的(不管你使用懒加载还是预加载的方式,在缓存加载的那一刻,它一定和数据库一致)。这种不一致,一定是由于你更新数据所引发的。前面我们讲了更新数据的策略,先更新数据库,然后删除缓存。因此,不一致的原因,一定是数据库更新了,但是删除缓存失败了。

3. 对于第三种,情况和第二种类似,你把数据库的数据删了,但是删除缓存的时候失败了。

二、常见解决方案

因此,最终的结论是,需要解决的不一致,产生的原因是更新数据库成功,但是删除缓存失败。
解决方案大概有以下几种:
1. 对删除缓存进行重试,数据的一致性要求越高,我越是重试得快。
2. 定期全量更新,简单地说,就是我定期把缓存全部清掉,然后再全量加载。
3. 给所有的缓存一个失效期。

第三种方案可以说是一个大杀器,任何不一致,都可以靠失效期解决,失效期越短,数据一致性越高。但是失效期越短,查数据库就会越频繁。因此失效期应该根据业务来定。

并发不高的情况:
读: 读redis->没有,读mysql->把mysql数据写回redis,有的话直接从redis中取;
写: 写mysql->成功,再写redis;

并发高的情况:
读: 读redis->没有,读mysql->把mysql数据写回redis,有的话直接从redis中取;
写:异步话,先写入redis的缓存,就直接返回;定期或特定动作将数据保存到mysql,可以做到多次更新,一次保存;

三、分布式缓存更新策略

如果更新缓存:先更新DB,再更新缓存;先更新缓存,再更新DB。

如果删除缓存:先删除缓存,再更新DB;先更新DB,再删除缓存。

为了保证数据的一致性,不论何种方式处理缓存,都应该给缓存设置过期时间,这个是缓存必须的要素,否则数据的最终一致性很难得到保证。下述讨论的讨论方案中,是在没有设置缓存过期时间 的情况下的极端讨论,仅仅为了理清思路实际开发过程中,都应该给缓存加上过期时间

更新缓存先下结论:对于数据变化,不应该同步更新缓存因为:只有被查询的数据的数据建立缓存,才有意义。一个数据只会被更新,长期或者永远不会被查询,建立缓存就是浪费资源。总结:建立缓存的操作应该是在数据被读取的时候。

先更新缓存,再更新DB在更新缓存需要的时候,数据变更时如果:先更新缓存,再更新DB会有什么问题?

缓存的来源是DB,如果先更新缓存,在还未更新DB的这段时间内,如果有查询操作读取了这个缓存,读取的数据都是脏数据。

先更新DB,再更新缓存如果先更新DB,再更新缓存又会遇到什么问题了?在如下场景中,问题就会出现:

  1. 第一步:2个线程A和B同时更新DB的一条数据,A线程先更新DB,B后更新DB。(此时B的数据为最新的)。
  2. 第二步:接下来A和B需要更新缓存,因为网络原因,B发送的缓存更新指令先于A到达。(完全有可能,因为A和B是异步的)
  3. 第三步:缓存中间件中,缓存先被B更新(最新的数据),才被A更新(老的数据)。因为B的数据才最最新的,但是缓存最后被A更新,此时的缓存的数据是脏数据。

同样是脏数据的问题。

四、常见问题及解决方法

缓存一致性问题缓存系统与底层数据的一致性。这点在底层系统是“可读可写”时,写得尤为重要 2:有继承关系的缓存之间的一致性。为了尽量提高缓存命中率,缓存也是分层:全局缓存,二级缓存。他们是存在继承关系的。全局缓存可以有二级缓存来组成。 3:多个缓存副本之间的一致性。为了保证系统的高可用性,缓存系统背后往往会接两套存储系统(如memcache,redis等)

缓存数据的淘汰策略 (1) 定时去清理过期的缓存。 (2)当有用户请求过来时,再判断这个请求所用到的缓存是否过期,过期的话就去底层系统得到新数据并更新缓存。  两者各有优劣,第一种的缺点是维护大量缓存的key是比较麻烦的,第二种的缺点就是每次用户请求过来都要判断缓存失效,逻辑相对比较复杂,具体用哪种方案,大家可以根据自己的应用场景来权衡。   1. 预估失效时间 2. 版本号(必须单调递增,时间戳是最好的选择)3. 提供手动清理缓存的接口。

穿透频繁查询一个不存在的数据,由于缓存不命中,每次都要查询持久层。从而失去缓存的意义。
解决 持久层查询不到就缓存空结果,查询时先判断缓存中是否exists(key) ,如果有直接返回空,没有则查询后返回,注意insert时需清除查询的key,否则即便DB中有值也查询不到(当然也可以设置空缓存的过期时间)
雪崩缓存大量失效的时候,引发大量查询数据库。
解决用锁/分布式锁或者队列串行访问缓存失效时间均匀分布热点key(某个key访问非常频繁,当key失效的时候有大量线程来构建缓存,导致负载增加,系统崩溃。)
解决使用锁,单机用synchronized,lock等,分布式用分布式锁;缓存过期时间不设置,而是设置在key对应的value里。如果检测到存的时间超过过期时间则异步更新缓存;在value设置一个比过期时间t0小的过期时间值t1,当t1过期的时候,延长t1并做更新缓存操作;设置标签缓存,标签缓存设置过期时间,标签缓存过期后,需异步地更新实际缓存 具体参照userServiceImpl4的处理方式。

五、总结

在不设置过期时间的情况下,不管是先更新DB还是更新缓存,都非常容易出现缓存脏数据的问题,且不容易进行处理。再加上数据在不需要的时候被缓存就是浪费时间,所以不应该在数据发生变更的时候更新缓存。既然不更新缓存,那么就删除缓存吧。查询redis缓存时,一般查询如果以非id方式查询,建议先由条件查询到id,再由id查询pojo;比较简单的redis缓存,推荐使用canal。

没有更多推荐了,返回首页