Redis里头怎么搞烧饼模式,实际操作和那些坑分享
- 问答
- 2026-01-14 13:02:03
- 2
查不存在的数据,每次都怼到数据库
这指的是用户疯狂请求一个数据库中根本不存在的数据,请求一个不存在的商品ID,因为缓存里没有,所以每次请求都会穿过缓存去查数据库,数据库也查不到,自然也不会回写到缓存,这样一来,这个无效请求每次都能把数据库打得够呛。
实际操作:
- 缓存空对象(Null Object): 这是最直接的办法,就算数据库查不到,我也在缓存里给这个不存在的Key设置一个空值(比如
NULL或一个特殊的字符串),并给它一个较短的过期时间(比如1-5分钟),这样,后续的相同请求在缓存层就直接被拦住了,不会再到数据库。- 命令示例:
SETEX product_id:9999 300 "NULL"// 设置键product_id:9999,值为"NULL",过期时间300秒。
- 命令示例:
- 布隆过滤器(Bloom Filter): 这是个更高效的工具,你可以把它想象成一个放在缓存前面的、很大的二进制向量(bitmap)和一系列哈希函数,它的特点是:如果它说某个值不存在,那这个值一定不存在;如果它说存在,那么大概率是存在的(有极小的误判率)。
- 操作流程: 系统启动时,把所有数据库里存在的合法Key(比如所有有效的商品ID)预先加载到布隆过滤器中,当有请求来时,先让布隆过滤器判断一下这个Key是否存在。
- 如果布隆过滤器说“不存在”,那直接返回空结果给客户端,根本不用查缓存和数据库。
- 如果布隆过滤器说“存在”,才允许去查缓存/数据库,这样能拦截掉绝大部分恶意攻击或随机请求。
- 操作流程: 系统启动时,把所有数据库里存在的合法Key(比如所有有效的商品ID)预先加载到布隆过滤器中,当有请求来时,先让布隆过滤器判断一下这个Key是否存在。
坑点分享:
- 缓存空对象的坑:
- 内存浪费: 如果攻击者构造大量不同的、随机的无效Key,你的缓存里会塞满这些空值,占用宝贵的内存空间,所以过期时间一定要设短,并且要监控缓存的内存使用情况。
- 数据不一致: 假设你缓存了
product_id:9999为空,但后来后台真的新增了这个商品,在缓存过期之前,用户依然会看到“无此商品”,这就需要你在数据新增时,有逻辑去主动删除或更新这个缓存空值。
- 布隆过滤器的坑:
- 误判率: 这是天生的缺陷,它无法做到100%准确,对于要求绝对精确的场景(如金融扣款)要慎用,但在商品查询这类可以容忍极低概率误判的场景下,非常合适。
- 数据更新困难: 传统的布隆过滤器不支持删除操作,如果数据库里的有效数据删除了,你很难从布隆过滤器中把这个Key标记为无效,有变种的布隆过滤器(如Counting Bloom Filter)支持删除,但更复杂,通常的做法是定期重建整个布隆过滤器。
缓存击穿:热点Key过期,瞬间并发全打向数据库
这指的是某一个热点Key(比如某个秒杀商品)在缓存中过期失效的瞬间,同时有大量的请求过来,这些请求发现缓存没了,于是全部同时去查数据库,导致数据库瞬间压力激增,就像被击穿了一样。

实际操作:
- 永不过期 + 逻辑过期: 不给热点Key设置物理过期时间(TTL),让它永远存在,但同时,在存储的Value里,嵌入一个逻辑过期时间字段(比如一个时间戳),当业务线程读取缓存时,发现逻辑时间已过期,就主动去更新这个缓存。
- 互斥锁(Mutex Lock): 这是最常用的方法,当第一个发现缓存失效的请求到来时,它不会立即去查数据库,而是先尝试在Redis中设置一个互斥锁(比如
SETNX product_lock:9999 1 EX 10,表示设置一个10秒过期的锁)。- 如果抢锁成功: 这个线程就去查数据库,并回写缓存,最后删除锁。
- 如果抢锁失败: 说明已经有别的线程在更新缓存了,这个线程就等待一小段时间(比如睡眠50毫秒),然后重新从缓存获取数据,这样,大量并发请求中,只有一个线程会去访问数据库,其它线程都在等待缓存被重建。
坑点分享:
- 永不过期的坑: 如果更新缓存的逻辑出了问题,或者后台更新了数据但没触发缓存更新,那缓存里的数据就永远都是旧数据了。需要有一个额外的保障机制,比如用消息队列或者定时任务来定期刷新热点数据。
- 互斥锁的坑:
- 死锁风险: 如果那个抢到锁的线程,在更新缓存的过程中挂了,没能及时释放锁,那么其他线程就会一直等待,导致系统不可用。设置锁的时候一定要加过期时间,这是保命符。
- 锁过期时间设置: 过期时间设得太短,可能数据库查询还没完成锁就释放了,又会导致其他线程抢锁去查数据库,设得太长,万一持有锁的线程挂了,系统恢复时间又太长,需要根据业务查询的耗时来合理设置,并留有余量。
- 性能损耗: 虽然避免了数据库被打垮,但大量线程在睡眠和重试,对系统吞吐量还是有影响的,这属于一种牺牲部分体验保整体可用的权衡。
缓存雪崩:大量Key同时过期,数据库全面崩溃

缓存击穿是单个热点Key失效,而雪崩是大量的Key在同一时间点(或时间段)集体失效,系统初始化时批量加载数据到缓存,都设置了相同的过期时间(如2小时),那么2小时后这些缓存会同时失效,所有请求都会涌向数据库。
实际操作:
- 错开过期时间: 这是治本的方法,在给缓存设置过期时间时,使用一个基础时间加上一个随机的偏移量,原本想设1小时过期,现在可以设成
3600 + random(0, 300)秒,即1小时到1小时5分钟之间随机过期,这样就能避免大量缓存同时失效。 - 构建缓存集群: 采用Redis哨兵(Sentinel)或集群(Cluster)模式,实现高可用,即使某个Redis节点宕机,也能快速切换,避免整个缓存服务不可用。
- 服务降级和熔断: 在应用层,当发现数据库压力过大或响应过慢时,启动降级策略,对于非核心业务数据,直接返回预定义的默认值或友好提示,不再访问数据库,使用Hystrix、Sentinel等组件可以实现熔断,当失败率达到阈值,自动切断对数据库的访问。
坑点分享:
- 错开过期时间的坑: 这个策略很简单,但容易被忽略,很多新手在代码里写死一个过期时间,或者批量操作时忘记加随机因子,就埋下了雪崩的隐患。这应该成为编码时的一种习惯。
- 集群的坑: 集群解决了单点故障,但如果失效的Key太多,即使缓存服务是好的,数据库也扛不住,所以集群必须和“错开过期时间”结合使用。
- 降级/熔断的坑: 降级策略需要提前规划好,哪些功能可以降级,降级后返回什么,如果设计不好,可能会严重影响用户体验,熔断器的恢复策略(如半开状态)也需要仔细调参。
对付“烧饼模式”,没有银弹,需要根据具体场景组合拳:
- 防穿透: 布隆过滤器 + 缓存空对象(短过期时间)。
- 防击穿: 互斥锁(务必加超时) + 永不过期/逻辑过期。
- 防雪崩: 过期时间随机化 + 缓存集群高可用 + 服务降级熔断。
核心思想就是:尽最大努力保护数据库,哪怕牺牲一点缓存的一致性或者增加一点复杂度,也要保证系统不被冲垮。
本文由畅苗于2026-01-14发表在笙亿网络策划,如有疑问,请联系我们。
本文链接:http://waw.haoid.cn/wenda/80566.html
