作者:京东物流 陈昌浩

1 导读

Redis 是以后最风行的 NoSQL数据库。Redis次要用来做缓存应用,在进步数据查问效率、爱护数据库等方面起到了关键性的作用,很大水平上进步零碎的性能。当然在应用过程中,也会呈现一些异样情景,导致Redis失去缓存作用。

2 异样类型

异样次要有 缓存雪崩 缓存穿透 缓存击穿。

2.1 缓存雪崩

2.1.1 景象

缓存雪崩是指大量申请在缓存中没有查到数据,间接拜访数据库,导致数据库压力增大,最终导致数据库解体,从而波及整个零碎不可用,如同雪崩一样。

2.1.2 异样起因

  • 缓存服务不可用。
  • 缓存服务可用,然而大量KEY同时生效。

2.1.3 解决方案

1.缓存服务不可用
redis的部署形式次要有单机、主从、哨兵和 cluster模式。

  • 单机
    只有一台机器,所有数据都存在这台机器上,当机器出现异常时,redis将生效,可能会导致redis缓存雪崩。
  • 主从
    主从其实就是一台机器做主,一个或多个机器做从,从节点从主节点复制数据,能够实现读写拆散,主节点做写,从节点做读。
    长处:当某个从节点异样时,不影响应用。
    毛病:当主节点异样时,服务将不可用。
  • 哨兵
    哨兵模式也是一种主从,只不过减少了哨兵的性能,用于监控主节点的状态,当主节点宕机之后会进行投票在从节点中从新选出主节点。
    长处:高可用,当主节点异样时,主动在从节点当中抉择一个主节点。
    毛病:只有一个主节点,当数据比拟多时,主节点压力会很大。
  • cluster模式
    集群采纳了多主多从,依照肯定的规定进行分片,将数据别离存储,肯定水平上解决了哨兵模式下单机存储无限的问题。
    长处:高可用,配置了多主多从,能够使数据分区,去中心化,减小了单台机子的累赘.
    毛病:机器资源应用比拟多,配置简单。
  • 小结
    从高可用得角度思考,应用哨兵模式和cluster模式能够避免因为redis不可用导致的缓存雪崩问题。

2.大量KEY同时生效
能够通过设置永不生效、设置不同生效工夫、应用二级缓存和定时更新缓存生效工夫

  • 设置永不生效
    如果所有的key都设置不生效,不就不会呈现因为KEY生效导致的缓存雪崩问题了。redis设置key永远无效的命令如下:
    PERSIST key
    毛病:会导致redis的空间资源需要变大。
  • 设置随机失效工夫
    如果key的生效工夫不雷同,就不会在同一时刻生效,这样就不会呈现大量拜访数据库的状况。
    redis设置key无效工夫命令如下:
    Expire key
    示例代码如下,通过RedisClient实现
/*** 随机设置小于30分钟的生效工夫* @param redisKey* @param value*/private void setRandomTimeForReidsKey(String redisKey,String value){//随机函数Random rand = new Random();//随机获取30分钟内(30*60)的随机数int times = rand.nextInt(1800);//设置缓存工夫(缓存的key,缓存的值,生效工夫:单位秒)redisClient.setNxEx(redisKey,value,times);}
  • 应用二级缓存
    二级缓存是应用两组缓存,1级缓存和2级缓存,同一个Key在两组缓存里都保留,然而他们的生效工夫不同,这样1级缓存没有查到数据时,能够在二级缓存里查问,不会间接拜访数据库。
    示例代码如下:
public static void main(String[] args) {CacheTest test = new CacheTest();//从1级缓存中获取数据String value = test.queryByOneCacheKey("key");//如果1级缓存中没有数据,再二级缓存中查找if(StringUtils.isBlank(value)){value = test.queryBySecondCacheKey("key");//如果二级缓存中没有,从数据库中查找if(StringUtils.isBlank(value)){value =test.getFromDb();//如果数据库中也没有,就返回空if(StringUtils.isBlank(value)){System.out.println("数据不存在!");}else{//二级缓存中保留数据test.secondCacheSave("key",value);//一级缓存中保留数据test.oneCacheSave("key",value);System.out.println("数据库中返回数据!");}}else{//一级缓存中保留数据test.oneCacheSave("key",value);System.out.println("二级缓存中返回数据!");}}else {System.out.println("一级缓存中返回数据!");}}
  • 异步更新缓存工夫
    每次拜访缓存时,启动一个线程或者建设一个异步工作来,更新缓存工夫。
    示例代码如下:
public class CacheRunnable implements Runnable {private ClusterRedisClientAdapter redisClient;/*** 要更新的key*/public String key;public CacheRunnable(String key){this.key =key;}@Overridepublic void run() {//更细缓存工夫redisClient.expire(this.getKey(),1800);}public String getKey() {return key;}public void setKey(String key) {this.key = key;}}public static void main(String[] args) {CacheTest test = new CacheTest();//从缓存中获取数据String value = test.getFromCache("key");if(StringUtils.isBlank(value)){//从数据库中获取数据value = test.getFromDb("key");//将数据放在缓存中test.oneCacheSave("key",value);//返回数据System.out.println("返回数据");}else{//异步工作更新缓存CacheRunnable runnable = new CacheRunnable("key");runnable.run();//返回数据System.out.println("返回数据");}}

3.小结
下面从服务不可用和key大面积生效两个方面,列举了几种解决方案,下面的代码只是提供一些思路,具体实施还要思考到现实情况。当然也有其余的解决方案,我这里举例是比拟罕用的。毕竟现实情况,变幻无穷,没有最好的计划,只有最实用的计划。

2.2 缓存穿透

2.2.1 景象

缓存穿透是指当用户在查问一条数据的时候,而此时数据库和缓存却没有对于这条数据的任何记录,而这条数据在缓存中没找到就会向数据库申请获取数据。用户拿不到数据时,就会始终发申请,查询数据库,这样会对数据库的拜访造成很大的压力。

2.2.2 异样起因

  • 非法调用

2.2.3 解决方案

1.非法调用
能够通过缓存空值或过滤器来解决非法调用引起的缓存穿透问题。

  • 缓存空值
    当缓存和数据库中都没有值时,能够在缓存中寄存一个空值,这样就能够缩小反复查问空值引起的零碎压力增大,从而优化了缓存穿透问题。
    示例代码如下:
private String queryMessager(String key){//从缓存中获取数据String message = getFromCache(key);//如果缓存中没有 从数据库中查找if(StringUtils.isBlank(message)){message = getFromDb(key);//如果数据库中也没有数据 就设置短时间的缓存if(StringUtils.isBlank(message)){//设置缓存工夫(缓存的key,缓存的值,生效工夫:单位秒)redisClient.setNxEx(key,null,60);}else{redisClient.setNxEx(key,message,1800);}}return message;}

毛病:大量的空缓存导致资源的节约,也有可能导致缓存和数据库中的数据不统一。

  • 布隆过滤器
    布隆过滤器由布隆在 1970 年提出。它实际上是一个很长的二进制向量和一系列随机映射函数。布隆过滤器能够用于检索一个元素是否在一个汇合中。是以空间换工夫的算法。

布隆过滤器的实现原理是一个超大的位数组和几个哈希函数。
假如哈希函数的个数为 3。首先将位数组进行初始化,初始化状态的维数组的每个位都设置位 0。如果一次数据申请的后果为空,就将key顺次通过 3 个哈希函数进行映射,每次映射都会产生一个哈希值,这个值对应位数组下面的一个点,而后将位数组对应的地位标记为 1。当数据申请再次发过来时,用同样的办法将 key 通过哈希映射到位数组上的 3 个点。如果 3 个点中任意一个点不为 1,则能够判断key不为空。反之,如果 3 个点都为 1,则该KEY肯定为空。

毛病:
可能呈现误判,例如 A 通过哈希函数 存到 1、3和5地位。B通过哈希函数存到 3、5和7地位。C通过哈希函数失去地位 3、5和7地位。因为3、5和7都有值,导致判断A也在数组中。这种状况随着数据的增多,几率也变大。
布隆过滤器没法删除数据。
  • 布隆过滤器增强版
    增强版是将布隆过滤器的bitmap更换成数组,当数组某地位被映射一次时就+1,当删除时就-1,这样就防止了一般布隆过滤器删除数据后须要从新计算其余数据包Hash的问题,然而仍旧没法防止误判。
  • 布谷鸟过滤器
    然而如果这两个地位都满了,它就不得不「鸠占鹊巢」,随机踢走一个,而后本人霸占了这个地位。不同于布谷鸟的是,布谷鸟哈希算法会帮这些受害者(被挤走的蛋)寻找其它的窝。因为每一个元素都能够放在两个地位,只有任意一个有空地位,就能够塞进去。所以这个伤心的被挤走的蛋会看看本人的另一个地位有没有空,如果空了,本人挪过来也就大快人心了。然而如果这个地位也被他人占了呢?好,那么它会再来一次「鸠占鹊巢」,将受害者的角色转嫁给他人。而后这个新的受害者还会反复这个过程直到所有的蛋都找到了本人的巢为止。

    毛病:
    如果数组太拥挤了,间断踢来踢去几百次还没有停下来,这时候会重大影响插入效率。这时候布谷鸟哈希会设置一个阈值,当间断占巢行为超出了某个阈值,就认为这个数组曾经简直满了。这时候就须要对它进行扩容,从新搁置所有元素。

2.小结
以上办法尽管都有毛病,然而能够无效的避免因为大量空数据查问导致的缓存穿透问题,除了零碎上的优化,还要增强对系统的监控,发下异样调用时,及时退出黑名单。升高异样调用对系统的影响。

2.3 缓存击穿

2.3.1 景象

key中对应数据存在,当key中对应的数据在缓存中过期,而此时又有大量申请拜访该数据,缓存中过期了,申请会间接拜访数据库并回设到缓存中,高并发拜访数据库会导致数据库解体。redis的高QPS个性,能够很好的解决查数据库很慢的问题。然而如果咱们零碎的并发很高,在某个工夫节点,忽然缓存生效,这时候有大量的申请打过去,那么因为redis没有缓存数据,这时候咱们的申请会全副去查一遍数据库,这时候咱们的数据库服务会面临十分大的危险,要么连贯被占满,要么其余业务不可用,这种状况就是redis的缓存击穿。

2.3.2 异样起因

热点KEY生效的同时,大量雷同KEY申请同时拜访。

2.3.3 解决方案

1.热点key生效

  • 设置永不生效
    如果所有的key都设置不生效,不就不会呈现因为KEY生效导致的缓存雪崩问题了。redis设置key永远无效的命令如下:
    PERSIST key
    毛病:会导致redis的空间资源需要变大。
  • 设置随机失效工夫
    如果key的生效工夫不雷同,就不会在同一时刻生效,这样就不会呈现大量拜访数据库的状况。
    redis设置key无效工夫命令如下:
    Expire key
    示例代码如下,通过RedisClient实现
/*** 随机设置小于30分钟的生效工夫* @param redisKey* @param value*/private void setRandomTimeForReidsKey(String redisKey,String value){//随机函数Random rand = new Random();//随机获取30分钟内(30*60)的随机数int times = rand.nextInt(1800);//设置缓存工夫(缓存的key,缓存的值,生效工夫:单位秒)redisClient.setNxEx(redisKey,value,times);}
  • 应用二级缓存
    二级缓存是应用两组缓存,1级缓存和2级缓存,同一个Key在两组缓存里都保留,然而他们的生效工夫不同,这样1级缓存没有查到数据时,能够在二级缓存里查问,不会间接拜访数据库。
    示例代码如下:
public static void main(String[] args) {CacheTest test = new CacheTest();//从1级缓存中获取数据String value = test.queryByOneCacheKey("key");//如果1级缓存中没有数据,再二级缓存中查找if(StringUtils.isBlank(value)){value = test.queryBySecondCacheKey("key");//如果二级缓存中没有,从数据库中查找if(StringUtils.isBlank(value)){value =test.getFromDb();//如果数据库中也没有,就返回空if(StringUtils.isBlank(value)){System.out.println("数据不存在!");}else{//二级缓存中保留数据test.secondCacheSave("key",value);//一级缓存中保留数据test.oneCacheSave("key",value);System.out.println("数据库中返回数据!");}}else{//一级缓存中保留数据test.oneCacheSave("key",value);System.out.println("二级缓存中返回数据!");}}else {System.out.println("一级缓存中返回数据!");}}
  • 异步更新缓存工夫
    每次拜访缓存时,启动一个线程或者建设一个异步工作来,更新缓存工夫。
    示例代码如下:
public class CacheRunnable implements Runnable {private ClusterRedisClientAdapter redisClient;/*** 要更新的key*/public String key;public CacheRunnable(String key){this.key =key;}@Overridepublic void run() {//更细缓存工夫redisClient.expire(this.getKey(),1800);}public String getKey() {return key;}public void setKey(String key) {this.key = key;}}public static void main(String[] args) {CacheTest test = new CacheTest();//从缓存中获取数据String value = test.getFromCache("key");if(StringUtils.isBlank(value)){//从数据库中获取数据value = test.getFromDb("key");//将数据放在缓存中test.oneCacheSave("key",value);//返回数据System.out.println("返回数据");}else{//异步工作更新缓存CacheRunnable runnable = new CacheRunnable("key");runnable.run();//返回数据System.out.println("返回数据");}}
  • 分布式锁
    应用分布式锁,同一时间只有1个申请能够拜访到数据库,其余申请期待一段时间后,反复调用。
    示例代码如下:
/*** 依据key获取数据* @param key* @return* @throws InterruptedException*/public String queryForMessage(String key) throws InterruptedException {//初始化返回后果String result = StringUtils.EMPTY;//从缓存中获取数据result = queryByOneCacheKey(key);//如果缓存中有数据,间接返回if(StringUtils.isNotBlank(result)){return result;}else{//获取分布式锁if(lockByBusiness(key)){//从数据库中获取数据result = getFromDb(key);//如果数据库中有数据,就加在缓存中if(StringUtils.isNotBlank(result)){oneCacheSave(key,result);}}else {//如果没有获取到分布式锁,睡眠一下,再接着查问数据Thread.sleep(500);return queryForMessage(key);}}return result;}

2.小结
除了以上解决办法,还能够事后设置热门数据,通过一些监控办法,及时收集热点数据,将数据事后保留在缓存中。

3 总结

Redis缓存在互联网中至关重要,能够很大的晋升零碎效率。 本文介绍的缓存异样以及解决思路有可能不够全面,但也提供相应的解决思路和代码大体实现,心愿能够为大家提供一些遇到缓存问题时的解决思路。如果有有余的中央,也请帮忙指出,大家共同进步。