优质博文:IT-BLOG-CN
一、背景
为什么要写这篇文章?
生产缓存生成服务转java
时,需要通过配置文件进行流量切换。开发人员同时打开了两个配置页面。原配置信息=ABCDEF
。在第一个配置页面进行缓存切换,添加G
业务缓存,配置信息=ABCDEFG
。随后H
业务也需要进行缓存切换,但开发人员在第二个配置页面添加了H
,配置信息=ABCDEFH
导致第一次添加的G
业务写缓存丢失。随着时间的推移8
分钟后G
业务缓存陆续过期,造成缓存雪崩。
为什么保存配置和审核配置的时候没有发现异常
切流量设计10
多种业务需要修改20
多次配置,操作上惯性认为页面刷入是最新的版本,没有逐一进行检查。
监控为什么没有及时告警
监控比较滞后,目前监控的指标比较粗粒度,只有缓存过期后,导致订单量下降才会进行告警。同时缓存命令率的监控也是10
分钟后直接掉至0
。
影响的单量
影响订单300
单,并立即回滚配置信息。
二、缓存雪崩
大量的key
设置了相同的过期时间,或者因为Redis
实例宕机,在同一时刻缓存全部失效,造成请求全部穿透到DB
,瞬时DB
请求量大、压力骤增,可能导致DB
被打挂。
解决方案:
【1】过期时间打散: 避免大量key
同时过期,可以在设置过期时间时,在基础过期时间上加一个随机值来打散过期时间,通过打散过期时间,避免同一时间大量缓存过期。
【2】不过期: 不为数据设置过期时间或者设置比较长的过期时间,由专门的job
应用定时刷新缓存。
【3】Redis
高可用: 通过主从复制,故障转移等高可用机制避免 Redis 集群不可用导致的缓存雪崩。
【4】数据库限流与熔断: 对数据库的读添加限流与熔断机制,目的主要是为了在无可避免的缓存雪崩时保护数据库,使得之后能快速恢复缓存。
【5】数据库解耦: 应用完全与数据库解耦,只读Redis
,由专门的job
应用主动填充缓存。
三、缓存穿透
访问一个缓存和数据库都不存在的key
,此时会直接打到数据库上,并且因为查不到数据,也不会写入缓存,所以下一次同样会打到数据库上,请求每次都会走到数据库,流量大时可能导致数据库被打挂。
解决方案:
【1】空值缓存: 查询数据库发现没有数据,给对应的key
存入一个空值缓存,代表数据库中没有数据,应用查询到空值就直接返回,避免了穿透到DB
的读流量。
【2】布隆过滤器:
1、空值缓存已经能解决问题,但是在数据量非常大的情况下,我们还得考虑空间利用的高效。
2、布隆过滤器是这样一个数据结构,它有一个初始值为0
的bit
数组以及N
个hash
函数构成,它可以快速判断或标记当前值是否存在,标记的主要步骤为三个:
☑️ 使用N
个hash
函数计算的N
个hash
值。
☑️ N
个hash
值与bit
数组取模得到N
个映射的下标。
☑️ 将bit
数组中N
个下标对应的位置置为1
。
3、当我们需要查询一个值时,重复上述步骤1,2
得到N
个下标,当这N
个下标上对应的值均为1
,则说明该值已被标记,否则未被标记。
4、当然由于存在hash
冲突,会存在一定的误判,所以布隆过滤器的特点是判断不存在的,则一定不存在;判断存在的,大概率存在。以存在误判的代价,换取空间利用上的高效。
【3】非法值校验: 对于一些请求参数,我们是能够判断出是否合法,如果不合法直接在入口处拦截,自然不需要穿透到DB
。比如机票查询请求了一个不存在的航线,我们可以在入口校验时将该请求拦截。
四、缓存击穿
存在一个高并发读的热点key
,在缓存过期的一瞬间,有大量的读请求,由于此时缓存过期了,所以请求最终都会走到数据库,造成瞬时数据库请求量大、压力骤增,可能导致数据库被打挂。
解决方案:
【1】锁:
1、当大量请求并发读取缓存并失败时,其实只需要一个线程/机器去访问数据库并填充缓存,其他线程/机器只需等待缓存填充完成后读取缓存就行。
2、我们可以通过互斥锁使得只有一个线程/机器能够访问数据库并填充缓存,线程间的同步比较容易,因为是在一个进程内,使用java
提供的同步机制,比如ReentrantLock
即可。
3、机器间的同步,由于涉及到分布式环境,因此需要分布式锁来进行同步。简单的分布式锁可以通过Redis
或者DB
实现,更高级的可以通过zookeeper
这样的分布式协调服务来实现。
【2】热点数据不过期: 既然缓存击穿是由热点key
过期导致的,那么我们可以不为热点数据设置过期时间,而是由专门的后台job
应用进行定时的刷新,在携程内部qshcedule
能很好的解决我们的需求。