缓存击穿是指 一个非常热点的数据(被高并发访问)在缓存中过期失效的瞬间,导致大量并发请求同时穿透缓存,直接落到底层数据库,造成数据库瞬间压力剧增甚至崩溃的现象。
关键特征和你的描述解析
“数据库没有就需要命中的数据” - 不完全准确:
缓存击穿发生时,数据库里通常是有这个数据的!这是它与“缓存穿透”(查询不存在的数据)的关键区别。
问题的核心在于:缓存层暂时失去了对这个热点数据的保护能力(因为它刚好过期了)。
“导致Redis一直没有数据” - 原因在于并发重建:
当第一个请求发现缓存过期(或不存在),它会去数据库查询,然后打算把结果写回Redis。
问题在于,在这个第一个请求从数据库查完数据、准备写回Redis的极短时间窗口内,成百上千的其他并发请求也同时到达了。它们也发现缓存是空的(因为第一个请求还没写完),于是它们也都认为需要自己去数据库查!
结果就是:大量请求在极短时间内重复查询同一个数据,数据库不堪重负。Redis 不是“一直没有数据”,而是在缓存失效后、第一个请求成功重建缓存之前的那一瞬间,它无法为后续请求提供数据保护。
“而一直命中数据库” - 这是核心表现和危害:
在缓存失效的那一瞬间,所有针对这个特定热点Key的请求,都绕过了Redis,直接打到了数据库上。
如果这个Key访问量极大(热点),数据库瞬间就可能被压垮(CPU飙升、连接池耗尽、响应超时),进而影响整个系统。
用场景说明缓存击穿
场景: 一个电商网站首页,有一个“今日秒杀爆款”商品的信息(商品ID=1001)。这个信息被缓存在Redis中,设置过期时间为5分钟。
正常情况: 用户访问首页,请求“秒杀爆款”信息,Redis直接返回,数据库很轻松。
缓存击穿发生:
下午2:00:00,商品1001的缓存刚好过期。
下午2:00:01,同时有10,000个用户刷新了首页,都需要获取商品1001的信息。
这10,000个请求都发现Redis中没有商品1001的缓存了。
于是,这10,000个请求几乎同时(在毫秒级内)都去数据库查询商品1001的信息。
数据库瞬间收到10,000个完全相同的查询请求。即使数据库能处理,也会消耗大量资源,导致响应变慢甚至崩溃。其他业务的数据库请求也可能被阻塞。
为什么说你的描述抓住了核心?
“Redis一直没有数据”: 在缓存失效后、第一个请求重建缓存成功前,对于后续请求来说,Redis 看起来 就像是“一直没有”这个数据(虽然它马上就要有了)。
“一直命中数据库”: 在缓存重建完成之前,所有请求确实都命中了数据库。这个“一直”是相对的,指的是在缓存失效后的那个短暂但致命的并发高峰期内持续命中数据库。
如何解决缓存击穿?
核心思路:防止在缓存失效瞬间,大量请求同时去数据库重建缓存。
互斥锁(分布式锁):
当第一个请求发现缓存失效时,它先去获取一个针对这个Key的分布式锁(例如用Redis的
SETNX
命令)。只有拿到锁的请求才有资格去数据库查询数据并重建缓存。
其他并发请求拿不到锁,就等待(短暂睡眠后重试)或者直接返回一个空值/默认值/错误提示(根据业务容忍度),直到缓存被重建好。
优点: 保证只有一个请求去数据库,彻底防止数据库被压垮。
缺点: 增加了复杂度(锁管理),可能造成部分请求延迟。如果拿锁的请求重建失败,需要处理锁释放和重试机制。
“逻辑”过期:
在缓存的数据结构中,不设置物理TTL过期时间。
而是在存储的值里额外加一个逻辑过期时间字段。
当请求读取缓存时:
如果发现数据存在,且逻辑过期时间还没到,直接返回数据。
如果发现数据存在,但逻辑过期时间已到:
当前请求立刻返回旧数据(虽然过期了,但总比没有好)。
当前请求异步触发一个后台任务去数据库拉取新数据并更新缓存(同时更新逻辑过期时间)。
优点: 用户请求基本无延迟(总是有数据返回),数据库压力平缓(只有一个后台任务去更新)。
缺点: 实现更复杂,存在短暂的数据不一致(用户看到的是略微过期的旧数据)。需要维护逻辑过期时间。
永不过期 + 后台更新:
缓存设置永不过期。
由独立的后台进程/定时任务定期去数据库检查数据是否变更,如有变更则主动更新缓存。
优点: 用户请求永远命中缓存(除非数据真的被删了),数据库压力最小。
缺点: 数据更新有延迟(依赖后台任务频率)。如果后台更新失败,缓存会一直保持旧数据。需要实现可靠的后台更新机制。
总结
缓存击穿是指 一个热点Key缓存失效的瞬间,大量并发请求同时穿透缓存直接访问数据库 的现象。你的描述“Redis一直没有数据,而一直命中数据库”抓住了核心表现,但需要理解其发生的根本原因在于缓存失效瞬间的高并发重建,并且数据库里通常是有这个数据的。解决的核心是控制并发重建缓存的请求数量(通常只允许一个请求去重建)。