缓存击穿

1.1现象

key 中对应数据存在,当 key 中对应的数据在缓存中过期,而此时又有大量请求访问该数据,缓存中过期了,请求会直接访问数据库并回设到缓存中,高并发访问数据库会导致数据库崩溃。redis 的高 QPS 特性,可以很好的解决查数据库很慢的问题。但是如果我们系统的并发很高,在某个时间节点,突然缓存失效,这时候有大量的请求打过来,那么由于 redis 没有缓存数据,这时候我们的请求会全部去查一遍数据库,这时候我们的数据库服务会面临非常大的风险,要么连接被占满,要么其他业务不可用,这种情况就是 redis 的缓存击穿。

redis 连接异常 redis异常及解决方案_缓存

1.2异常原因

热点 KEY 失效的同时,大量相同 KEY 请求同时访问。

1.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.小结 除了以上解决方法,还可以预先设置热门数据,通过一些监控方法,及时收集热点数据,将数据预先保存在缓存中。

总结

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