18.Redis 缓存三兄弟 之 穿透
缓存穿透
- 为什么只要访问 Redis,最后却搞挂了数据库?
- 因为在代码逻辑里,程序员通常是这样写的(这叫 Cache-Aside Pattern 旁路缓存模式):
正常的流程是这样的:
1. 用户来了:我想查 id=100 的用户信息。
2. 先问 Redis(前台):你手里有 id=100 的资料吗?
3. Redis 回答:有!给你。(请求结束,不打扰数据库)
但如果 Redis 里没有(这就是关键点):
1. 用户来了:我想查 id=100 的用户信息。
2. 先问 Redis:你有吗?
3. Redis 回答:我没有啊(可能是过期了,或者本来就没存)。
4. 代码会自动执行下一步:既然前台没有,那我必须去后面的仓库(数据库)里翻箱倒柜找出来,不然怎么给用户看?
5. 查询数据库:找到了!
6. 回填 Redis:把这份资料复印一份放 Redis 前台,下次就不用去仓库找了。
2.回到“缓存穿透”的场景
现在的恶意攻击是这样的:
• 黑客:我要查 id = -1 (一个根本不存在的人)。
• Step 1 问 Redis:你有 id = -1 吗?
• Redis:没有。(非常快)
• Step 2 问 数据库:因为 Redis 说没有,代码逻辑被迫去查数据库。
• 数据库:我去硬盘里翻了一遍,也没找到。(非常慢,而且消耗大量 CPU)
• Step 3:这次查询结束,返回“空”。
重点来了: 因为 id = -1 根本不存在,所以数据库永远查不到,也永远没机会把数据“回填”给 Redis。
下一次黑客再问 id = -1,Redis 还是说“没有”,代码还是得去查数据库。
如果黑客每秒发 1 万个 id = -1 的请求:
• Redis 只是不断摇头说“没有”(它扛得住)。
• 你的代码就发起了 1 万次数据库查询。
• 数据库本来每秒只能处理 2000 次,直接被这 1 万次无效查询拖垮了。
3.解决办法
-
缓存空值
-
有漏洞的代码
public class UserService { // 模拟 Redis 客户端 RedisClient redis = new RedisClient(); // 模拟 数据库 DAO UserDao database = new UserDao(); /** * 这是一个有“缓存穿透”漏洞的方法 */ public User getUserByIdVulnerable(String userId) { // 定义 Redis 的 Key,比如 "user:100" String cacheKey = "user:" + userId; // === Step 1: 先问 Redis (前台) === String userJson = redis.get(cacheKey); if (userJson != null) { // 命中缓存!直接把字符串转成对象返回。 System.out.println("Redis里有,直接返回,不打扰数据库。"); return jsonToUser(userJson); } // === Step 2: Redis没有,只好去问数据库 (仓库) === // 【警报】如果 userId 是 -1,这里就会产生一次昂贵的数据库查询 System.out.println("Redis没货了,正在去仓库翻找..."); User user = database.findUserById(userId); // === Step 3: 判断数据库有没有查到 === if (user != null) { // 查到了!赶紧复印一份放到 Redis 前台,过期时间 1 小时 redis.setEx(cacheKey, 3600, userToJson(user)); } // 【关键漏洞点】:如果 user 是 null (没查到),这里什么都没做! // 这意味着下次再来查这个不存在的 ID,流程又会重新走一遍 Step 2。 return user; } } -
加上防弹衣后的“聪明”代码(解决穿透)
现在我们将修复漏洞。核心思路是:数据库查不到,也要在 Redis 里记一笔“查不到”!
为了区分“真的存了个空对象”和“没命中缓存”,我们通常会在 Redis 里存一个特殊的空标记字符串,比如 "@@NULL@@" 或者简单的空字符串 ""。public class UserService { // ... redis 和 database 变量省略 ... // 定义一个特殊的标记,表示“数据库里也没这个人” private static final String NULL_MARKER = "@@NULL@@"; /** * 这是一个修复了“缓存穿透”的方法 */ public User getUserByIdSafe(String userId) { String cacheKey = "user:" + userId; // === Step 1: 先问 Redis === String cacheValue = redis.get(cacheKey); // 1.1 如果 Redis 返回的不是 null (说明有点东西) if (cacheValue != null) { // 【关键判断】:看看这个东西是不是我们之前做的“空标记” if (NULL_MARKER.equals(cacheValue)) { // 哎哟,是空标记!说明之前已经有人去数据库确认过了,确实没有。 System.out.println("Redis防弹衣生效:拦截了一个恶意请求,直接返回空。"); // 直接返回 null,坚决不去查数据库! return null; } // 不是空标记,那就是真的用户数据了 System.out.println("Redis命中正常数据。"); return jsonToUser(cacheValue); } // === Step 2: Redis 什么都没有 (null),才去查数据库 === System.out.println("Redis完全没记录,正在去仓库翻找..."); User user = database.findUserById(userId); if (user != null) { // 3.1 数据库查到了,正常缓存,保存 1 小时 redis.setEx(cacheKey, 3600, userToJson(user)); } else { // === Step 3.2 【修复的核心】数据库也没查到!=== // 千万不能什么都不做!要在 Redis 里记下一笔空账。 System.out.println("数据库也没有!在 Redis 里记录一个空标记,防止下次再被查。"); // 【注意】:空值的过期时间一定要设置得短一点(比如 60秒) // 为什么?万一这 60秒内,真的有个用户注册了这个 ID 呢? // 存太久会导致新数据长时间查不出来。 redis.setEx(cacheKey, 60, NULL_MARKER); } return user; } }
-
-
布隆过滤器 (Bloom Filter)
布隆过滤器的底层依赖 二进制数组 + 多个独立哈希函数 实现
第一步:初始化(一张无限长的白纸) 它本质上是一个很长很长的二进制数组(Bit Array),里面只存 0 或 1。 最开始,全是 0。 [0, 0, 0, 0, 0, 0, 0, ...] 第二步:添加数据(按指印) 假设我们要记录 id=100 存在。 布隆过滤器不会直接把 "100" 存进去,而是用 K 个哈希函数(比如 3 个)对 "100" 进行运算,算出 3 个位置下标。 • Hash1(100) = 2 • Hash2(100) = 5 • Hash3(100) = 9 然后,把数组里下标为 2、5、9 的位置全都改成 1。 [0, 0, 1, 0, 0, 1, 0, 0, 0, 1, ...] (对应位置变黑了) 第三步:判断是否存在(查指印) 现在来了个请求,查 id=999。 我们用同样的 3 个哈希函数算一下: • Hash1(999) = 2 • Hash2(999) = 8 • Hash3(999) = 9 我们去数组里看: • 位置 2 是 1(对上了) • 位置 8 是 0(没对上!) • 位置 9 是 1(对上了) 只要有任何一个位置是 0,布隆过滤器就敢拍着胸脯保证:“这东西绝对不存在!” —— 直接拦截,不用去查数据库。 布隆过滤器的“死穴”:误判 (False Positive) 这也就是我为什么说它“视力有点模糊”。 假设我们又存了个 id=200,它把位置 8 也涂成了 1。 现在数组变成了:位置 2, 5, 8, 9 都是 1。 这时候,回头再查刚才那个 id=999 (哈希值是 2, 8, 9)。 • 位置 2 是 1 (id=100 干的) • 位置 8 是 1 (id=200 干的) • 位置 9 是 1 (id=100 干的) 坏了! 3 个位置全是 1。布隆过滤器会说:“id=999 可能存在。” 程序放行,去查 Redis/数据库,结果发现数据库里根本没有 id=999。 布隆过滤器还有一个致命缺点 它很难删除数据。 为什么? 比如你想删除 id=100。 你把位置 2, 5, 9 从 1 改回 0? 不行! 万一 id=888 刚好也映射到了位置 5 怎么办?你把位置 5 改成 0,id=888 就被你误删了。 解决方案: 1. 定期重建:每隔一段时间,生成一个新的布隆过滤器,把数据库里的数据全量重新导一遍。 2. 布谷鸟过滤器 (Cuckoo Filter):这是布隆过滤器的升级版,支持删除,但实现更复杂

浙公网安备 33010602011771号