使用Redis可能会出现的系统异常及解决方案

在设计和使用缓存的时候,我们必须要考虑到缓存系统的可用性,避免因使用缓存出现的问题,本文涉及到的内容是在整个架构体系正常运行的情况下可能会出现的致命问题,适合使用缓存做具体业务的开发人员进行查看

缓存击穿

如果我们对于某一个热点数据进行了缓存,在缓存失效的一瞬间有大量的请求进来,这个时候所有的请求都会被系统推到数据库,瞬间的请求完全可以把数据库压垮

根据缓存击穿的发生条件,频繁失效和**大量请求共同造成了这个问题,而可以产生缓存击穿的数据必然是极大的热点数据,我们不可能把访问频繁的问题通过任何方式处理掉,只需要考虑剩下两个问题即可

  • 使用互斥量,如果缓存未能命中,不是直接去访问数据库,而是先去争夺一个锁资源,通过Redis的SetNX或者Memcached的Add操作,都可以保证有一个唯一的请求可以争夺到锁,争夺到锁的线程就可以去访问数据库并且更新缓存,而其他线程没有得到该锁,就应该继续尝试获取缓存内容,做一个自旋锁,这个自旋锁会在其他线程更新缓存失败或者完成数据缓存之后结束,虽然占用了无用的CPU但是实际上自旋的时间很短
  • 提前更新,还是使用互斥量去争夺更新缓存的锁,不过更新ConcurrentHashMap的时间不再是失效时,而是在失效之前,通过在缓存中存储一个预计时效的时间,来判断是否需要更新缓存,而且可以灵活的根据实际场景选择没有得到更新锁的线程是自旋等待还是直接取当前缓存
  • 修改过期时间,如果业务要求并不严格而且缓存更新实在是屈指可数,我们可以不设定Redis的过期,而是由一个定时任务去定时更新缓存内容,这样可以减少上述因为缓存更新造成的CPU空跑现象和频繁校验,不过一定需要确定,在更新缓存的定时任务挂掉的情况下,业务功能是可以接受缓存不更新的

    缓存穿透

    如果有不怀好意的人员针对某个接口进行攻击,不断的查询一个不可能存在的数据,这个时候我们即使使用了缓存,也不会在缓存里完成任何一次命中,这个请求就会到数据访问层进行数据库交互并查询出空结果集,或者发生异常,如果到数据库的查询过多,很可能会使数据库崩溃

根据缓存穿透的发生条件,频繁不可能存在的键是两个触发此问题的根源,我们只需要限制这两个条件就会使该问题得到缓解

  • 使用限流,任何方式的限流都可以有效的解决这个问题,无论是针对DDOS攻击而购买的高防IP,或者是主流RPC框架提供的限流都会让这个问题得到缓解,进行查询的接口如果有了上线,那么只需要考虑在访问上限的情况下数据库能否承受就好了
  • Key值过滤,我们在使用缓存的时候,key值的选择一般是有着一定的命名规范的,比如:系统名-业务名-KEY,这样能够做到避免key值冲突引发的惨案,而我们可以根据我们已知的命名规则,把不可能存在的key值做直接过滤也可以解决这个问题,常见的做法是使用布隆过滤器(一个超大的hashMap),或者直接把结果为空进行缓存,让下一个请求在缓存里命中,但是缓存时间不宜过长,因为我们并没有去考虑这个键是不会存在还是暂时不存在

    缓存雪崩

    针对某种业务需要缓存大量的数据,如果这些数据时同时放入的,由于业务规则的一致性,这些缓存将会在某个时刻同时失效,在失效的这一时刻,大量的请求会发生缓存未命中的情况,同时请求数据库而把数据库压垮

根据缓存雪崩发生的条件,大量缓存同时失效是产生问题的原因

  • 使用随机失效时间,在预估的失效时间之上做一个分散,比如之前的缓存希望存放60s,我们可以改为40S+随机数的模式,让缓存失效的时间分散在一个时间范围内,这样数据库的访问会得到一定的缓解

缓存并发

在访问高并发数据的缓存副本时,如果缓存失效,无法保证设置缓存和更新数据库是同一个线程,可能会造成数据不一致的情况

根据缓存并发发生的条件,并发更新设置缓存是产生问题的原因,我们只需要保证这两个操作的原子性即可

  • 使用互斥量,由争取到分布式更新锁的线程去更新缓存,其他线程自旋等待,只要更新线程能够设置缓存和及时释放更新锁就能达到效果

缓存预热

此场景发生在功能上线的时候,如果有大量的请求访问进来,但是刚刚上线的系统还没有任何缓存,这个时候所有请求又会到数据库中进行查询

根据缓存预热发生的条件,大量请求还未缓存是产生问题的原因,和此问题的名字一样,我们需要制定一些策略在上线之前对缓存进行预热

  • 使用预热程序,在系统上线之前先启动预热程序,将可能产生的缓存进行尽可能的放入缓存中,然后再进行部署