在系统规模还很小时,许多问题都像不存在一样。但当用户量突破数百万、上千万后,你会发现某些你从未关注过的“小细节”,会突然变成拖垮系统的幕后黑手。
我就踩过这样一个坑——一个看似微不足道的用户名重复校验,差点把数据库打到 CPU 100%。
这篇文章将带你从“数据库快烧掉了”,一路走到“靠 Redis Bloom Filter 稳住场面”的完整过程,并给出 Spring Boot + Redis 最小可运行 Demo。
数据库第一次给我“吼”:你别查了行不行?
故事的开始超级普通。
注册接口需要校验用户名是否已经存在:
SELECT COUNT(*) FROM users WHERE username = 'praveen';
没任何问题。 没任何难度。 也没任何风险——直到用户数量突破 10,000,000。
然后,我的世界开始变得不太美好:
- CPU 一直拉满
- 请求响应变慢
- 数据库连接池被瞬间占满
- 监控图上红线一条接一条
让我震惊的是,整个注册流程最耗时的居然不是密码校验、不是业务逻辑,而是这个看似 harmless 的“是否存在”查询。
那一刻我终于清楚了一件事:
数据库天生不适合做 “我以前看过这个吗?” 的判断。
哪怕你给 username 建了索引,也一样会触发:
- I/O
- 网络往返
- CPU 计算
- 连接池争抢
规模一大,这种查询就是慢性毒药。
我需要一个 能在访问数据库之前做预筛选 的机制。
这时,我遇见了 Bloom Filter:数据库门口的保镖
如果你把数据库比喻成一家酒吧,那 Bloom Filter 就像门口的保镖。
它不需要认识所有人,但它很快就能告知你:
- “这人肯定没来过(不存在)” —— 100% 准确
- “嗯?可能来过,你进去问下(可能存在)” —— 有必定误报率
这样,大量根本不存在的用户名 就会被挡在数据库门外。
在实际测试中,它帮我过滤掉 90% ~ 99% 的数据库查询。
数据库:终于可以喘口气了。
为什么我选择 Redis 来承载 Bloom Filter?
Bloom Filter 本质上是一个位数组 + 多个哈希函数。 Redis 天然适合作为它的容器,由于:
- Redis 全内存存储,速度是纳秒级别
- 支持位图操作(setbit / getbit)
- 可以集群共享
- 可以持久化
几乎是为 Bloom Filter 量身定制的。
特别适合这些场景:
- 用户名 / 邮箱是否重复
- 交易请求是否重复
- 抓取系统去重
- 爬虫 URL 判重
- 防垃圾消息
手把手带你搭一个最小可用 Demo(Spring Boot + Redis)
接下来,打造一个本地可跑的版本。
Maven 依赖
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>io.lettuce.core</groupId>
<artifactId>lettuce-core</artifactId>
</dependency>
</dependencies>
本地安装 Redis
Mac/Linux
brew install redis
redis-server
Windows
choco install redis
redis-server
配置文件 application.properties
spring.application.name=bloom-filter-demo
spring.redis.host=localhost
spring.redis.port=6379
核心逻辑:Bloom Filter 实现(已优化代码)
路径:
src/main/java/com/icoderoad/bloomfilter/service/BloomFilterService.java
package com.icoderoad.bloomfilter.service;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import java.nio.charset.StandardCharsets;
@Service
public class BloomFilterService {
// 位数组长度
private static final int SIZE = 1_000_000;
// 多种哈希函数种子
private static final int[] SEEDS = {7, 11, 13, 31, 37, 61};
@Autowired
private RedisTemplate<String, Object> redisTemplate;
private static final String BLOOM_KEY = "username_bloom_filter";
/** 添加元素 */
public void add(String username) {
for (int seed : SEEDS) {
int hash = hash(username, seed);
redisTemplate.opsForValue().setBit(BLOOM_KEY, hash, true);
}
}
/** 判断可能存在 */
public boolean mightContain(String username) {
for (int seed : SEEDS) {
int hash = hash(username, seed);
Boolean bit = redisTemplate.opsForValue().getBit(BLOOM_KEY, hash);
if (bit == null || !bit) {
return false; // 只要有一个 bit 为 false,则必定不存在
}
}
return true; // 所有 bit 都是 true,则可能存在
}
/** 哈希函数 */
private int hash(String value, int seed) {
int result = 0;
byte[] data = value.getBytes(StandardCharsets.UTF_8);
for (byte b : data) {
result = result * seed + b;
}
return Math.abs(result % SIZE);
}
}
REST 接口
路径:
src/main/java/com/icoderoad/bloomfilter/controller/UsernameController.java
package com.icoderoad.bloomfilter.controller;
import com.icoderoad.bloomfilter.service.BloomFilterService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/usernames")
public class UsernameController {
@Autowired
private BloomFilterService bloomFilterService;
@PostMapping("/add/{username}")
public String addUsername(@PathVariable String username) {
bloomFilterService.add(username);
return "Username added: " + username;
}
@GetMapping("/check/{username}")
public String checkUsername(@PathVariable String username) {
boolean exists = bloomFilterService.mightContain(username);
return exists ? "Username might exist!" : "Username definitely does not exist!";
}
}
本地测试
添加用户名
curl -X POST http://localhost:8080/usernames/add/praveencodes
检查已添加用户
curl http://localhost:8080/usernames/check/praveencodes
检查不存在的用户
curl http://localhost:8080/usernames/check/randomuser
内部到底发生了什么?
过程超级简单但高效:
- 使用多个哈希函数对 username 计算多个 hash
- Redis 位数组对应位置全部置 1
- 查询时只检查这些位是否都是 1
- 若都为 1 → 可能存在
- 若任意为 0 → 肯定不存在
- 如果判断“不存在”,则不会访问数据库
结果就是:
数据库请求从 千万级下降到十万级QPS 提升数倍延迟从 3ms → 0.05ms
真实压测结果(1000 万条数据)
|
指标 |
传统数据库 |
Bloom Filter 预筛选 |
|
单次查询耗时 |
3–5 ms |
0.05 ms |
|
总 DB 查询数 |
10,000,000 |
~100,000 |
|
内存占用 |
高 |
仅几 MB |
Bloom Filter 就像一个超级前置缓存,把无意义的数据请求挡在数据库之外。
Bloom Filter 的局限性
Bloom Filter 看似完美,但必须认识这几点:
有误报(False Positive)
- 会误判“存在”
- 不会误判“不存在” (即:不会漏掉实际存在的数据)
无法删除
- 位数组只有 0/1
- 删除会影响其他元素 (除非使用 Counting Bloom Filter)
参数需要调优
- 位数组大小
- 哈希函数数量
- 容量规划 都会影响误报率与性能
结语:唯一比 Bloom Filter 更可怕的,是忽视它
我们常常会把注意力放在复杂的优化上,列如多线程、分库分表、连接池调优。 但真正让系统崩溃的,有时就是这样一个不起眼的存在校验。
Bloom Filter 的出现不是为了替代数据库,而是为了 保护数据库。
它能把大量重复、无意义、可预判的请求挡掉, 让数据库把资源留给真正有价值的请求。
如果你的系统有以下情况:
- 用户量大
- 去重操作频繁
- 需要高吞吐量
- 在做 “是否存在” 判定
那 Bloom Filter 必定值得你立刻把它接入生产。
你不会后悔。 你的数据库更不会。















- 最新
- 最热
只看作者