文章目錄
- 前言
- 什么是布隆過濾器
- 項目中引入布隆過濾器
- 與緩存結合的最佳實踐
- 場景:高并發用戶訪問商品詳情頁(防止緩存穿透)
- 總結:
前言
okok 我們已經學完了 所有的redis中的常用的數據結構 下面就是進階
我會用一系列的例子 去講解 如何使用這些數據結構 以及 如何結合到我們的實際業務代碼中去
本文將會介紹布隆過濾器 以及結合緩存的最佳實踐
什么是布隆過濾器
Bloom過濾器是一種空間高效的概率性數據結構,用于快速判斷一個元素是否可能存在于一個集合中。
基本組成:
位數組(Bit Array): 長度為m的二進制數組,初始全為0
哈希函數集合: k個獨立的哈希函數 h?, h?, …, h?
元素集合: 需要存儲的數據集合
工作流程分析:
插入操作 (Add)
- 對元素x計算k個哈希值: h?(x), h?(x), …, h?(x)
- 將位數組中對應位置設為1:
bit[h?(x) % m] = 1
bit[h?(x) % m] = 1
bit[h?(x) % m] = 1 …
查詢操作 (Query)
- 對元素x計算k個哈希值: h?(x), h?(x), …, h?(x)
- 檢查位數組中對應位置: if bit[h?(x) % m] == 1 AND
bit[h?(x) % m] == 1 AND
… AND
bit[h?(x) % m] == 1:
return “可能存在” else:
return “一定不存在”
怎么簡單去理解
就是有 n個 Hash表 每次插入數據 首先hash(數據) 到這 N 個 hash表 每個位置設置為1
然后查詢的時候
再次使用相同的hash函數去hash(數據) 到這 N 個 hash表 檢查每個位置是否為1
若為1 則可能存在通過校驗 (疑問 為什么是可能存在呢?)
若存在某一位為0 則一定不存在 (疑問 為什么是一定不存在呢?)
怎么理解:可能存在
因為存在hash沖突啊 還有可能另外的數據也把當前位置 修改成了1 所以是可能而不是一定
怎么理解:一定不存在
你想想 當前受檢的hash位置 都是0 說明根本就沒有插入過這個數據 如果插入過 則一定會把這一位 置為1 但是為0 說明這個元素沒有插入過當前的布隆過濾器
項目中引入布隆過濾器
依賴:
<dependency><groupId>org.redisson</groupId><artifactId>redisson</artifactId><version>3.50.0</version></dependency>
配置類:
@Configuration
public class RedissonConfig {@Beanpublic RedissonClient redissonClient() {Config config = new Config();// 如果你是單機模式config.useSingleServer().setAddress("redis://127.0.0.1:6379");return Redisson.create(config);}
}
@Service
@RequiredArgsConstructor
public class RedisBloomFilterService {private final RedissonClient redissonClient;public void initBloomFilter(String name, long expectedInsertions, double falseProbability) {RBloomFilter<String> bloomFilter = redissonClient.getBloomFilter(name);// 初始化:預估元素數量 + 期望誤判率(如 0.01)bloomFilter.tryInit(expectedInsertions, falseProbability);}public void add(String name, String value) {RBloomFilter<String> bloomFilter = redissonClient.getBloomFilter(name);bloomFilter.add(value);}public boolean mightContain(String name, String value) {RBloomFilter<String> bloomFilter = redissonClient.getBloomFilter(name);return bloomFilter.contains(value);}
}
測試類
@SpringBootTest
class RedisBloomFilterServiceTest {@Autowiredprivate RedisBloomFilterService bloomFilterService;@Testvoid testBloomFilter() {String filterName = "user:bloom";// 初始化:10 萬數據,誤判率 1%bloomFilterService.initBloomFilter(filterName, 100_000, 0.01);bloomFilterService.add(filterName, "user_1001");bloomFilterService.add(filterName, "user_1002");assert bloomFilterService.mightContain(filterName, "user_1001"); // trueassert !bloomFilterService.mightContain(filterName, "user_9999"); // very likely false}
}
與緩存結合的最佳實踐
三級防護體系
請求 → Bloom過濾器 → 本地緩存 → Redis緩存 → 數據庫↓ ↓ ↓ ↓攔截99%無效請求 熱點數據 分布式緩存 最終數據源
@Service
public class MultiLevelCacheService {private BloomFilter<String> bloomFilter; // L0: 布隆過濾器private Cache<String, Object> localCache; // L1: 本地緩存private RedisTemplate redisTemplate; // L2: Redis緩存private Database database; // L3: 數據庫public Object getData(String key) {// L0: Bloom過濾器檢查if (!bloomFilter.mightContain(key)) {return null; // 一定不存在}// L1: 本地緩存檢查Object data = localCache.getIfPresent(key);if (data != null) {return data;}// L2: Redis緩存檢查data = redisTemplate.opsForValue().get(key);if (data != null) {localCache.put(key, data); // 回填本地緩存return data;}// L3: 數據庫查詢data = database.findById(key);if (data != null) {redisTemplate.opsForValue().set(key, data, Duration.ofMinutes(30));localCache.put(key, data);} else {// 設置空值緩存,防止緩存穿透redisTemplate.opsForValue().set(key, "NULL", Duration.ofMinutes(5));}return data;}
}
場景:高并發用戶訪問商品詳情頁(防止緩存穿透)
用戶通過 /product/{id} 請求商品詳情;
正常流程:Redis 緩存命中 → 返回數據;
若 Redis 未命中,就查 DB → 然后寫入 Redis;
問題:惡意請求 / 錯誤 id 請求不斷打穿緩存,導致 DB 被打爆,這就是「緩存穿透」。
如何解決這個問題 需要使用布隆過濾器
- 用戶請求商品 /product/99999
- 系統先檢查 BloomFilter.contains(“99999”): → false → 直接拒絕,無需查緩存/數據庫 → true → 正常查 Redis 緩存,未命中則查數據庫
loomFilter 初始化(商品上線時構建)
@PostConstruct
public void initBloomFilter() {RBloomFilter<String> bloomFilter = redissonClient.getBloomFilter("bloom:product");bloomFilter.tryInit(1_000_000L, 0.01); // 容量:100w,誤判率:1%// 假設所有商品ID:1~1000000for (int i = 1; i <= 1000000; i++) {bloomFilter.add(String.valueOf(i));}
}
Controller 攔截請求:
@GetMapping("/product/{id}")
public ResponseEntity<?> getProduct(@PathVariable String id) {RBloomFilter<String> bloomFilter = redissonClient.getBloomFilter("bloom:product");if (!bloomFilter.contains(id)) {return ResponseEntity.status(HttpStatus.NOT_FOUND).body("非法商品ID,拒絕訪問");}// 查詢緩存String redisKey = "product:" + id;Object product = redisTemplate.opsForValue().get(redisKey);if (product != null) return ResponseEntity.ok(product);// 查詢數據庫(模擬)Product result = productService.queryById(id);if (result != null) {redisTemplate.opsForValue().set(redisKey, result, Duration.ofMinutes(30));return ResponseEntity.ok(result);} else {// 緩存空對象防止緩存穿透redisTemplate.opsForValue().set(redisKey, "NULL", Duration.ofMinutes(10));return ResponseEntity.status(HttpStatus.NOT_FOUND).body("商品不存在");}
}
測試代碼
@Test
public void testBloomFilterConcurrency() throws InterruptedException {ExecutorService executor = Executors.newFixedThreadPool(20);for (int i = 0; i < 1000; i++) {final int id = i;executor.submit(() -> {String fakeId = String.valueOf(id);boolean mayExist = redissonClient.getBloomFilter("bloom:product").contains(fakeId);if (mayExist) {System.out.println("合法ID:" + fakeId + " → 繼續查緩存/DB");} else {System.out.println("非法ID:" + fakeId + " → 攔截");}});}executor.shutdown();executor.awaitTermination(1, TimeUnit.MINUTES);
}
總結:
布隆過濾器 快速判斷一個元素是否可能存在于一個集合中