
本文聚焦Redis缓存实战中的三大核心痛点,结合真实业务场景拆解问题成因:从穿透时”空值缓存+布隆过滤器”的双重拦截,到击穿场景下”互斥锁+热点数据永不过期”的防护机制,再到雪崩预防的”过期时间随机化+多级缓存”组合策略,逐一提供可落地的解决方案。同时融入缓存预热、降级熔断、监控告警等配套实践,详解如何根据业务量级(如高并发秒杀、海量数据查询)动态调整策略,避免陷入”缓存配置全凭经验”的误区。
无论你是初涉分布式系统的开发者,还是需要优化现有缓存架构的运维人员,这份避坑指南都将帮你系统梳理缓存设计逻辑,掌握从问题诊断到方案落地的全流程方法论,让Redis缓存真正成为系统稳定性的”助推器”而非”风险点”。
你有没有遇到过这样的情况:线上系统突然变慢,用户投诉页面加载要等5秒以上,一查监控发现数据库CPU飙到90%,缓存命中率却跌到10%?去年帮电商客户做618秒杀系统时,就碰到过更惊险的——某个爆款商品的缓存key凌晨过期,瞬间几万请求直接打穿缓存冲向MySQL,3分钟内数据库就被压垮,订单服务直接熔断。后来复盘才发现,他们的缓存策略几乎是”拍脑袋”设计的:所有key过期时间统一设2小时,热点数据没特殊处理,连基本的空值缓存都没配。
其实Redis缓存这东西,看着简单,真要玩转了避坑,得把穿透、击穿、雪崩这三个”老大难”彻底搞明白。今天就用大白话给你拆解这三个问题的实战解法,全是我踩过坑、验证过的有效方案,不管你是开发还是运维,看完就能上手调。
从”数据库被打穿”到”零穿透”:空值缓存+布隆过滤器的组合拳
先说说缓存穿透——这就像有人故意往你家墙上扔石头,虽然石头(不存在的key请求)不大,但扔多了墙(数据库)也会被砸穿。比如电商系统里,总有人用不存在的商品ID疯狂刷接口,缓存里查不到,就直接去查数据库,结果就是数据库被这些无效请求拖垮。
为啥会出现穿透?
本质是”缓存未命中+数据库也未命中”。举个例子,用户搜”xxx商品12345″,但12345这个ID根本不存在,缓存里肯定没有,请求就一路跑到数据库,查了个空结果回来。如果这种请求每秒有几千次,数据库连接池很快就会被占满。
去年帮做商品搜索的朋友解决过类似问题,他们当时的日志里,每天有30%的请求都是查不存在的商品ID。一开始他们只加了空值缓存,就是数据库返回空结果时,也往缓存里存一个”空值”,比如setex null_key:12345 300 ""
(300秒过期),结果无效请求确实少了60%。但后来发现,有些恶意用户会用不同的随机ID刷,空值缓存存不过来,缓存里很快堆满了无效的空key。
这时候就需要布隆过滤器出场了。你可以把布隆过滤器理解成一个”门卫”,所有可能存在的商品ID(比如数据库里有的ID)都先注册到这个”门卫”那里。用户请求来时,先让”门卫”看看这个ID存不存在——如果”门卫”说”没有”,直接返回404,连缓存都不用查;如果”门卫”说”可能有”,再走缓存+数据库的流程。这里的”可能有”是因为布隆过滤器有极小的误判率(一般设0.01%以下),但比起穿透的风险,这点误判完全能接受。
具体怎么配?
简单两步:
这里有个坑要注意:布隆过滤器里的数据要定期更新。比如商品表新增了ID,得同步到过滤器里,不然新商品就会被当成”不存在”拦截掉。我们当时是用定时任务每天凌晨从数据库同步一次,如果你是高频率新增数据的场景,可以用Canal监听数据库binlog实时同步。
Redis官方文档里其实提过,对于海量低命中率的查询场景,布隆过滤器是性价比极高的方案(Redis官方文档关于布隆过滤器的应用场景,nofollow)。现在我朋友的系统,无效请求被拦截了99.9%,数据库负载直接降了80%,空值缓存里的key也从每天几十万降到了几千个。
热点key”炸穿”缓存?互斥锁+永不过期的双重保险
再说说缓存击穿——这就像千军万马过独木桥,桥(热点key)突然断了,所有人(请求)都掉到河里(数据库)。比如秒杀活动里的”9.9元手机”,这个商品的key是热点中的热点,一旦过期,几万请求同时发现缓存失效,就会同时去数据库查数据、更新缓存,瞬间把数据库冲垮。
击穿的核心原因
是”热点key集中过期+并发请求量巨大”。去年双11前,帮电商客户做秒杀系统压测时就碰到过:当时把热点商品key的过期时间设成了2小时,结果到点时,5万个请求同时发现缓存失效,一起去打数据库,直接把MySQL的CPU干到100%,订单服务超时熔断。
解决击穿有两个实战效果最好的方案,你可以根据场景选:
方案一:互斥锁——让请求”排队”更新缓存
简单说就是,当缓存失效时,不是所有请求都去查数据库,而是只有一个请求能拿到”锁”,去数据库查数据、更新缓存,其他请求都等着,等缓存更新完了再从缓存拿数据。这就像厕所排队,一次只能进去一个人,其他人在外面等着。
具体实现可以用Redis的setnx
命令(set if not exists),比如请求进来发现缓存失效,就执行set lock:hotkey true ex 5 nx
——如果返回成功(拿到锁),就去查数据库、更新缓存,最后del lock:hotkey
释放锁;如果返回失败(没拿到锁),就等100ms再重试,直到缓存更新完成。
这里有个细节要注意:锁的过期时间一定要设,比如5秒,防止拿到锁的请求意外挂了,导致锁永远不释放。当时我们压测时,把锁超时设成了3秒(根据数据库查询耗时定,一般比正常查询时间长2倍),重试间隔100ms,结果5万个请求里,只有1个请求去查了数据库,其他都在重试后从缓存拿到了数据,数据库压力直接降了99%。
方案二:热点数据”永不过期”——给key”买保险”
如果你的热点key数据更新不频繁(比如首页banner图、秒杀商品信息),可以直接让它”永不过期”。注意不是真的不设过期时间(persist key
),而是把过期时间存在另一个key里,比如hotkey:expire
存时间戳,业务代码里每次拿数据时,先判断这个时间戳是否过期——过期了就异步去更新缓存,不阻塞当前请求。
举个例子:商品详情页的热点keygoods:10086
,不设过期时间,但存一个goods:10086:expire 1620000000
(时间戳)。每次请求进来,先查goods:10086
的数据,再查goods:10086:expire
的时间戳,对比当前时间——如果没过期,直接返回数据;如果过期了,就开个异步线程去查数据库、更新goods:10086
和goods:10086:expire
,当前请求还是返回旧数据。这样用户体验不受影响,缓存也不会突然失效。
美团技术团队在他们的秒杀系统里就用过类似方案(美团技术博客:秒杀系统的缓存设计,nofollow),他们提到对于更新频率低的热点数据,”逻辑过期”比”物理过期”更稳定,能避免过期瞬间的并发问题。
从”缓存集体失效”到”雪崩防护”:过期时间随机化+多级缓存的组合策略
最后说缓存雪崩——这可比前两个问题严重多了,就像雪山发生雪崩,一片雪(缓存key)塌了,会带动更多雪(其他key)塌,最后把整个村庄(系统)埋了。比如你给所有缓存key都设了”0点过期”,结果到0点时,大量key同时失效,请求全去打数据库,导致数据库宕机,然后缓存服务器因为连接不上数据库,新的缓存也生成不了,形成恶性循环。
前年帮金融客户做系统时就踩过这个坑:当时为了方便管理,所有缓存key的过期时间都设成了”86400″(24小时),结果每天凌晨3点(系统低峰期),缓存大面积失效,数据库CPU从10%飙升到90%,持续20分钟才恢复。后来查日志发现,光是用户余额缓存,就有50万个key在同一秒过期。
雪崩的根源
就是”过期时间集中+缓存层不可用”。要解决它,得从”避免同时过期”和”防止缓存层挂了”两方面入手: 第一步:过期时间”随机化”——给key”错开生日”
最简单有效的办法,就是给每个key的过期时间加个随机值,比如本来想设2小时过期,就改成23600 + random(0, 600)
(2小时+0-10分钟随机),这样原本集中在同一秒过期的key,就会分散到10分钟内慢慢过期,数据库就能”错峰”处理缓存更新请求。
当时金融客户的系统,我们把所有key的过期时间都加了random(0, 1800)
(0-30分钟随机),结果凌晨3点的数据库峰值CPU从90%降到了30%,完全在正常范围内。这个方法实现起来也简单,代码里改一行就行,比如Java里int expire = 86400 + new Random().nextInt(1800);
。
第二步:多级缓存——给系统”穿防弹衣”
就算缓存层(Redis)突然挂了,也不能让请求全去打数据库。这时候就需要多级缓存:本地缓存(比如Caffeine、Ehcache)+ Redis + 数据库。用户请求进来,先查本地缓存,没有再查Redis,最后查数据库。
比如电商首页的热点数据,除了Redis缓存,还可以在应用服务器的本地缓存里存一份,过期时间比Redis短一点(比如Redis存2小时,本地存10分钟)。这样就算Redis挂了,本地缓存还能撑10分钟,足够你重启Redis或者切备用集群了。
美团技术团队在《分布式系统缓存设计》里提到,他们的多级缓存架构能将Redis故障时的请求量拦截70%以上(美团技术团队:多级缓存实践,nofollow)。当时我们给金融客户加了Caffeine本地缓存,配置maximumSize=1000
(最多存1000条热点数据),expireAfterWrite=600
(10分钟过期),Redis故障演练时,本地缓存确实扛住了大部分请求。
最后给你个小工具:调缓存时,一定要开Redis的慢查询日志(slowlog-log-slower-than 1000
,记录1ms以上的命令)和监控告警(比如用Prometheus监控keyspace_hits
/keyspace_misses
计算命中率,低于90%就告警)。去年有个朋友就是因为没开监控,缓存命中率掉到60%都没发现,直到数据库出问题才后知后觉。
你平时调Redis缓存时,有没有遇到过特别棘手的问题?比如缓存和数据库数据不一致?可以在评论区说说,咱们一起拆解解决方案~
其实空值缓存和布隆过滤器的选择,主要看你系统里“无效请求”的调皮程度。我之前帮朋友调过一个博客系统,他们后台每天会收到一些用户输错的文章ID请求,比如把“202310”写成“202301”,这种就是典型的“无效请求量不大、key随机性低”的场景——每天也就几百条,重复的ID还挺多(同一个用户可能输错好几次)。当时就简单配了空值缓存:数据库查不到结果时,就存一个空字符串到Redis,过期时间设5-10分钟,比如setex article:202301 600 ""
。结果一周后看日志,重复的无效请求直接少了80%,数据库压力小了一大截,这种场景下空值缓存就够用了,简单还不占资源。
但要是遇到“无效请求量大到离谱、key还特别随机”的情况,空值缓存就扛不住了。去年帮一个电商客户处理过爬虫攻击,对方用脚本生成随机商品ID(比如100000-999999之间的随机数),每秒发几千个请求过来,想爬他们还没上架的商品信息。一开始也用空值缓存,结果三天下来Redis里堆了20多万个空值key,不仅占内存,缓存命中率还跌到60%——因为这些随机ID几乎不重复,空值缓存等于白费劲。后来上了布隆过滤器才解决:先从数据库把所有有效商品ID(大概50万条)导出来,用Guava的BloomFilter初始化,误判率设0.01%,然后在API入口加了层过滤——请求过来先查布隆过滤器,不存在的ID直接返回404,存在的才走缓存+数据库流程。这么一搞,无效请求被拦截了99.5%,空值缓存里的key也降到每天几百个。不过得注意,布隆过滤器不是100%准的,0.01%-1%的误判率还是会让极少数无效请求漏过去,所以最后还是得配上空值缓存兜底,相当于“双保险”。
什么是缓存穿透?和缓存击穿、缓存雪崩有什么区别?
缓存穿透、击穿、雪崩是Redis缓存的三大核心问题,核心区别在于“影响范围”和“触发原因”:缓存穿透是无效请求(不存在的key)穿透缓存直击数据库,比如用随机ID刷接口;缓存击穿是单个热点key突然失效,导致大量并发请求同时访问数据库,比如秒杀商品key过期;缓存雪崩则是大量key集中过期或缓存服务不可用,引发请求“雪崩式”冲击数据库,比如所有key设置同一过期时间。三者的影响程度递增,穿透影响单点查询,击穿影响热点服务,雪崩可能导致整个系统瘫痪。
什么场景下适合用布隆过滤器?什么场景下用空值缓存就够了?
空值缓存适合无效请求量不大、key随机性低的场景,比如正常业务中偶尔出现的不存在ID查询(如用户输错商品ID),只需缓存空值5-10分钟即可拦截大部分重复请求。而布隆过滤器适合无效请求量大、key随机性高的场景,比如电商平台被恶意刷随机商品ID、爬虫批量爬取不存在资源,此时空值缓存会导致缓存膨胀,布隆过滤器可通过预加载有效ID集合,在请求入口直接拦截99%以上的无效请求(注意布隆过滤器有0.01%-1%的误判率,需配合空值缓存兜底)。
热点数据“永不过期”会不会导致数据不一致?如何解决?
热点数据“永不过期”并非物理上不过期(Redis key设置persist),而是业务层逻辑过期:key本身不设过期时间,但存储数据时额外加一个“过期时间戳”,业务代码读取时先判断时间戳是否过期——未过期直接返回,已过期则异步触发数据更新(不阻塞当前请求)。这种方式几乎不会导致数据不一致,因为:
如何通过监控指标提前发现Redis缓存异常?
重点关注三个核心指标:
多级缓存(本地缓存+Redis)架构中,如何避免数据不一致?
多级缓存(如应用本地Caffeine缓存+Redis)的数据一致性可通过“更新策略”保障: