本文目录导读:

- 第一步:先理解什么是缓存穿透
- 第二步:布隆过滤器的核心原理
- 第三步:如何用布隆过滤器防止缓存穿透
- 第四步:代码示例(基于Google Guava + Redis + Spring Boot)
- 第五步:布隆过滤器的优缺点
- 总结:何时使用布隆过滤器?
这是一个非常经典的分布式系统设计问题。布隆过滤器是防止缓存穿透的最有效手段之一。
下面我会用通俗的语言,结合一个极简的代码示例,详细解释其原理和实现步骤。
第一步:先理解什么是缓存穿透
正常流程:
- 客户端请求数据。
- 先去 Redis(缓存)里查。
- 缓存有:直接返回,很快。
- 缓存没有:去数据库查,把结果写回缓存,再返回。
缓存穿透: 是指缓存和数据库中都没有的数据被持续请求。
- 攻击者伪造了一个 ID = -1 的用户,或者一个不存在的 ID。
- 每次请求都直接打到了数据库。
- 如果并发极高(如秒杀、爬虫攻击),数据库会因为扛不住查询压力而宕机。
- 本质:缓存失去了“保护数据库”的作用。
第二步:布隆过滤器的核心原理
布隆过滤器本质上是一个很长的二进制位数组 和 多个哈希函数。
- 特性:
- 判断“肯定不存在”:非常准确,如果布隆过滤器说“不存在”,那这个数据绝对不存在。
- 判断“可能存在”:有一定误判率,如果布隆过滤器说“存在”,这个数据可能真的存在,也可能不存在(哈希冲突导致)。
- 工作流程:
- 添加数据:将数据(如用户ID)通过多个哈希函数计算出多个位置,把这些位置的位设置成 1。
- 查询数据:对查询的数据同样进行哈希计算,检查对应位是否全部为 1。
- 只要有任意一位是 0:数据一定不存在。
- 如果全部是 1:数据可能存在。
第三步:如何用布隆过滤器防止缓存穿透
这是一个标准的“双保险”流程:
架构图(文字版):
客户端 -> 布隆过滤器 -> 缓存 (Redis) -> 数据库 (MySQL)
(挡在第一道) (第二道) (最后防线)
具体步骤:
初始化(项目启动时) 预先将数据库中所有存在的业务ID(如用户ID、商品ID),全部加载到布隆过滤器中。
// 伪代码 - 初始化布隆过滤器
BloomFilter<Long> bloomFilter = BloomFilter.create(Funnels.longFunnel(), expectedInsertions, falsePositiveRate);
// 从数据库读取所有存在的ID,并存入布隆过滤器
List<Long> allUserIds = userMapper.getAllIds();
for (Long id : allUserIds) {
bloomFilter.put(id);
}
请求处理流程
-
第一步:拦截(核心步骤)
- 用户请求一个 ID。
- 先问布隆过滤器:
bloomFilter.mightContain(ID) - 如果是
false:布隆过滤器说“这个 ID 绝对不在数据库里”。- 直接拒绝:返回空结果或错误信息。不查缓存,不查数据库。
- 如果是
true:布隆过滤器说“这个 ID 可能存在”。
-
第二步:查询缓存
- 如果布隆过滤器放行,再去查 Redis。
- Redis 有,直接返回。
- Redis 没有,去查数据库。
-
第三步:查数据库并回写
- 数据库查到了:写回缓存,返回数据。
- 数据库没查到:这是一个罕见的“幽灵”情况(布隆过滤器误判导致),此时为了防止下一次同样请求再次穿透,可以在 Redis 中缓存一个空值(设置较短的过期时间)。
流程图:
请求来了(ID = -1)
|
v
布隆过滤器: mightContain(-1) ?
|
+-- false (不存在) --------> 直接返回 "无效ID" (保护了数据库)
|
+-- true (可能存在)
|
v
查缓存 (Redis)
|
+-- 有数据 -----> 返回 (正常)
|
+-- 无数据
|
v
查数据库 (MySQL)
|
+-- 有数据 -----> 回写Redis并返回
|
+-- 无数据 -----> 缓存一个空值到Redis (防止下一次穿透)
(设置较短的过期时间,如5分钟)
第四步:代码示例(基于Google Guava + Redis + Spring Boot)
@Service
public class UserService {
@Autowired
private StringRedisTemplate redisTemplate;
@Autowired
private UserMapper userMapper;
// 1. 项目启动时初始化布隆过滤器(实际应该用@PostConstruct或者在启动类中执行)
private BloomFilter<Long> bloomFilter;
@PostConstruct
public void init() {
// 预计插入100万条数据,误判率设为1%
this.bloomFilter = BloomFilter.create(Funnels.longFunnel(), 1000000, 0.01);
List<Long> allIds = userMapper.getAllUserIds();
allIds.forEach(id -> bloomFilter.put(id));
log.info("布隆过滤器初始化完成,共加载{}个ID", allIds.size());
}
public User getUserById(Long id) {
// 步骤1:布隆过滤器前置拦截
if (!bloomFilter.mightContain(id)) {
// 一定不存在,直接返回null(防止穿透)
return null;
}
// 步骤2:查缓存
String cacheKey = "user:" + id;
String json = redisTemplate.opsForValue().get(cacheKey);
if (StringUtils.hasText(json)) {
return JSON.parseObject(json, User.class);
}
// 步骤3:数据库查询
User user = userMapper.selectById(id);
// 步骤4:回写缓存(包括空值缓存)
if (user != null) {
redisTemplate.opsForValue().set(cacheKey, JSON.toJSONString(user), Duration.ofHours(1)); // 正常数据1小时过期
return user;
} else {
// 数据库里也没有,但布隆过滤器说可能存在(误判)
// 为了防穿透,缓存一个空值,设置短过期时间
redisTemplate.opsForValue().set(cacheKey, "", Duration.ofMinutes(5));
return null;
}
}
}
第五步:布隆过滤器的优缺点
优点:
- 内存占用极小:1亿条数据,误判率1%时,大约只占 114 MB 内存。
- 查询速度极快:O(k) 复杂度,k 是哈希函数个数(通常几个到十几个)。
- 完全防止了无效请求到达数据库:这是最核心的价值。
缺点:
- 有误判率:可能把不存在的 ID 判断为存在(导致一次无效的缓存+数据库查询,但不会穿透到数据库导致崩溃)。
- 解决办法:接受,因为误判率可控(如0.01%)。
- 无法删除元素:标准的布隆过滤器不支持删除,如果数据库删掉了一个ID,布隆过滤器里对应的位无法清零(因为可能被多个ID复用)。
- 解决办法:
- 定时重建:每天凌晨低峰期,重新加载数据库全量ID到新的布隆过滤器,然后切换。
- 使用 Counting Bloom Filter(布谷鸟过滤器):支持删除,但更耗内存。
- 只做第一步拦截:即便有误判,后面的缓存空值机制也能兜底。
- 解决办法:
- 需要预热:项目重启后,需要先加载所有ID,否则新布隆过滤器是空的,正常请求也会被误拦。
何时使用布隆过滤器?
| 场景 | 推荐度 | 说明 |
|---|---|---|
| 高并发、数据在DB中持续存在(如有ID的字典表) | 强烈推荐 | 提前一次加载完所有ID,能完美防穿透。 |
| 动态新增、删除频繁的数据 | 谨慎使用 | 需要定时重建,或者配合布谷鸟过滤器。 |
| 数据量极小 | 用不着 | 直接缓存空值或加锁即可。 |
| 误判导致业务无法接受 | 不建议 | 比如实时对账,错判会导致一笔交易被拦截。 |
一句话总结: 布隆过滤器像一个“黑名单”守卫,说“不在”你就一定不在,它用极小的内存,把绝大部分无效请求挡在数据库之外,是防止缓存穿透的终极防线。
版权声明:NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
足球直播
足球直播
足球直播
篮球直播
篮球直播
篮球直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
亚冠直播
亚冠直播
亚冠直播
欧冠直播
欧冠直播
欧冠直播
英超直播
英超直播
英超直播
西甲直播
西甲直播
西甲直播
中超直播
中超直播
中超直播
足球直播
足球直播
足球直播
篮球直播
篮球直播
篮球直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
亚冠直播
亚冠直播
亚冠直播
欧冠直播
欧冠直播
欧冠直播
英超直播
英超直播
英超直播
西甲直播
西甲直播
西甲直播
中超直播
中超直播
中超直播
足球直播
足球直播
足球直播
篮球直播
篮球直播
篮球直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
亚冠直播
亚冠直播
亚冠直播
欧冠直播
欧冠直播
欧冠直播
英超直播
英超直播
英超直播
西甲直播
西甲直播
西甲直播
中超直播
中超直播
中超直播
足球直播
足球直播
足球直播
篮球直播
篮球直播
篮球直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
亚冠直播
亚冠直播
亚冠直播
欧冠直播
欧冠直播
欧冠直播
英超直播
英超直播
英超直播
西甲直播
西甲直播
西甲直播
中超直播
中超直播
中超直播
曼联直播
曼联直播
曼联直播
日职联直播
日职联直播
日职联直播
澳超直播
澳超直播
澳超直播
阿森纳直播
阿森纳直播
阿森纳直播
足球直播
足球直播
足球直播
NBA直播
NBA直播
NBA直播
曼联直播
曼联直播
曼联直播
日职联直播
日职联直播
日职联直播
澳超直播
澳超直播
澳超直播
阿森纳直播
阿森纳直播
阿森纳直播
足球直播
足球直播
足球直播
NBA直播
NBA直播
NBA直播
曼联直播
曼联直播
曼联直播
日职联直播
日职联直播
日职联直播
澳超直播
澳超直播
澳超直播
阿森纳直播
阿森纳直播
阿森纳直播
足球直播
足球直播
足球直播
NBA直播
NBA直播
NBA直播
曼联直播
曼联直播
曼联直播
日职联直播
日职联直播
日职联直播
澳超直播
澳超直播
澳超直播
阿森纳直播
阿森纳直播
阿森纳直播
足球直播
足球直播
足球直播
NBA直播
NBA直播
NBA直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
足球直播
足球直播
足球直播
足球直播
足球直播
英超直播
英超直播
英超直播
英超直播
英超直播
篮球直播
篮球直播
篮球直播
篮球直播
篮球直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
中超直播
中超直播
中超直播
中超直播
中超直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
足球直播
足球直播
足球直播
足球直播
足球直播
英超直播
英超直播
英超直播
英超直播
英超直播
篮球直播
篮球直播
篮球直播
篮球直播
篮球直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
中超直播
中超直播
中超直播
中超直播
中超直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播