关于redis:Redis缓存的主要异常及解决方案

36次阅读

共计 7374 个字符,预计需要花费 19 分钟才能阅读完成。

作者:京东物流 陈昌浩

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;}

@Override
public 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;}

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

正文完
 0