缓存穿透解决方案详解:从原理到布隆过滤器实战

缓存穿透解决方案详解:从原理到布隆过滤器实战 ​一、什么是缓存穿透在日常开发中我们通常会使用 Redis 作为缓存层来保护数据库提升系统的响应速度。但有一种特殊的场景会让缓存完全失效——缓存穿透。1.1 缓存穿透的定义缓存穿透是指查询一个数据库中一定不存在的数据由于缓存中也没有所以每次请求都会穿透缓存层直接打到数据库上。正常的缓存工作流程是请求先查 Redis 缓存缓存命中则直接返回缓存未命中则查数据库数据库查到后写入缓存并返回但如果查询的是一个根本不存在的数据数据库返回空结果这个空结果通常不会被写入缓存导致下一次同样的请求还是会打到数据库。1.2 缓存穿透的危害如果有人恶意构造大量不存在的 key 来请求你的系统会造成什么后果数据库压力剧增大量请求直接打到数据库可能导致数据库连接被占满缓存形同虚设缓存层完全失去保护作用系统宕机风险严重时可能拖垮数据库造成整个服务不可用特别是在秒杀、促销等大流量场景下缓存穿透带来的危害会被放大数倍。二、常见解决方案2.1 方案一缓存空值最简单直接的方案当数据库查询为空时仍然把这个空结果缓存起来设置一个较短的过期时间。优点实现简单改造成本低缺点会占用大量内存空间存了很多没用的空值如果有人恶意用随机 key 攻击还是会产生大量缓存数据不一致窗口期如果之后这条数据真的被插入了缓存的空值还没过期会导致用户一段时间内查不到2.2 方案二接口层参数校验在入口层就做参数合法性校验把明显不合理的请求直接挡掉。比如用户 ID 都是正整数那传负数的请求直接返回错误商品 ID 有固定的格式规则不符合格式的直接拒绝。优点简单高效能挡住大部分无脑攻击缺点只能防住格式明显不对的请求对于格式合法但数据不存在的请求无能为力2.3 方案三布隆过滤器Bloom Filter这是目前业界最常用、也是最高效的缓存穿透解决方案。在查询缓存之前先通过布隆过滤器判断这个 key 是否可能存在。如果布隆过滤器说不存在 → 一定不存在直接返回不用查缓存和数据库如果布隆过滤器说存在 → 才继续查缓存 → 查数据库下面我们来详细了解布隆过滤器的原理。三、布隆过滤器Bloom Filter详解3.1 核心原理布隆过滤器是由Burton Howard Bloom在 1970 年提出的一种空间效率极高的概率型数据结构。它的核心组成很简单一个很长的二进制位数组bit array初始状态所有位都是 0多个相互独立的哈希函数每个哈希函数都能把元素映射到位数组的某一个位置添加元素的过程将元素分别传入 k 个哈希函数得到 k 个数组下标位置将这 k 个位置的 bit 都设置为 1查询元素的过程同样将元素传入 k 个哈希函数得到 k 个位置检查这 k 个位置的 bit 是否全部为 1只要有一个位置是 0 → 元素一定不存在如果全部都是 1 → 元素可能存在3.2 为什么会有误判率这是布隆过滤器最核心的特点——它可能会冤枉好人但绝不会放过坏人。不存在的元素它绝不会判断为存在不会漏判存在的元素它可能判断为不存在不正好相反不存在的元素可能被误判为存在有误判举个例子元素 A 添加时哈希到了位置 1、3、5元素 B 添加时哈希到了位置 2、4、6现在查一个不存在的元素 C它刚好哈希到了位置 1、4、5这三个位置都是 1 → 布隆过滤器会说可能存在但实际上 C 从来没被添加过这就是假阳性False Positive——把不存在的误判为存在。3.3 优缺点分析优点空间效率极高只存 bit 位100 万数据只需几 MB 内存查询速度极快O(k) 时间复杂度k 是哈希函数个数通常很小保密性好不存储原始数据适合敏感场景缺点存在误判率只能说可能存在不能 100% 确定存在不支持删除标准布隆过滤器不能删除元素删除会影响其他元素初始参数需要预估位数组大小和哈希函数个数需要提前根据数据量和可接受误判率计算四、代码实战4.1 Guava 实现本地单机版Google Guava 库提供了开箱即用的布隆过滤器实现import com.google.common.hash.BloomFilter;import com.google.common.hash.Funnels;public class BloomFilterDemo {public static void main(String[] args) {// 预计插入100万条数据期望误判率0.01BloomFilter bloomFilter BloomFilter.create(Funnels.integerFunnel(),1000000,0.01);// 添加100万条数据 for (int i 0; i 1000000; i) { bloomFilter.put(i); } // 判断是否存在 System.out.println(bloomFilter.mightContain(99999)); // true System.out.println(bloomFilter.mightContain(1000001)); // 大概率false小概率误判为true }}### 4.2 Redis 实现分布式版 生产环境通常用 Redis 的 BitMap 来实现分布式布隆过滤器 java public class RedisBloomFilter { private RedisTemplateString, Object redisTemplate; private int bitSize; // 位数组大小 private int hashNum; // 哈希函数个数 public boolean mightContain(String key, String value) { long[] offsets getOffsets(value); for (long offset : offsets) { Boolean bit redisTemplate.opsForValue().getBit(key, offset); if (bit null || !bit) { return false; // 有一个位是0一定不存在 } } return true; // 全部为1可能存在 } public void add(String key, String value) { long[] offsets getOffsets(value); for (long offset : offsets) { redisTemplate.opsForValue().setBit(key, offset, true); } } }Redis 4.0 还可以直接安装 RedisBloom 模块提供原生的布隆过滤器命令性能更好。五、总结缓存穿透是高并发系统中必须解决的问题三种方案各有适用场景方案优点缺点适用场景缓存空值简单易实现占内存、可能数据不一致数据量小、空查询少参数校验性能最好只能防格式错误接口入口层第一道防线布隆过滤器省内存、高效有误判、不支持删除大数据量、防穿透为主最佳实践通常是多层防御配合使用——接口层先做参数校验然后用布隆过滤器快速拦截不存在的 key最后缓存层再做空值兜底这样层层递进才能最大程度保护数据库不被击穿。如果觉得文章对你有帮助欢迎点赞收藏~ 有问题可以在评论区交流​