事情回溯
这几年618大促越来越早了,5月14日集团决定要加一场全链路压测用于验证16日的618付尾款的链路稳定性。
于是我们白天就准备好了晚上压测用到的数据预热任务:预热任务是12点启动,数据的缓存过期时间是43200秒,即12个小时,也就是晚上12点数据过期,足够覆盖压测的时间段了。
我们的预热逻辑是这样的:从odps离线表里拿到key,然后预热接口会请求这些key,如果没有的话,就会去数据库里查,然后再put到缓存里,逻辑很简单。
然后整个预热过程非常顺利,命中率和成功率都满足要求:
晚上压测分为几个阶段:20点压“现货+尾款”阶段,系统表现正常符合预期。然后21点休息半小时,21点半开始脉冲流量压“纯现货交易”阶段,而这一波流量刚起来,上游就在群里反馈由于我们系统挂了导致他们出现了大量错误:
然后我们一看错误细节,发现是缓存有大量的写操作,即数据库被打爆了。换言之就是缓存数据过期了,但是不应该呀,上面说过了,我们的数据应该是晚上12点过期的,为什么21点半就过期了?
再针对一个key,看了一下具体的put时间和失效时间:
先去tair里,发现一天内并没有逐出的现象,而且tair的容量也没有满:
然后预热的离线表内容也没有任何的变化,前几天也用的同样的预热数据压测却没有任何的问题,可见不是因为预热数据发生了变更,导致有数据漏掉了:
后来把这个缓存的写入时间拉长发现了一点端倪:
后来问了一下负责压测的同学,他早上9点上班之后,会用压测流量走一下渲染的接口拉一把最新的偏移好的营销压测数据,防止营销数据过期导致压测的时候下单阻断,这个是常规的操作。而我们这个应用是在渲染的下游,所以早上他跑接口的时候也会请求到我们这个应用。
结论就是:击穿到DB的请求实际没有影子数据,会通过空保护对象进行缓存,这类对象无法通过主动预热任务写进缓存,依赖JIT压测流量自然预热。早上09:10到09:50有压测试跑流量,这时会将这些流量的空保护对象写进缓存,压测数据过期时间统一设置为12h +(1min内随机时间),因此晚上21点到21点30期间这些空保护对象会集中失效,从而导致21:31分脉冲时缓存击穿到DB,进而引起DB抖动。
事后action:将缓存失效时间打散,不要一批全部失效,打散时间从原来的1分钟延长到1个小时内随机失效。同时减少tair调用量,降低空查询比例,提升主动预热任务的有效性。
空保护
啥是空保护呢?是一种通过缓存“空值”来防止缓存穿透的策略。它的核心目标是避免因查询不存在的数据而导致数据库或后端服务被频繁访问,从而保护系统稳定性。
例如用户恶意构造不存在的ID,导致每次请求都会穿透到数据库,可能引发数据库压力激增甚至崩溃。举个例子:用户频繁查询 ID=99999999 的数据(该ID在数据库中不存在)→ 缓存中没有该ID → 请求穿透到数据库 → 数据库返回空 → 重复触发,直到嗝屁。
所以就通过缓存“空值”(如 null 或特定标记),将不存在的数据结果缓存一段时间,避免重复请求穿透到数据库。
实现流程:
1 | 1.查询缓存: |