京东科技作者:刘须华
一、背景概述:
R2M缓存的使用,极大的提升了应用程序的性能和效率,特别是数据查询方面。而缓存最常见的问题是缓存穿透、击穿和雪崩,在高并发下这三种情况都会有大量请求落到数据库,导致数据库资源占满,引起数据库故障。平时对缓存测试时除了关注增删修改查询等基本功能,应该要重点关注缓存穿透、击穿和雪崩三种异常场景的测试覆盖,避免出现线上事故。
二、基本概念说明:
1、缓存击穿:是指在超级热点数据突然过期,导致针对超级热点的数据请求在过期期间直接打到数据库,这样数据库服务器会因为某一超热数据导致压力过大而崩掉。
2、缓存穿透:是指查找的数据在缓存和数据库中都不存在,导致每一次请求数据从缓存中都获取不到,而将请求打到数据库服务器,但数据库中也没有对应的数据,最后每一次请求都到数据库;如果在高并发场景或有人恶意攻击,就会导致后台数据库服务器压力增大,最终系统可能崩掉。
3、缓存雪崩:是指突然缓存层不可用,导致大量请求直接打到数据库,最终由于数据库压力过大可能导致系统崩掉。缓存层不可用指以下两方面:缓存服务器宕机,系统将请求打到数据库; 缓存数据突然大范围集中过期失效,导致大量请求打到数据库重新加载数据, 与缓存击穿的区别在于这里针对很多key缓存,前者则是某一个key。
三、测试工具(非必须):
1、使用Titan压测平台进行并发请求测试
2、使用jmeter工具模拟并发请求
四、测试方法举例说明(非必须):
环境:测试环境
工具:jmeter
(1)缓存穿透场景
测试方法:查询一个根本不存在的数据,缓存层和存储层都不会命中。
查询接口相关代码实现:
通过JMETER模拟多次重复调用:单线程重复调用
查看日志结果: 从日志可以看出:执行并发请求后, 所有请求每次都走向了数据库。
预防方案:
当数据库查询为空时,将缓存赋值默认值,后续查询都走缓存,减少数据库压力。
上述接口,增加赋值为empty,则第一次查询到数据库为空,后续查询都查询到缓存中,缓存值为empty。
再次执行并发测试:从日志可以看出,可以看出每个ID都只执行了一次数据库查询并设置缓存,之后请求都命中了缓存,有效防止了缓存穿透问题。
(2)缓存击穿场景
测试方法:对某个Key有大量的并发请求,这时从缓存中删除这个key。模拟热key过期失效的场景。这个时候大并发的请求可能会瞬间把后端DB压垮。
接口相关部分代码实现:
操作步骤:
1、查询pin为liuxuhua的请求,这时pin为liuxuhua的数据会加载到缓存
2、再次查询pin为liuxuhua的请求,命中缓存
3、50并发请求pin为liuxuhua的数据,这个时候请求全部命中缓存
4、将pin为liuxuhua的缓存手动删除,模拟缓存失效
5、50并发请求pin为liuxuhua的数据,这个时候大量请求走向数据库,pin为liuxuhua的缓存被击穿
查看日志结果:
预防方案:
在设置默认缓存值的基础上,进行加锁处理。只有拿到锁的第一个线程去请求数据库,然后插入缓存,当然每次拿到锁的时候都要去查询一下缓存有没有。
从日志记录可以看到只有一个请求执行了数据库查询并设置缓存,其他请求都命中了缓存, 有效防止了缓存的击穿。
(3)缓存雪崩
测试方法:对多个使用到缓存的接口进行并发调用,设置这些缓存时间已过期(即删除缓存),调用时这些接口查询缓存时无数据,去查询数据库,这些请求都指向数据库,数据库压力增大,耗时增加。
模拟接口:
通过JMETER模拟多次重复调用:单线程多接口重复调用
查看日志结果:可以看出大量请求到达数据库,并且同一个pin或id执行了多次数据库查询
预防方案:
增加限流操作,即接口频繁调用时,增加一个缓存,设置时间为3s,3s内处理一定次数的请求,超过限制次数的请求直接返回结果,不做处理。
接口:3s内处理6次请求,超过则不处理;
从日志可以看出:可以看到每个都只查询了一次数据库并设置缓存,之后的请求都命中了缓存
五、测试指标:(或者叫通过标准,包括关注点以及意义)
1、模拟缓存穿透场景测试,每个不存在的数据都只执行了一次数据库查询并设置缓存,之后请求都命中了缓存,有效防止了缓存穿透问题。
2、模拟缓存雪崩场景测试,每个缓存失效的数据都只执行了一次数据库查询并设置缓存,之后请求都命中了缓存。
3、模拟缓存击穿场景测试,缓存失效的那个数据只有一个请求执行了数据库查询并设置缓存,其他请求都命中了缓存。
六、适用业务场景:
1、秒杀活动
2、热门营销活动
3、618和双11大促
七、研发侧常见解决方案(参考):
1、缓存穿透解决方案:
(1)缓存空值 之所以发生穿透,是因为缓存中没有存储这些数据的key,从而每次都查询数据库 我们可以为这些key在缓存中设置对应的值为null,后面查询这个key的时候就不用查询数据库了 当然为了健壮性,我们要对这些key设置过期时间,以防止真的有数据
(2)BloomFilter BloomFilter 类似于一个hbase set 用来判断某个元素(key)是否存在于某个集合中 我们把有数据的key都放到BloomFilter中,每次查询的时候都先去BloomFilter判断,如果没有就直接返回null 注意BloomFilter没有删除操作,对于删除的key,查询就会经过BloomFilter然后查询缓存再查询数据库,所以BloomFilter可以结合缓存空值用,对于删除的key,可以在缓存中缓存null 缓存击穿
2、缓存击穿解决方案:
采用分布式锁,只有拿到锁的第一个线程去请求数据库,然后插入缓存,当然每次拿到锁的时候都要去查询一下缓存有没有
3、缓存雪崩解决方案:
(1)采用集群,降低服务宕机的概率
(2)ehcache本地缓存 + 限流&降级
(3)均匀过期,通常可以为有效期增加随机值