在之前的一篇文章《一次缓存雪崩的灾难复盘》中,我们比较清晰的描述了缓存雪崩、穿透、击穿的各自特征和解决方案,想详细了解的可以移步。
最近在配合HR筛选候选人,作为大厂的业务方向负责人,招人主要也是我们自己团队在用,而缓存是必不可少的面试选项之一。下面我们就来聊一聊在特定业务场景下缓存击穿和雪崩的应对场景!
★ 分析:上述类型的应用具有很明显的峰值 高斯分布的特征,就是9~10点是用户早高峰。微信是,百度APP是,钉钉也是,钉钉一般给政企、教学等使用,通用是10点左右峰值期,每天的峰值如下:
既然是可预见的峰值期,那么缓存预热是一个好办法,比如在9 ~ 10点是高峰期,在7 ~ 9点这两个小时中,可以均匀的把部分缓存做上。
缺点:这种仅仅只能解决可预见的缓存失效情况。如果是突发缓存失效情况,假设在10点高峰期因为某些原因(比如上面说的 故障导致缓存失效、程序bug导致缓存误删、服务器重启导致内存清理)是没有效果的。
缓存既然大部分是在高峰期(9~10点)创建的(假设Cache的Expire Time都一样,比如8h),那很有可能失效时间会很接近。几乎同一时间一起失效,这样确实也会引起群起创建的情况,也会导致上面说的击穿的情况发生。
我们在创建同一类型的批次缓存的时候,会采用3-4-3 分布原则。比如一个缓存的Expire Time 是 10H,
那么就是3H + 4H * random() + 3h ,来进行错开!
缺点:同4.1类似,仅仅解决可预见的问题,对突发故障导致的无预期的缓存失效毫无办法。
为什么每个用户的基本信息都独立存储一个缓存呢?可不可以按照用户类型分片,一类的用户合在一起不是只要查询一次,不会出现峰值期群起攻击数据库的情况。
说明:只有信息修改率非常低的缓存才适合聚合在一个缓存值中,大部分情况下不会这么做。比如你的缓存中聚合了1W个人的信息,Value非常大,但凡其中一个信息修改,那么这个缓存就要更新,不然应用读取到的信息就没有时效性,大Value的缓存频繁的存取是一个很不友好的事情。
用户信息还算修改频率比较低的,你的积分信息,购物车可是很高频变动的,这种的就不能这么干了。
引进消息队列之类的中间件,将用户的请求放入队列,逐一执行,避免拥挤请求!
同一个用户的信息查询只让第一个请求进入,进入之后加锁,在获取到数据库信息并更新缓存之后释放锁,
这样单一个信息只请求一次!
为了避免把服务端打挂,在上线前做一次无缓存压测,看数据库与服务端能支撑的最大值。并设置成限流的阈值,保证不会超过服务所能承载的压力,避免过载!
缺点:
备注:数据库也有限流方案,细粒度到这个层级更好
你的缓存层存在主备场景,他们之间定时异步同步,所以存在短暂数据不一致。
当你的主服务挂了之后,降级去读备服务,数据时效性没那么高,但是也避免了数据库被打穿的情况发生。
参考Redis 6.0的 Client Side Cache,看我这篇《追求性能极致:客户端缓存带来的革命》。
类似4.5做法,客户端缓存时效性会差一点,毕竟存在订阅跟同步的过程,数据没那么新。但是避免大量的请求直接上缓存服务,又因无效的缓存服务有把压力转移给数据库。
这是一种短暂降级的方式,大概流程如下:
可以看出,整个过程中我们牺牲了A、B、C、D的请求,他们拿回了一个空值或者默认值,但是这局部的降级却保证整个数据库系统不被拥堵的请求击穿。
在不同的场景下各种方法都有各自的优缺点,我们要做的就是根据实际的应用场景来判断和抉择。
本文与大家一起学习并介绍领域驱动设计(Domain Drive Design) 简称DDD,以及为什么我们需要领域驱动设计,它有哪些优缺点,尽量用一些通俗易懂文字来描述讲解领域驱动设计