Redis缓存被击穿导致数据库压力暴增怎么预防
摘要:# Redis缓存被击穿,你的数据库扛得住吗? 我前两天帮一个做电商的朋友看线上问题,半夜两点接到电话,说数据库CPU直接飙到100%,整个站点卡得跟PPT似的。一查日志,好家伙,一个热门商品详情页的缓存Key刚好过期,瞬间几万请求全砸到数据库上——典型…
Redis缓存被击穿,你的数据库扛得住吗?
我前两天帮一个做电商的朋友看线上问题,半夜两点接到电话,说数据库CPU直接飙到100%,整个站点卡得跟PPT似的。一查日志,好家伙,一个热门商品详情页的缓存Key刚好过期,瞬间几万请求全砸到数据库上——典型的缓存击穿现场。
这种场景你应该不陌生吧?平时Redis跑得好好的,一到某个时间点或者某个热点数据失效,数据库压力瞬间爆炸。说白了,这就是缓存系统里最要命的那种“精准打击”。
一、缓存击穿到底是个啥?
先别被那些高大上的术语吓到。咱们用大白话讲:
想象一下双十一零点,所有人都在抢茅台。这个茅台的商品信息存在Redis里,设置了30分钟过期。好巧不巧,零点零一分,这个Key过期了。这时候几万人同时点开这个商品页面——Redis里没有,怎么办?程序只能老老实实去数据库查。
数据库平时也就接几十个查询,突然几万查询涌进来,什么概念?这就好比平时你家水龙头慢慢流水,突然消防栓爆了,水管直接炸裂。
很多团队上了Redis就觉得高枕无忧了,其实缓存击穿这种问题,往往在你最忙、最不能出错的时候给你来个“惊喜”。
二、为什么你的“常规方案”可能不靠谱?
1. 设置永久缓存?那是偷懒
我见过不少程序员图省事:“那简单啊,热点数据不设置过期时间不就完了?”
兄弟,这招真不行。首先,数据会变啊,商品价格调整、库存更新,你缓存不更新,用户看到的就是旧数据。更麻烦的是内存,Redis内存多贵你心里有数吧?一堆“永久”热点数据堆在那,迟早把内存撑爆。
2. 延长过期时间?治标不治本
把30分钟改成24小时?看起来击穿概率小了,但万一真在高峰期过期,场面更难看。而且数据延迟问题依然存在。
3. 那些PPT上很猛的方案,真用起来可能露馅
有些方案听起来很美好,比如“用分布式锁保证只有一个请求去查数据库”。理论上没问题,但在实际高并发场景下——锁竞争本身就成了瓶颈。我实测过一个电商项目,用Redis分布式锁处理击穿,QPS(每秒查询率)过万的时候,锁竞争直接让响应时间从20ms飙升到500ms以上。
三、真正能落地的预防方案(亲测有效)
方案一:缓存永不过期?不,是“逻辑上”永不过期
这是我最推荐的一种思路。具体怎么做:
- 缓存里不设过期时间——没错,物理上不设置
- 启动一个后台任务(或定时任务),定期异步更新缓存
- 程序读取时,如果发现缓存数据“太旧”(比如超过30分钟),就触发一次异步更新,但依然返回旧数据
# 伪代码示例,理解思路就行
def get_product_info(product_id):
data = redis.get(f"product:{product_id}")
if not data:
# 缓存不存在,从数据库加载(这里可以加锁防并发)
data = load_from_db(product_id)
redis.set(f"product:{product_id}", data)
return data
# 检查数据是否“太旧”
if data.update_time < now() - 30*60:
# 触发异步更新,不阻塞当前请求
async_update_cache(product_id)
return data
优点:永远有数据返回,永远不会因为过期导致击穿。数据更新有延迟,但通常能接受。
缺点:需要维护异步更新逻辑,架构稍复杂。适合那些数据变更不要求绝对实时(秒级)的场景。
方案二:互斥锁,但得用“聪明的锁”
如果你非得用锁,那得优化:
- 用缓存标记,别真锁数据库操作
- 锁要设置超时,防止死锁
- 获取锁失败后别傻等,用旧数据或默认数据快速返回
def get_with_lock(key):
data = redis.get(key)
if data:
return data
# 尝试获取锁
lock_key = f"lock:{key}"
# 用SET NX EX原子操作,避免竞态条件
locked = redis.set(lock_key, "1", nx=True, ex=5)
if locked:
# 拿到锁,查数据库
data = query_db(key)
redis.set(key, data, ex=300)
redis.delete(lock_key)
else:
# 没拿到锁,说明有其他线程在查数据库
# 方案A:短暂等待后重试
time.sleep(0.1)
data = redis.get(key)
# 方案B:返回兜底数据(如果有的话)
# data = get_default_data()
# 方案C:直接返回空/错误,前端展示“加载中”
return data
关键点:锁的过期时间一定要设置,5-10秒足够。我曾经见过设30秒的,一个慢查询直接让所有请求卡半分钟。
方案三:预热缓存,把工作做在前面
这招特别适合你知道什么时候会有热点的情况。比如:
- 秒杀活动开始前5分钟,提前加载商品数据到缓存
- 热门文章发布后,立即缓存
- 每天凌晨访问量低的时候,预加载当天可能的热点数据
# 活动开始前预热
def preheat_for_seckill(activity_id):
products = get_seckill_products(activity_id)
for product in products:
redis.setex(f"product:{product.id}", 3600, product.to_json())
# 顺便把活动信息也预热了
redis.setex(f"activity:{activity_id}", 3600, get_activity_info(activity_id))
实用技巧:根据历史数据自动识别热点。比如统计过去24小时访问最多的商品,每天凌晨预加载前100个。
四、不同场景怎么选方案?
场景1:电商商品详情页
推荐:逻辑永不过期 + 异步更新 理由:商品信息变更相对不频繁(价格、库存有专门系统处理),用户对稍微旧一点的数据容忍度高。绝对不能因为缓存问题导致页面打不开。
场景2:新闻、资讯详情
推荐:互斥锁 + 兜底数据 理由:内容更新后需要尽快生效。可以用旧文章作为兜底,但新文章发布后应该尽快可见。
场景3:配置信息、基础数据
推荐:永不过期,变更时主动更新 理由:这类数据很少变,变了也能接受短暂延迟。直接在管理后台更新时,同步更新所有Redis节点。
场景4:秒杀、抢购
推荐:预热 + 逻辑永不过期 + 限流降级 理由:这是最极端场景。除了缓存方案,一定要配合限流(比如前端排队、服务端限流),数据库前面加熔断器。
五、别忘了这些“辅助手段”
光防击穿还不够,得有个完整的防御体系:
- 数据库连接池调优:适当增大连接数,设置合理的等待超时
- SQL优化:被频繁查询的语句,索引必须到位。我见过因为没索引,一个简单查询拖垮整个库的
- 限流降级:在应用层或API网关做限流,超过阈值直接返回“稍后重试”
- 监控告警:设置缓存命中率监控,低于阈值就告警。监控数据库QPS突增
- 多级缓存:本地缓存(Caffeine/Ehcache) + Redis + 数据库。本地缓存可以有效缓解Redis压力
六、一个真实案例的解决过程
去年帮一个在线教育平台处理过这个问题。他们每周五晚8点有直播课,开课前10分钟,课程详情页访问量暴增。
问题就出在:课程信息缓存设置的是1小时过期,经常在7:50左右过期——正是访问量开始上涨的时候。
解决方案:
- 课程开始前2小时,缓存设置为“逻辑永不过期”
- 开发一个缓存预热任务,在每周五晚7点自动加载当天所有直播课信息
- 在课程详情页增加本地缓存(5分钟过期)
- 数据库查询加上限流,超过每秒1000查询直接走兜底数据
调整后,数据库压力峰值下降了80%,页面加载时间从偶尔的3秒+稳定到200ms以内。
最后说点大实话
缓存击穿这个问题,说难不难,就那么几种方案。但为什么那么多公司还是栽跟头?
因为很多团队只停留在“知道方案”的层面,没根据自己业务特点真正去适配、去压测、去演练。
你的业务数据更新频率怎么样?用户能接受多旧的数据?你的数据库极限QPS是多少?这些问题的答案,决定了你应该用哪种方案。
别等到数据库真挂了才着急。找个流量低峰期,模拟一下缓存Key同时过期,看看你的系统表现如何——我敢打赌,很多系统第一次测试都会出问题。
行了,方案都摆在这儿了,关键还是得动手去试、去调整。你的业务场景最适合哪种?评论区聊聊你遇到的坑?

