缓存穿透:

发生原因:无意/恶意 大量访问数据库和缓存中都没有的数据key,缓存没有特殊处理时,数据库中没有的也不会去加入到缓存,此时缓存相当于失效,DB压力增大,严重时可能导致DB宕机;
解决方式:

  • 1.后端不能信任前端传递的参数,保持不信任的心,去验证参数,给不存在的key,定义默认值/空值(例如 稍后重试,或者403,404等等...通用或者自定义状态码或者看具体的场景)缓存有效时间可以设置短点,太长可能导致正常情况出问题;
    • 1-1.正常用户一般不会在几秒内产生特别大量的请求,因此还可以在反向代理服务器(例如Nginx,Apache TS,HAProxy,Varnish 等等...)加以控制,使用nginx限制某些异常ip,或者基于nginx的Openresty可伸缩的Web平台中可以使用Lua脚本筛查ip;
  • 2.布隆过滤器(Bloom Filter):可以检查到某key一定不存在或可能存在,我们只需要知道一定不存在就可以满足需求;

缓存击穿:

发生原因:指一个高热点Key,在不停的扛着大并发,大并发集中对这一个点进行访问,当这个Key在失效的瞬间,持续的大并发就穿破缓存,直接请求数据库,就像在一个完好无损的桶上凿开了一个洞;
解决方式:

  • 1.设置热点数据永远不过期;
  • 2.锁更新
    synchronized关键字:
public synchronized String getCacheData() {
  // Read redis
  String cacheData = getDataFromRedis();
  if (StringUtils.isEmpty(cacheData )) {
    // Read database
    cacheData = getDataFromDB();
    // Write redis
    setDataToCache(cacheData);
  }
  return cacheData;
}

synchronized 这个锁太宽泛,会造成大量的请求阻塞,性能极低,进一步优化缩小锁的范围

private static final byte[] lock = new byte[0];
public String getCacheData() {
  String cacheData = getDataFromRedis();
  if (cacheData.isEmpty()) {
    synchronized (lock) {
      cacheData = getDataFromDB();
      setDataToCache(cacheData);
    }
  }
  return cacheData;
}

还可以使用互斥锁优化

public String getCacheData() {
    String result = getDataFromRedis();
    if (result.isEmpty()) {
        if (reenLock.tryLock()) {
            try {
                //Read database
                result = getDataFromDB();
                //Write redis
                setDataToCache(result);
            }catch(Exception e){
                e.printStackTrace();
            }finally {
                reenLock.unlock (); // release lock
            }
            return result;
        }
    }
    result = getDataFromRedis();
    if (result.isEmpty()) {
        try {
            Thread.sleep(100);
            return getCacheData();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    return result;
}
  • 3.异步更新:
    把缓存设置为永久不过期,异步定时更新缓存。比如后台有个值守线程专门定时更新缓存,但一般还要定时频繁地去检测缓存,一旦发现被踢掉(比如被缓存的失效策略 FIFO、LFU、LRU 等)需要立刻更新缓存,但这个“定时”的度是比较难掌握的,实现简单但用户体验一般;
    其实异步更新机制比较适合缓存预热,缓存预热是指系统上线时,将相关的缓存数据直接加载到缓存系统,避免在用户请求时才缓存数据,提高了性能;

缓存雪崩:

发生原因:因为大量的缓存同时失效,高并发都访问了DB,可能因此打崩了DB;如果没用什么特别的方案来处理这个故障,即使重启数据库,也可能会被持续的高并发给打死,几乎是灾难性的
解决方式:

  • 1.集群:
    高可用方案的本质就是冗余,集群是其实现方式之一,使用集群可以避免服务单点故障,但集群也带来了复杂度,好在很多成熟的中间件都有稳妥的集群方案,比如 Redis 集群;
  • 2.差异性过期时间:
    根据业务特点随机生成过期时间,让过期时间分散;
    也有是通过定时刷新过期时间,类似于 refresh token 机制;
  • 3.服务降级或熔断
    服务熔断:当缓存服务器宕机或超时响应时,为了防止整个系统出现雪崩,暂时停止业务服务访问缓存系统;
    服务降级:当出现大量缓存失效,而且处在高并发高负荷的情况下,在业务系统内部暂时舍弃对一些非核心的接口和数据的请求,而直接返回一个提前准备好的 fallback(退路)错误处理信息;
    (Hystrix限流+降级)限流组件:
    牺牲部分用户的体验换来服务器的安全;
    确保了每秒只有多少个请求能通过。 只要数据库不死,就是说,对用户来说,3/5 的请求都是可以被处理的。 只要有 3/5 的请求可以被处理,就意味着你的系统没死,对用户来说,可能就是点击几次刷不出来页面,但是多点几次,就可以刷出来一次;


未完待续 ......