前言
工作中,我们经常使用缓存来提高查询速度,减轻数据库的压力,不让过多的流量压垮数据库,所以缓存也成了我们系统重要的环节之一,而缓存最常见的问题是缓存穿透、击穿和雪崩,在高并发下,缓存的这三个问题会导致大量的请求落到数据库,导致数据库的最大连接数占满,导致数据库故障,引发我们系统一系列故障。今天我们就来分析一下缓存这三种缓存问题有什么区别?
缓存穿透
缓存穿透指的是数据库查询一个不存在的数据,而我们的程序是只有数据库有数据时,我们才将数据存入缓存,这样会导致如果有人利用这个漏洞频繁访问这个数据值去查询,大量的请求打到数据库上可能会导致数据库挂掉,这就失去了缓存的意义
解决方案
- 一种简单的解决方案是:我们可以短暂的将这种不存在的数据缓存起来,来防止这种恶意请求频繁打击数据库,将缓存设置过期时间30秒,或更短,确保改key不会过长的占用内存
- 上述方案我们可能会因为大量的这种数据而占用很大的内存,而且如果同时过期的话也会有缓存雪崩的问题,我们还可以利用布隆过滤器来进行优化,这里就不具体阐述布隆过滤器了,感兴趣的小伙伴可以百度一下布隆过滤器
缓存击穿
例子
本人在工作中就碰到缓存击穿的问题,我们电商app的首页往往会有很多的数据信息,这些数据信息从数据库取出来存入缓存中,但是客服突然返回我们的app有那么一段时间首页卡顿,用户体验不好,经过检查,有一段时间缓存失效,导致大量请求访问数据库,造成数据库卡顿。
描述
大家估计从上面的例子也能体会到了什么是缓存击穿,其实就是一个热点数据失效之后,在我们还没有重新加载缓存时,大量请求访问数据库,导致数据库奔溃
解决方案
- 将热点数据设置为永不过期
- 在缓存即将过期时,或者缓存过期时,加互斥锁访问数据库,保证只有一个线程访问到数据库,下面是分布式加锁代码
public static final String LOCK_PREFIX = "redis_lock_"; //加锁失效时间,毫秒 public static final int LOCK_EXPIRE = 3000; // ms public boolean lock(String key){ String lock = LOCK_PREFIX + key; // 利用lambda表达式 return (Boolean) redisTemplate.execute((RedisCallback) connection -> { //设置redis锁过期时间 long expireAt = System.currentTimeMillis() + LOCK_EXPIRE + 1; Boolean acquire = connection.setNX(lock.getBytes(), String.valueOf(expireAt).getBytes()); //获取锁成功 if (acquire) { return true; } else { byte[] value = connection.get(lock.getBytes()); if (Objects.nonNull(value) && value.length > 0) { long expireTime = Long.parseLong(new String(value)); // 如果锁已经过期 if (expireTime < System.currentTimeMillis()) { // 重新加锁,防止死锁,返回之前的值 byte[] oldValue = connection.getSet(lock.getBytes(), String.valueOf(System.currentTimeMillis() + LOCK_EXPIRE + 1).getBytes()); //当前时间大于之前锁的时间,则代表获取到锁 return Long.parseLong(new String(oldValue)) < System.currentTimeMillis(); } } } return false; }); } /** * 删除锁 * * @param key */ public void deleteLock(String key) { redisTemplate.delete(LOCK_PREFIX + key); }
设置缓存
//缓存的key private String key = "testBreakdown"; //设置超时事件2分钟 public static final long timeout = 120; public String textRedisBreakdown(){ Long expire = redisTemplate.getExpire(key, TimeUnit.SECONDS); //小于60秒 if(expire>0&&expire<60){ if(lock(key)){ System.out.println("预刷新缓存,获取到锁"); //获取到锁了 try { System.out.println("操作数据库存入缓存"); redisTemplate.opsForValue().set(key, "11", timeout, TimeUnit.SECONDS); }catch (Exception e){ System.out.println("异常出错"); }finally { System.out.println("最终都释放锁"); deleteLock(key); } } //去获取缓存的值 textRedisBreakdown(); }else if(expire<0){ //没有值 if(lock(key)){ System.out.println("获取到锁"); //获取到锁了 try { System.out.println("操作数据库存入缓存"); redisTemplate.opsForValue().set(key, "11", timeout, TimeUnit.SECONDS); }catch (Exception e){ System.out.println("异常出错"); }finally { System.out.println("最终都释放锁"); deleteLock(key); } } //这里也可以写else使用Thead.sleep()来等待数据库和业务处理的事件,这里各位道友仁者见仁智者见智了 textRedisBreakdown(); } return redisTemplate.opsForValue().get(key).toString(); }
测试返回的结果
缓存雪崩
缓存雪崩指的是大量缓存数据统一时刻失效,当用户访问导致大量不同的接口访问数据库,引起数据库压力增大甚至宕机,像淘宝首页的数据都是缓存在数据库,分很多模块,这些模块的数据都是从缓存获取,如果这些缓存同时失效,导致的负面影响是巨大的
解决方案
- 设置缓存过期时间的时候,在原有缓存时间的基础上加上随机值,尽量是每个缓存过期时间分布在不同的时间,避免大批量缓存过期的发生
- 对与这些数据设置永不过期,但是我们项目中很少这么做,谁也不能保证这些数据之后还会不会用,如果不用了会导致这些数据一只占用内存,导致内存的浪费
最后,如果上述表述有问题或者有歧义的地方,请大家评论滴滴我,没有问题就不会有进步,希望大家能带着我进步