Redis 缓存三大坑:穿透、雪崩与布隆过滤器(新手入门指南)
开篇为什么你必须懂这三个知识点想象你开了一家奶茶店。顾客点单时你会先看已经做好的成品区缓存有没有现成的奶茶有就直接端走没有再让后厨数据库现做。这个流程本来很顺畅直到三种意外发生有人故意点菜单上根本不存在的饮品成品区没有后厨也做不了但每次都得跑一趟后厨确认 —— 这叫穿透。成品区的奶茶同一时间全部过期倒掉了所有顾客一瞬间全涌向后厨 —— 这叫雪崩。而布隆过滤器就是你在店门口放的一个 快速筛查员一秒钟就能告诉顾客 我们店绝对没有这个饮品别进去问了。这三个概念是后端开发面试的必考题也是实际项目中真实会踩的坑。今天我们一次性讲清楚。一、Redis 穿透小偷绕开大门直接闯进家1.1 什么是缓存穿透正常的请求流程是这样的用户请求 → 先查 Redis 缓存 → 有数据就返回 → 没有就查数据库 → 查到后写入缓存 → 返回缓存穿透指的是用户请求的数据Redis 里没有数据库里也没有。每次请求都会查 Redis → 没有 → 查数据库 → 也没有 → 什么都没缓存 → 下次还是打到数据库通俗类比一个小偷发现你家大门Redis没锁因为根本没有这个数据的钥匙直接闯进你家里数据库翻找发现啥也没有。但他不死心带着一群人反复闯 —— 你家迟早被翻烂。1.2 为什么会发生穿透场景举例恶意攻击黑客故意用不存在的 ID如-1、99999999疯狂请求你的接口业务 Bug前端传了错误参数查询的数据压根不存在数据被删除数据库中的数据被删了但请求还在不断进来关键特征缓存和数据库双双落空。1.3 穿透的危害有多大对于新手来说记住一句话就够了穿透的本质 缓存形同虚设所有压力直接砸向数据库。正常情况1 万个请求 → 9900 个被缓存挡住 → 100 个到数据库轻松应对 穿透时 1 万个请求 → 全部穿过缓存 → 1 万个到数据库直接崩溃数据库一崩整个系统就瘫痪了 —— 用户看到的就是页面一直转圈、报错 500。1.4 怎么解决穿透方案一缓存空值最简单新手首选理解思路查了数据库发现没有也把 没有 这个结果缓存起来。public String getData(String key) { // 第一步查缓存 String value redis.get(key); if (value ! null) { // 缓存中有值包括空值标记直接返回 if (NULL_FLAG.equals(value)) { return null; // 之前查过确认不存在 } return value; } // 第二步缓存没有查数据库 String dbValue database.query(key); if (dbValue null) { // 数据库也没有 → 缓存一个空值标记设置较短的过期时间 redis.set(key, NULL_FLAG, 300); // 缓存 5 分钟 return null; } // 数据库有 → 正常缓存 redis.set(key, dbValue, 3600); // 缓存 1 小时 return dbValue; }优点简单直接几行代码搞定。缺点如果黑客用海量不同的假 key 攻击Redis 里会被塞满大量空值浪费内存。方案二布隆过滤器拦截更优雅后面第三章详细讲思路在缓存前面加一层 筛子快速判断这个数据是否可能存在。用户请求 → 布隆过滤器判断 → 不存在直接拒绝不查缓存也不查数据库 → 可能存在放行走正常缓存流程新手先记住布隆过滤器就是穿透问题的 终极方案。方案三参数校验最容易被忽略的基本功在业务入口处先校验请求参数的合法性public String getData(Long id) { // 基本校验ID 不可能小于等于 0 if (id null || id 0) { return 参数不合法; } // 正常流程... }很多穿透问题一个简单的参数校验就能挡住大半。三种方案对比方案实现难度适用场景局限性缓存空值⭐ 简单异常 key 种类少key 太多会浪费内存布隆过滤器⭐⭐ 中等大规模数据过滤有误判率不支持删除参数校验⭐ 简单所有场景的第一道防线只能挡住明显违规的请求 新手重点先理解 穿透 绕过缓存 这个核心逻辑不用深扣源码。能说出 缓存空值 布隆过滤器 两个方案面试就过关了。二、Redis 雪崩大坝同时决堤洪水冲垮后方2.1 什么是缓存雪崩缓存雪崩指的是大量缓存数据在同一时间失效或 Redis 服务直接宕机导致海量请求在同一时刻全部涌向数据库。正常缓存挡住 99% 的请求数据库很轻松 雪崩缓存突然集体罢工100% 的请求砸向数据库 → 数据库扛不住 → 系统崩溃通俗类比一座大坝平时挡住了上游的洪水大量请求。突然间大坝上所有的闸门同时打开缓存集中过期洪水瞬间冲向下游的村庄数据库—— 毁灭性的。2.2 雪崩的三大原因原因一缓存集中过期最常见// 错误示范所有缓存都设置了相同的过期时间 redis.set(product:1001, data1, 3600); // 1 小时后过期 redis.set(product:1002, data2, 3600); // 1 小时后过期 redis.set(product:1003, data3, 3600); // 1 小时后过期 // ... 1万个 key 都在同一秒过期一小时后1 万个缓存同时消失1 万个请求同时打到数据库。原因二Redis 服务宕机Redis 服务器直接挂了所有缓存瞬间全部不可用 —— 相当于大坝直接消失。原因三缓存击穿引发的连锁反应某个热点数据过期后大量请求同时穿透到数据库数据库被压垮连带其他正常请求也无法处理。2.3 雪崩的危害用一张图感受正常状态 雪崩状态 用户请求 ───→ Redis挡住99% 用户请求 ───→ Redis全部失效 ↓1%漏过去 ↓100%涌过去 数据库很轻松 数据库瞬间过载 ↓ ↓ 正常响应 超时/崩溃 ↓ 整个系统瘫痪 ↓ 用户看到页面502/5032.4 怎么解决雪崩方案一缓存过期时间加随机值最简单有效核心思路让缓存不要在同一时刻集中过期。// 正确做法在基础过期时间上加一个随机值 int baseExpire 3600; // 基础过期时间1小时 int randomExpire new Random().nextInt(600); // 随机 0~600 秒 redis.set(product:1001, data1, baseExpire randomExpire); // 3600~4200秒 redis.set(product:1002, data2, baseExpire randomExpire); // 3600~4200秒 redis.set(product:1003, data3, baseExpire randomExpire); // 3600~4200秒这样 1 万个 key 会在 3600~4200 秒之间分散过期不会同时冲击数据库。一句话记忆过期时间 固定时间 随机时间。方案二Redis 集群部署防止 Redis 宕机单台 Redis 挂了怎么办多台 Redis 互相备份。Redis 集群 ┌──────────────┐ │ Redis 主节点 │ │ (读写) │ └──────┬───────┘ ↙ ↘ ┌──────────┐ ┌──────────┐ │ Redis从节点│ │ Redis从节点│ │ (备份1) │ │ (备份2) │ └──────────┘ └──────────┘主节点挂了从节点自动顶上哨兵机制 / Cluster 模式数据有多份备份不怕单点故障新手只需知道生产环境不要用单机 Redis至少要有主从备份。方案三服务降级与熔断兜底方案当数据库压力过大时主动放弃一部分请求保护系统不被彻底压垮。用户请求商品详情 ↓ 缓存失效 数据库压力过大 ↓ 触发降级策略 - 返回默认数据商品信息加载中请稍后重试 - 或返回上次缓存的旧数据 - 而不是让所有请求都去冲击数据库类比暴风雪来了高速公路不是让所有车硬开而是临时封路降级等暴风雪过了再开放。方案四互斥锁重建缓存防止缓存击穿引发雪崩当某个热点 key 过期时不是所有线程都去查数据库而是只让一个线程去查其他线程等着。public String getDataWithLock(String key) { String value redis.get(key); if (value ! null) { return value; // 缓存命中直接返回 } // 缓存未命中尝试获取锁 String lockKey lock: key; if (redis.setnx(lockKey, 1, 30)) { try { // 拿到锁的线程去查数据库 value database.query(key); redis.set(key, value, 3600 new Random().nextInt(600)); return value; } finally { redis.del(lockKey); // 释放锁 } } else { // 没拿到锁的线程等一会儿再重试 Thread.sleep(100); return getDataWithLock(key); // 递归重试 } }四种方案总结方案解决的问题难度优先级过期时间加随机值缓存集中过期⭐必做Redis 集群Redis 宕机⭐⭐必做降级 / 熔断数据库过载兜底⭐⭐推荐互斥锁热点 key 重建时的并发冲击⭐⭐⭐推荐2.5 穿透 vs 雪崩一张表彻底分清对比维度穿透雪崩核心区别查的数据压根不存在数据存在但缓存集体失效请求数量可能是少量恶意请求一定是大量请求同时涌入缓存状态缓存和数据库都没有缓存曾经有但过期了或 Redis 挂了类比小偷闯空门大坝决堤核心方案布隆过滤器 缓存空值过期时间错开 集群 降级 一句话区分穿透是 查不存在的数据雪崩是 大量缓存同时消失。三、布隆过滤器门口的高效 筛子3.1 什么是布隆过滤器布隆过滤器Bloom Filter是一种空间效率极高的数据结构专门用来回答一个问题某个元素是否存在于集合中通俗类比你家门口放了一个智能门禁系统。有人来访时它能在0.001 秒内告诉你 这个人绝对不是业主 → 直接拒绝不开门 这个人可能是业主 → 放进来再做进一步验证注意那个 可能是—— 布隆过滤器可能会误判后面详细讲。3.2 三个核心特性新手必记表格特性说明类比高效判断速度极快时间复杂度 O (k)k 是哈希函数个数门禁刷卡只需 0.001 秒省空间只用几个比特位就能记录一个元素100 万用户只需约 1.2MB 内存有误判率说 不存在 一定对说 存在可能错门禁可能把某个陌生人误认为业主 最重要的一句话布隆过滤器说 没有 就是真没有说 有不一定真有。3.3 布隆过滤器的原理简单版不用背公式看懂下面这个过程就行第一步初始化一个位数组全是 0位数组假设 10 位[0, 0, 0, 0, 0, 0, 0, 0, 0, 0] 0 1 2 3 4 5 6 7 8 9第二步添加元素时用多个哈希函数计算位置把对应位置设为 1添加 apple 哈希函数1(apple) 2 哈希函数2(apple) 5 哈希函数3(apple) 8 位数组变为[0, 0, 1, 0, 0, 1, 0, 0, 1, 0] ↑ ↑ ↑ 位置2 位置5 位置8再添加 banana 哈希函数1(banana) 1 哈希函数2(banana) 5 ← 注意和apple的位置5重叠了 哈希函数3(banana) 9 位数组变为[0, 1, 1, 0, 0, 1, 0, 0, 1, 1] ↑ ↑ ↑ ↑ ↑第三步查询元素时看对应位置是否全为 1查询 apple位置 2、5、8 → 全是 1 → 可能存在 ✅ 查询 cherry 哈希函数1(cherry) 2 哈希函数2(cherry) 3 ← 位置3是0 哈希函数3(cherry) 8 → 不全为 1 → 绝对不存在 ❌为什么会误判查询 grape 哈希函数1(grape) 1 ← 被 banana 设为 1 哈希函数2(grape) 5 ← 被 apple 和 banana 设为 1 哈希函数3(grape) 8 ← 被 apple 设为 1 → 全是 1 → 可能存在 → 但实际上 grape 从没添加过这就是 **误判**误判的本质不同元素的哈希值恰好占据了相同的位置撞车 了。3.4 误判率怎么控制不需要记公式记住两个规律就行操作效果增大位数组长度误判率降低空间更大撞车 概率更低增加哈希函数个数误判率先降后升太多也不好位数组很快被填满实际项目中常见配置误判率控制在 1%~5%之间已经足够用了。3.5 布隆过滤器 Redis解决缓存穿透这是布隆过滤器最经典的应用场景整体流程用户请求查询 ID9999 的商品 ↓ 第一关布隆过滤器判断 ↓ ID9999 绝对不存在 → 直接返回空不查缓存、不查数据库 ✅ ↓ ID9999 可能存在 → 继续走正常流程 ↓ 第二关查 Redis 缓存 ↓ 缓存命中 → 返回数据 缓存未命中 → 查数据库 → 写入缓存 → 返回数据代码示例伪代码新手看思路// 系统启动时把数据库中所有存在的商品 ID 加入布隆过滤器 PostConstruct public void initBloomFilter() { // 创建布隆过滤器预计放入100万个元素误判率1% BloomFilterLong bloomFilter BloomFilter.create( Funnels.longFunnel(), 1_000_000, // 预计元素数量 0.01 // 误判率 1% ); // 把数据库所有商品ID加入布隆过滤器 ListLong allProductIds database.getAllProductIds(); for (Long id : allProductIds) { bloomFilter.add(id); } } // 查询商品 public Product getProduct(Long productId) { // 第一关布隆过滤器拦截 if (!bloomFilter.mightContain(productId)) { // 布隆过滤器说绝对不存在 → 直接返回 log.info(布隆过滤器拦截商品ID {} 不存在, productId); return null; } // 第二关查 Redis 缓存 Product product redis.get(product: productId); if (product ! null) { return product; } // 第三关查数据库 product database.getProductById(productId); if (product ! null) { redis.set(product: productId, product, 3600); } else { // 布隆过滤器误判了说存在但实际不存在缓存空值兜底 redis.set(product: productId, NULL, 300); } return product; }效果对比没有布隆过滤器 黑客发送 100 万个不存在的 ID → 100 万次数据库查询 → 数据库崩溃 有布隆过滤器 黑客发送 100 万个不存在的 ID → 99 万被布隆过滤器拦截 → 1 万误判走到数据库 → 数据库轻松应对 ✅3.6 布隆过滤器的局限性新手也要知道局限说明解决方式不支持删除一旦添加元素无法删除删了可能影响其他元素使用 计数布隆过滤器进阶内容有误判率说 存在 不一定真存在配合缓存空值做兜底需要预先加载数据启动时要把已有数据全部导入新增数据时同步更新布隆过滤器3.7 Redis 中使用布隆过滤器实际开发方式Redis 4.0 以后支持通过RedisBloom 模块直接使用布隆过滤器# 创建布隆过滤器错误率 0.01预计容量 100 万 BF.RESERVE product_filter 0.01 1000000 # 添加元素 BF.ADD product_filter 1001 BF.ADD product_filter 1002 BF.ADD product_filter 1003 # 判断是否存在 BF.EXISTS product_filter 1001 # 返回 1可能存在 BF.EXISTS product_filter 9999 # 返回 0绝对不存在 # 批量操作 BF.MADD product_filter 2001 2002 2003 BF.MEXISTS product_filter 2001 9999新手建议先用 Google Guava 库的BloomFilter单机版上手简单熟悉后再用 Redis 的 RedisBloom分布式版。总结三大知识点串联回顾完整的知识链路缓存系统常见问题 ┌──────┴──────┐ ↓ ↓ 缓存穿透 缓存雪崩 查不存在的数据 大量缓存同时失效 ↓ ↓ 解决方案 解决方案 ┌────┴────┐ ┌────┴─────────┐ ↓ ↓ ↓ ↓ ↓ 缓存空值 布隆过滤器 过期时间 Redis集群 降级/熔断 ↑ 错开 │ 布隆过滤器也能辅助缓解雪崩 拦截无效请求减轻系统整体压力核心要点速记卡知识点一句话定义核心方案穿透请求的数据根本不存在缓存和数据库都查不到布隆过滤器 缓存空值雪崩大量缓存同时失效请求瞬间涌向数据库过期时间随机化 集群 降级布隆过滤器用极小空间快速判断数据是否存在的筛子说 没有 一定对说 有 可能错新手入门学习建议三步走策略阶段目标建议第一步懂场景知道穿透、雪崩分别是什么问题重点理解本文的类比和流程图第二步记方案能说出每种问题的 2~3 个解决思路面试时按照 问题→原因→方案 的逻辑回答第三步动手练用代码实现布隆过滤器和缓存策略先跑通 Demo再深入理解底层原理不要急于深扣底层源码。这三个概念的核心是架构思维和方案选型而不是让你手写一个布隆过滤器。先把 什么场景用什么方案 搞清楚你就已经超过 80% 的初学者了。
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2474391.html
如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!