暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

深思:缓存击穿(并发)、缓存穿透和缓存雪崩

Onebyte 2021-04-14
243

01

案发回顾


三月一天,线上某核心服务在业务高峰期突然出现卡死、异常提示和Tomcat线程资源耗尽。经过排查分析,发现如下异常堆栈:
Dubbo线程池因请求阻塞卡顿,导致线程池资源耗尽:
Caused by: java.util.concurrent.RejectedExecutionException: Thread pool is EXHAUSTED! Thread Name: DubboServerHandler-172.16.4.1:20051, Pool Size: 2100 (active: 2100, core: 0, max: 2100, largest: 2100), Task: 874734 (completed: 872634), Executor status:(isShutdown:false, isTerminated:false, isTerminating:false).
  ... 24 more

导致请求链路阻塞的根本原因是大量并发请求到达数据库,导致其连接被耗尽:

java.lang.RuntimeException: org.mybatis.spring.MyBatisSystemException: nested exception is org.apache.ibatis.exceptions.PersistenceException: 
### Error querying database. Cause: org.springframework.jdbc.CannotGetJdbcConnectionException: Could not get JDBC Connection; nested exception is com.alibaba.druid.pool.GetConnectionTimeoutException: wait millis 45003, active 50
  ... 77 more

经过分析无法获取连接的SQL语句,以及相关业务,我们发现该查询操作,应从缓存读取,但为什么大量请求打到数据库?在排除了缓存未生效的情况下,我们初步断定是由于高峰期大量并发导致的缓存击穿,甚至缓存穿透。
基于缓存击穿的解决思路,临时发布了服务,次日高峰期,一切正常。

02

常见的缓存使用模型(选读)


常见缓存使用模型:双读双写、异步更新和串联模式。

双读双写

这是最常用的缓存使用模型:

对于读操作:先读缓存,如果缓存不存在,则再读数据库,读取数据库后再回写到缓存;

对于写操作:先写数据库,后写缓存。

异步更新

异步更新的示意图如下:

对于异步更新方式,应用层只读写缓存;缓存中存放全量数据,且不设置其过期时间,由异步的更新服务将数据库的新增或变更刷新到缓存中。

串联模式

串联模式示意图如下:

应用直接在缓存上进行读写操作,缓存作为代理层,根据需要和配置与数据库进行读写操作。在微服务中,不推荐此种方式。

03

缓存击穿(缓存并发)


在高并发的业务场景下,当缓存中某个key失效时,大量请求并发涌进,近乎同时发现缓存失效,因此会有多个请求同时访问数据库,并且回写缓存,这将导致应用卡顿、数据库负载增大和服务器资源逐步耗尽等问题。
注:这里说的缓存失效指缓存过期、缓存由于淘汰策略被清理和其他情况,是泛指。

如上图,缓存击穿发生条件:
  • 应用缓存设计上采用双读双写的模型;
  • 大量请求并发访问缓存中的某个key(一般为热点数据),发现该key失效,故多个请求同时访问数据库,并回写缓存。

解决方案:

分布式锁:保证对于缓存中的每个key同时只有一个线程去查询数据,并回写缓存,其他线程如若获取不到锁,等待即可;

本地锁:与分布式锁类似的思路,解决单个节点的缓存并发问题,分布式场景下仍然可能发生;

软过期:对缓存中的数据设置失效时间(注:不是缓存中间件的过期时间),不使用缓存服务提供的过期时间,而是业务层在数据中存储过期时间的信息,由业务程序判断该数据是否过期并更新,在发现了数据即将过期时,可将缓存的时效延长,程序可以派遣额外的线程去数据库获取最新的数据,并写回缓存。同时,其他线程看到延长了过期时间,就会继续使用旧数据,等派遣的线程将数据回写缓存成功后,其他线程即可读取到最新的数据。
也可以通过异步更新服务来更新设置软过期的缓存,这样应用层就不用关心缓存并发的问题了。

04

缓存穿透


缓存穿透指大量查询请求访问系统(缓存和数据库等)中并不存在key,这导致缓存无法命中,大量的请求涌进数据库进行查询,使数据库服务器资源耗尽。

如上图,缓存穿透的发生条件:
  • 应用缓存设计上采用双读双写的模型;
  • 大量请求并发访问,系统(缓存和数据库等)中不存在的key;

  • 缓存无法命中,请求涌进数据库,耗尽数据库服务器资源。

解决方案:

空值缓存:如果该次请求未查询到结果,缓存一个空结果对象;再次接到相同请求,即从缓存中查询,直接返回;这种方式无法避免恶意攻击;

入参校验:对入参的ID进行格式分析,如果不符合ID的生成规则,就直接拒绝,快速失败。甚至可以在ID生成上设计一些时间戳信息和校验串信息,这样在进行校验时候可以过滤掉一定数量的无效请求;

布隆过滤器:将缓存数据也映射到布隆过滤器中,当请求进来时,首先判断是否在布隆过滤器中存在,若不存在直接快速失败即可。即使布隆过滤器存在一定的误判率,也可以有效抵挡大部分的无效请求。

注:如果有对布隆过滤器不是很熟悉的同学,可以在另一篇文章中解惑:

众里寻他千百度:Bloom Filter

05

缓存雪崩


缓存雪崩指缓存服务器重启或者大量缓存集中在某个较短的时间段内失效,给后端数据库造成瞬时的负载升高,甚至耗尽数据库服务的资源,导致应用卡死。

如上图,缓存雪崩的发生条件:
  • 应用缓存设计上采用双读双写的模型;
  • 缓存服务器重启、耗时卡顿和假死,或者大量缓存集中在某个较短的时间段内失效;

解决方案:

高可用性:缓存服务器做好高可用,主从部署;

缓存预热与降级:必要时候做好缓存预热、缓存降级;

多级缓存方案:本地缓存 + Redis缓存 + 服务降级熔断策略;当Redis缓存失效时候,本地缓存兜底应急;

针对时间无关性:给缓存中不同的key设置随机的过期时间;

针对时间相关性:需考虑加锁,或者软过期;使到达数据库的请求数量控制在数据库性能极限以下。
注:时间相关性,如果缓存的过期时间和业务无关,则可以随机设置过期时间;如果缓存必须在某一时刻过期,与业务相关,则称作时间相关性,此时不能随机设置过期时间。

06

三者的共同点与不同点


共同点:
  • 大量请求,高并发场景;

  • 缓存失效后需要访问数据库;
  • 导致数据库和调用链路资源紧张,甚至导致应用程序停止服务。

不同点:

  • 不同的触发条件和产生原因。缓存击穿发生在热点key被高并发访问的时候、缓存穿透指key在系统(缓存和数据库)中不存在,缓存雪崩指大量缓存在某一时间段失效;

  • 不同的解决方案和策略。

07

总结


合理使用缓存。在分布式系统的缓存设计中,不仅要考虑业务的缓存设计,更要看到缓存实践后所潜在的风险。及时避免,未雨绸缪。

08

引用



[1] https://juejin.cn/post/6844903807797690376

[2] https://xie.infoq.cn/article/a035f12e5590385ac578778b0

[3] https://developpaper.com/cache-penetration-cache-breakdown-cache-avalanche-solution-analysis/

[4] 李艳鹏等,可伸缩服务架构:框架与中间件,电子工业出版社


点个在看你最好看

文章转载自Onebyte,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论