我把一千萬(wàn)次 SQL 校驗(yàn)干成了一次 Redis 布隆過(guò)濾——速度快到懷疑人生!
在系統(tǒng)規(guī)模還很小時(shí),很多問(wèn)題都像不存在一樣。但當(dāng)用戶量突破數(shù)百萬(wàn)、上千萬(wàn)后,你會(huì)發(fā)現(xiàn)某些你從未關(guān)注過(guò)的“小細(xì)節(jié)”,會(huì)突然變成拖垮系統(tǒng)的幕后黑手。
我就踩過(guò)這樣一個(gè)坑——一個(gè)看似微不足道的用戶名重復(fù)校驗(yàn),差點(diǎn)把數(shù)據(jù)庫(kù)打到 CPU 100%。
這篇文章將帶你從“數(shù)據(jù)庫(kù)快燒掉了”,一路走到“靠 Redis Bloom Filter 穩(wěn)住場(chǎng)面”的完整過(guò)程,并給出 Spring Boot + Redis 最小可運(yùn)行 Demo。
數(shù)據(jù)庫(kù)第一次給我“吼”:你別查了行不行?
故事的開(kāi)始非常普通。
注冊(cè)接口需要校驗(yàn)用戶名是否已經(jīng)存在:
SELECT COUNT(*) FROM users WHERE username = 'praveen';沒(méi)任何問(wèn)題。 沒(méi)任何難度。 也沒(méi)任何風(fēng)險(xiǎn)——直到用戶數(shù)量突破 10,000,000。
然后,我的世界開(kāi)始變得不太美好:
- CPU 一直拉滿
- 請(qǐng)求響應(yīng)變慢
- 數(shù)據(jù)庫(kù)連接池被瞬間占滿
- 監(jiān)控圖上紅線一條接一條
讓我震驚的是,整個(gè)注冊(cè)流程最耗時(shí)的居然不是密碼校驗(yàn)、不是業(yè)務(wù)邏輯,而是這個(gè)看似 harmless 的“是否存在”查詢。
那一刻我終于明白了一件事:
數(shù)據(jù)庫(kù)天生不適合做 “我以前看過(guò)這個(gè)嗎?” 的判斷。
哪怕你給 username 建了索引,也一樣會(huì)觸發(fā):
- I/O
- 網(wǎng)絡(luò)往返
- CPU 計(jì)算
- 連接池爭(zhēng)搶
規(guī)模一大,這種查詢就是慢性毒藥。
我需要一個(gè) 能在訪問(wèn)數(shù)據(jù)庫(kù)之前做預(yù)篩選 的機(jī)制。
這時(shí),我遇見(jiàn)了 Bloom Filter:數(shù)據(jù)庫(kù)門口的保鏢。
如果你把數(shù)據(jù)庫(kù)比喻成一家酒吧,那 Bloom Filter 就像門口的保鏢。
它不需要認(rèn)識(shí)所有人,但它很快就能告訴你:
- “這人肯定沒(méi)來(lái)過(guò)(不存在)” —— 100% 準(zhǔn)確
- “嗯?可能來(lái)過(guò),你進(jìn)去問(wèn)下(可能存在)” —— 有一定誤報(bào)率
這樣,大量根本不存在的用戶名 就會(huì)被擋在數(shù)據(jù)庫(kù)門外。
在實(shí)際測(cè)試中,它幫我過(guò)濾掉 90% ~ 99% 的數(shù)據(jù)庫(kù)查詢。
數(shù)據(jù)庫(kù):終于可以喘口氣了。
為什么我選擇 Redis 來(lái)承載 Bloom Filter?
Bloom Filter 本質(zhì)上是一個(gè)位數(shù)組 + 多個(gè)哈希函數(shù)。 Redis 天然適合作為它的容器,因?yàn)椋?/span>
- Redis 全內(nèi)存存儲(chǔ),速度是納秒級(jí)別
- 支持位圖操作(setbit / getbit)
- 可以集群共享
- 可以持久化
幾乎是為 Bloom Filter 量身定制的。
特別適合這些場(chǎng)景:
- 用戶名 / 郵箱是否重復(fù)
- 交易請(qǐng)求是否重復(fù)
- 抓取系統(tǒng)去重
- 爬蟲(chóng) URL 判重
- 防垃圾消息
手把手帶你搭一個(gè)最小可用 Demo(Spring Boot + Redis)
接下來(lái),打造一個(gè)本地可跑的版本。
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-serverWindows
choco install redis
redis-server配置文件 application.properties
spring.application.name=bloom-filter-demo
spring.redis.host=localhost
spring.redis.port=6379核心邏輯:Bloom Filter 實(shí)現(xiàn)(已優(yōu)化代碼)
路徑: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 {
// 位數(shù)組長(zhǎng)度
private static final int SIZE = 1_000_000;
// 多種哈希函數(shù)種子
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; // 只要有一個(gè) bit 為 false,則一定不存在
}
}
return true; // 所有 bit 都是 true,則可能存在
}
/** 哈希函數(shù) */
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!";
}
}本地測(cè)試
添加用戶名
curl -X POST http://localhost:8080/usernames/add/praveencodes檢查已添加用戶
curl http://localhost:8080/usernames/check/praveencodes檢查不存在的用戶
curl http://localhost:8080/usernames/check/randomuser內(nèi)部到底發(fā)生了什么?
過(guò)程非常簡(jiǎn)單但高效:
- 使用多個(gè)哈希函數(shù)對(duì) username 計(jì)算多個(gè) hash
- Redis 位數(shù)組對(duì)應(yīng)位置全部置 1
- 查詢時(shí)只檢查這些位是否都是 1
- 若都為 1 → 可能存在
- 若任意為 0 → 肯定不存在
- 如果判斷“不存在”,則不會(huì)訪問(wèn)數(shù)據(jù)庫(kù)
結(jié)果就是:
數(shù)據(jù)庫(kù)請(qǐng)求從 千萬(wàn)級(jí)下降到十萬(wàn)級(jí)QPS 提升數(shù)倍延遲從 3ms → 0.05ms
真實(shí)壓測(cè)結(jié)果(1000 萬(wàn)條數(shù)據(jù))
指標(biāo) | 傳統(tǒng)數(shù)據(jù)庫(kù) | Bloom Filter 預(yù)篩選 |
單次查詢耗時(shí) | 3–5 ms | 0.05 ms |
總 DB 查詢數(shù) | 10,000,000 | ~100,000 |
內(nèi)存占用 | 高 | 僅幾 MB |
Bloom Filter 就像一個(gè)超級(jí)前置緩存,把無(wú)意義的數(shù)據(jù)請(qǐng)求擋在數(shù)據(jù)庫(kù)之外。
Bloom Filter 的局限性
Bloom Filter 看似完美,但必須認(rèn)識(shí)這幾點(diǎn):
有誤報(bào)(False Positive)
- 會(huì)誤判“存在”
- 不會(huì)誤判“不存在” (即:不會(huì)漏掉實(shí)際存在的數(shù)據(jù))
無(wú)法刪除
- 位數(shù)組只有 0/1
- 刪除會(huì)影響其他元素 (除非使用 Counting Bloom Filter)
參數(shù)需要調(diào)優(yōu)
- 位數(shù)組大小
- 哈希函數(shù)數(shù)量
- 容量規(guī)劃 都會(huì)影響誤報(bào)率與性能
結(jié)語(yǔ):唯一比 Bloom Filter 更可怕的,是忽視它
我們常常會(huì)把注意力放在復(fù)雜的優(yōu)化上,比如多線程、分庫(kù)分表、連接池調(diào)優(yōu)。 但真正讓系統(tǒng)崩潰的,有時(shí)就是這樣一個(gè)不起眼的存在校驗(yàn)。
Bloom Filter 的出現(xiàn)不是為了替代數(shù)據(jù)庫(kù),而是為了 保護(hù)數(shù)據(jù)庫(kù)。
它能把大量重復(fù)、無(wú)意義、可預(yù)判的請(qǐng)求擋掉, 讓數(shù)據(jù)庫(kù)把資源留給真正有價(jià)值的請(qǐng)求。
如果你的系統(tǒng)有以下情況:
- 用戶量大
- 去重操作頻繁
- 需要高吞吐量
- 在做 “是否存在” 判定
那 Bloom Filter 一定值得你立刻把它接入生產(chǎn)。
你不會(huì)后悔。 你的數(shù)據(jù)庫(kù)更不會(huì)。

































