一、Redis大热key问题
# 1、大热Key的定义
一般而言,将大于1M或者Value超过5000个元素的一个KV存储,定义为大key(参考值);当一个Key的访问频率或资源占用明显高于其他Key时,则称之为热Key。 例如:每秒处理1000次请求,其中有300次都是操作同一个Key;
# 2、大热Key存在的问题
1、对于Redis集群中的单实例,一般会配置对应带宽200m/s,当访问的Key大且访问频次高容易超过Redis集群分片限流,导致线上服务受影响;
2、Redis集群中如果存在热Key,线上请求倾斜,大部分流量请求到热Key所在单实例,导致该实例CPU打满,影响服务;
# 3、案例分析
# 3.1、案例描述
某系统在双十一大促期间,遇到了一个严重的线上事故。业务人员创建一个大型活动,该大型活动由于活动条件和活动奖励比较多,导致生成的缓存内容非常大。活动上线后,系统就开始出现各种异常告警,核心接口可用率由100%持续下降到20%,系统访问Redis的调用次数和查询性能也断崖式下降,后续更是产生连锁反应影响了其他多个核心接口的可用率,导致整个系统服务不可用。
# 3.2、原因分析
在这个系统中,为了提高查询活动的性能,开发团队决定使用Redis作为缓存系统。将每个活动信息作为一个key-value存储在Redis中。由于业务需要,有时候业务运营人员也会创建一个非常庞大的活动,来支撑双十一期间的各种玩法。针对这种庞大的活动,开发团队也提前预料到了可能会出现的大key和热key问题,所以在查询活动缓存之前增加了一层本地jvm缓存,本地jvm缓存5分钟,缓存失效后再去回源查询Redis中的活动缓存,本以为会万无一失,没想到最后还是出了问题。
为什么加了本地缓存还是出了问题?这里其实就存在着第一个缓存陷阱:缓存击穿问题。首先解释一下什么是缓存击穿;缓存击穿(Cache Miss)是指在高并发的系统中,如果某个缓存键对应的值在缓存中不存在(即缓存失效),那么所有请求都会直接访问后端数据库,导致数据库的负载瞬间增加,可能会引发数据库宕机或服务不可用的情况。所以在本次事故里边,运营人员审批活动上线的一瞬间,活动缓存只是写入到了Redis缓存中,但是本地缓存还都是空的,所以此时就会有大量请求来同时访问Redis。 按照以往经验,Redis缓存都是纯内存操作,查询性能可以满足大量请求同时查询活动缓存,就在此时我们却陷入了第二个缓存陷阱:网络带宽瓶颈;Redis的高并发性能毋庸置疑,但是却忽略了一个大key和热key对网络带宽的影响,本次引发问题的大热key大小达到了1.5M,经过事后了解公司Redis对单分片的网络带宽也有限流,默认200M/s,根据换算,该热key最多只能支持133次的并发访问。所以就在活动上线的同一时刻,加上缓存击穿的影响,迅速达到了Redis单分片的带宽限流阈值,导致Redis线程进入阻塞状态,以至于所有的业务服务器都无法查询Redis缓存成功,最终引发了缓存雪崩效应。
# 4、解决方案
由于Redis是单线程架构,扩容CPU并不能解决问题,需要业务方进行改造,可以参考的改造方案包括如下:
(1)添加本地缓存,可以考虑增加应用层的本地缓存;当发现热key后,将热key加载到系统JVM中,这样请求就会直接从JVM中获取,而不会直接打到Redis,减轻了Redis压力。
(2)本地缓存预热,活动开启前,先对本地缓存进行预热处理,避免缓存击穿。
(3)热Key分片,可以考虑改造热key分布到不同分片;当发现热key后,将hotkey+随机数组合生成一个新key,打散到不同分片,这样就可以通过扩容分片,解决CPU 100%的问题。
(4)大key治理:更换缓存对象序列化方法,由原来的JSON序列化调整为Protostuff序列化方式。治理效果:缓存对象大小由1.5M减少到了0.5M。
(5)使用压缩算法:在存储缓存对象时,再使用压缩算法(如gzip)对数据进行压缩,注意设置压缩阈值,超过一定阈值后再进行压缩,以减少占用的内存空间和网络传输的数据量。压缩效果:500k压缩到了17k。
(6)缓存回源优化:本地缓存miss后回源查询Redis增加线程锁,减少回源Redis并发数量。