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

文章出自:http://www.javashuo.com/article/p-gnivlhoa-kv.html

1、不一致缘由分析

将不一致分为三种状况:mysql

  1. 数据库有数据,缓存没有数据;
  2. 数据库有数据,缓存也有数据,数据不相等;
  3. 数据库没有数据,缓存有数据。

在讨论这三种状况以前,先说明一下我使用缓存的策略,也是大多数人使用的策略,叫作 Cache Aside Pattern。简而言之,就是redis

  1. 首先尝试从缓存读取,读到数据则直接返回;若是读不到,就读数据库,并将数据会写到缓存,并返回。
  2. 须要更新数据时,先更新数据库,而后把缓存里对应的数据失效掉(删掉)。

读的逻辑你们都很容易理解,谈谈更新。若是不采起我提到的这种更新方法,你还能想到什么更新方法呢?大概会是:先删除缓存,而后再更新数据库。这么作引起的问题是,若是A,B两个线程同时要更新数据,而且A,B已经都作完了删除缓存这一步,接下来,A先更新了数据库,C线程读取数据,因为缓存没有,则查数据库,并把A更新的数据,写入了缓存,最后B更新数据库。那么缓存和数据库的值就不一致了。另外有人会问,若是采用你提到的方法,为何最后是把缓存的数据删掉,而不是把更新的数据写到缓存里。这么作引起的问题是,若是A,B两个线程同时作数据更新,A先更新了数据库,B后更新数据库,则此时数据库里存的是B的数据。而更新缓存的时候,是B先更新了缓存,而A后更新了缓存,则缓存里是A的数据。这样缓存和数据库的数据也不一致。按照我提到的这种更新缓存的策略,理论上也是有不一致的风险的,以前在其余的博客文章有看到过,只不过几率很小,咱们暂时能够不考虑,后面咱们有其余手段来补救。讨论完使用缓存的策略,咱们再来看这三种不一致的状况。sql

  1. 对于第一种,在读数据的时候,会自动把数据库的数据写到缓存,所以不一致自动消除.
  2. 对于第二种,数据最终变成了不相等,但他们以前在某一个时间点必定是相等的(无论你使用懒加载仍是预加载的方式,在缓存加载的那一刻,它必定和数据库一致)。这种不一致,必定是因为你更新数据所引起的。前面咱们讲了更新数据的策略,先更新数据库,而后删除缓存。所以,不一致的缘由,必定是数据库更新了,可是删除缓存失败了。
  3.  对于第三种,状况和第二种相似,你把数据库的数据删了,可是删除缓存的时候失败了。

2、常看法决方案

所以,最终的结论是,须要解决的不一致,产生的缘由是更新数据库成功,可是删除缓存失败。
解决方案大概有如下几种:数据库

  1.  对删除缓存进行重试,数据的一致性要求越高,我越是重试得快。
  2. 按期全量更新,简单地说,就是我按期把缓存所有清掉,而后再全量加载。
  3. 给全部的缓存一个失效期。

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

并发不高的状况:网络

  • 读: 读redis->没有,读mysql->把mysql数据写回redis,有的话直接从redis中取;
  • 写: 写mysql->成功,再写redis;

并发高的状况:并发

  • 读: 读redis->没有,读mysql->把mysql数据写回redis,有的话直接从redis中取;
  • 写:异步话,先写入redis的缓存,就直接返回;按期或特定动做将数据保存到mysql,能够作到屡次更新,一次保存;

3、分布式缓存更新策略

  1. 若是更新缓存:先更新DB,再更新缓存;先更新缓存,再更新DB。
  2. 若是删除缓存:先删除缓存,再更新DB;先更新DB,再删除缓存。

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

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

先更新缓存,再更新DB:在更新缓存须要的时候,数据变动时若是:先更新缓存,再更新DB会有什么问题?ide

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

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

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

一样是脏数据的问题。

4、常见问题及解决方法

1:缓存一致性问题:缓存系统与底层数据的一致性。这点在底层系统是“可读可写”时,写得尤其重要

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的处理方式。

5、总结

在不设置过时时间的状况下,不论是先更新DB仍是更新缓存,都很是容易出现缓存脏数据的问题,且不容易进行处理。再加上数据在不须要的时候被缓存就是浪费时间,因此不该该在数据发生变动的时候更新缓存。既然不更新缓存,那么就删除缓存吧。查询redis缓存时,通常查询若是以非id方式查询,建议先由条件查询到id,再由id查询pojo;比较简单的redis缓存,推荐使用canal。