【Redis】缓存击穿的解决办法
什么是缓存击穿?怎么解决?
缓存击穿的意思是,对于设置了过期时间的key,缓存在某个时间点过期的时候,恰好这个时间点对这个Key有大量的并发请求过来。这些请求发现缓存过期,一般都会从后端 DB 加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把 DB 压垮。解决方案有两种方式:第一,可以使用互斥锁:当缓存失效时,不立即去load db,先使用如 Redis 的 SETNX 去设置一个互斥锁。当操作成功返回时,再进行 load db的操作并回设缓存,否则重试get缓存的方法。第二种方案是设置当前key逻辑过期,大概思路如下:1) 在设置key的时候,设置一个过期时间字段一块存入缓存中,不给当前key设置过期时间;2) 当查询的时候,从redis取出数据后判断时间是否过期;3) 如果过期,则开通另外一个线程进行数据同步,当前线程正常返回数据,这个数据可能不是最新的。当然,两种方案各有利弊:如果选择数据的强一致性,建议使用分布式锁的方案,但性能上可能没那么高,且有可能产生死锁的问题。如果选择key的逻辑删除,则优先考虑高可用性,性能比较高,但数据同步这块做不到强一致。
想象一个网红奶茶店
- 缓存 (Redis) = 店门口放奶茶的取餐台。东西做好了就放上去,顾客拿了就走,速度极快。
- 数据库 (DB) = 店里的后厨。做一杯奶茶需要时间,效率比直接取餐慢得多。
- 设置了过期时间的key = 取餐台上的一杯**“限量版杨枝甘露”。这杯奶茶放了2小时**(过期时间)后,如果没人取,店员就会把它收走。
- 大量的并发请求 = 一大群学生(比如100人)在同一时刻冲到这个取餐台,都要买这杯“限量版杨枝甘露”。
问题:缓存击穿 (Cache Breakdown)
现在,最巧也是最倒霉的事情发生了:
就在这群学生冲过来的前一秒,取餐台上的那杯“杨枝甘露”因为到了2小时,刚好被店员收走了!
学生们一看取餐台是空的,会怎么做?他们会齐声对着后厨大喊:“老板!再做一杯杨枝甘露!”。
于是,悲剧发生了:后厨的老板瞬间接到了100个做同一杯奶茶的订单。他根本忙不过来,直接崩溃了(DB被压垮)。
这就是缓存击穿:一个热点数据在缓存中刚好过期的瞬间,海量的请求直接绕过了缓存,全部冲向了数据库。
解决方案一:互斥锁 (Mutex Lock) - “发号牌”
老板为了解决这个问题,想了个办法:
- 当取餐台上的奶茶被收走,第一个发现台子上没奶茶的学生A,不能直接喊。
- 他必须先去柜台领一个“独家制作权”的号牌(这就是使用Redis的
SETNX
命令设置一个互斥锁)。 - 如果领号牌成功,只有学生A一个人可以去后厨告诉老板:“做一杯杨枝甘露”。其他99个同学不能喊,只能在取餐台旁边等着。
- 等学生A从后厨拿到新做的奶茶,他会先放一杯到取餐台上,然后自己拿走一杯,最后把“独家制作权”的号牌还回去。
- 这时,其他在等待的同学看到取餐台上又有奶茶了,就直接从这里拿,再也不用去吵后厨了。
优点:绝对保护后厨,100个请求最终只有1个请求会到达数据库,数据库毫无压力。
缺点:体验稍差,那99个同学需要短暂地等待一下(阻塞)。如果领到号牌的人中途手机没电走了(拿到锁的客户端宕机),还没还号牌,就可能死锁(需要设置锁的过期时间来避免)。
解决方案二:逻辑过期 (Logical Expiration) - “卖预订单”
老板换了另一种更聪明的思路:
-
不设置2小时物理过期了。那杯“杨枝甘露”永远放在取餐台上。
-
但是,在杯子旁边贴一张小纸条,写上:“本杯奶茶的推荐饮用时间截止到今天下午4:00”(这就是逻辑过期时间,一个字段值,而不是Redis的真正过期时间)。
-
学生们来买奶茶时,会先看杯子上的纸条:
- 如果当前时间(比如3:50)比推荐饮用时间(4:00)早:没问题,直接把这杯奶茶拿走喝。(直接返回缓存数据)
- 如果当前时间(比如4:01)已经晚了:学生A会做两件事:
- a. 他依然会把这杯“过期的”奶茶先拿走喝(返回旧的缓存数据,可能不是最新的,但总比没有好)。
- b. 同时,他会对着后厨喊一声:“老板,再做一杯新的杨枝甘露,等下给后面的人!”(开通另外一个线程进行数据同步)。
-
后面的同学再来,就能直接拿到新做的、在推荐饮用时间内的奶茶了。
优点:体验极好,所有请求都能瞬间得到响应(要么拿到旧数据,要么很快拿到新数据),永远不会卡顿,高可用性极高。
缺点:数据可能短暂不一致。在4:01到新奶茶做好的这段时间里,同学们喝到的都是过期的奶茶(** stale data**)。做不到强一致性。
总结对比
方案 | 通俗比喻 | 优点 | 缺点 | 适用场景 |
---|---|---|---|---|
互斥锁 | 发号牌 | 保护数据库,数据强一致 | 部分请求需要等待,有死锁风险 | 对数据准确性要求极高的场景,如银行余额 |
逻辑过期 | 卖预订单 | 用户体验丝滑,性能极高 | 可能返回短期旧数据 | 对速度要求高、能容忍短暂不一致的场景,如商品详情页、新闻资讯 |
所以,选择哪种方案,就是在 “数据一致性” 和 “系统高可用” 之间做权衡。没有完美的方案,只有最适合你业务场景的方案。