布隆过滤器
概述
布隆过滤器(Bloom Filter)是一种**空间效率极高**的概率型数据结构,由 Burton Howard Bloom 于 1970 年提出。它用于判断一个元素是否在集合中,具有以下特性:
核心特性
空间高效 :使用位数组,空间复杂度远低于哈希表
查询快速 :O(k) 时间复杂度,k 为哈希函数数量
假阳性 :可能误判"存在",但不会误判"不存在"
不可删除 :标准布隆过滤器不支持删除操作
生活类比
想象一个图书馆的新书登记系统:每本新书在多个登记册上打勾。检查一本书是否是新书时,只要发现任意一个登记册上没有打勾,就一定不是新书;但如果所有登记册都打勾了,可能是这本书确实登记过,也可能是其他书碰巧在相同位置打了勾(假阳性)。
核心原理
数据结构
布隆过滤器由两部分组成:
Text Only 布隆过滤器结构:
┌─────────────────────────────────────────────────────┐
│ 位数组 (Bit Array) │
│ ┌───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┐ │
│ │ 0 │ 1 │ 0 │ 1 │ 1 │ 0 │ 0 │ 1 │ 0 │ 1 │ 0 │ 0 │ │ │
│ └───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┘ │
│ 0 1 2 3 4 5 6 7 8 9 10 11 │
└─────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────┐
│ k 个独立的哈希函数 │
│ h₁(x), h₂(x), h₃(x), ..., hₖ(x) │
└─────────────────────────────────────────────────────┘
工作流程
flowchart TB
subgraph "插入操作 Insert"
A1["输入元素 x"] --> B1["计算 k 个哈希值"]
B1 --> C1["h₁(x), h₂(x), ..., hₖ(x)"]
C1 --> D1["将对应位置设为 1"]
end
subgraph "查询操作 Query"
A2["输入元素 x"] --> B2["计算 k 个哈希值"]
B2 --> C2["检查所有对应位"]
C2 --> D2{"所有位都为 1?"}
D2 -->|是| E2["返回: 可能存在"]
D2 -->|否| F2["返回: 一定不存在"]
end
style E2 fill:#FFF3E0,stroke:#FF9800
style F2 fill:#E8F5E9,stroke:#4CAF50
插入操作详解
Text Only 插入元素 "apple" 的过程 (m=12, k=3):
步骤1: 计算哈希值
┌────────────────────────────────────────┐
│ h₁("apple") = 5381 → 5381 % 12 = 5 │
│ h₂("apple") = 2166 → 2166 % 12 = 6 │
│ h₃("apple") = 8923 → 8923 % 12 = 11 │
└────────────────────────────────────────┘
步骤2: 设置位数组
初始状态:
┌───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┐
│ 0 │ 0 │ 0 │ 0 │ 0 │ 0 │ 0 │ 0 │ 0 │ 0 │ 0 │ 0 │
└───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┘
0 1 2 3 4 5 6 7 8 9 10 11
设置后:
┌───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┐
│ 0 │ 0 │ 0 │ 0 │ 0 │ 1 │ 1 │ 0 │ 0 │ 0 │ 0 │ 1 │
└───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┘
0 1 2 3 4 5 6 7 8 9 10 11
↑ ↑ ↑
h₁ h₂ h₃
查询操作详解
Text Only 查询元素 "banana" 的过程:
步骤1: 计算哈希值
┌────────────────────────────────────────┐
│ h₁("banana") = 3521 → 3521 % 12 = 5 │
│ h₂("banana") = 7842 → 7842 % 12 = 6 │
│ h₃("banana") = 1234 → 1234 % 12 = 10 │
└────────────────────────────────────────┘
步骤2: 检查位数组
┌───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┐
│ 0 │ 0 │ 0 │ 0 │ 0 │ 1 │ 1 │ 0 │ 0 │ 0 │ 0 │ 1 │
└───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┘
0 1 2 3 4 5 6 7 8 9 10 11
↓ ↓ ↓
✓(1) ✓(1) ✗(0)
结果: bit[10] = 0 → "banana" 一定不存在 ✓
假阳性现象
Text Only 假阳性示例:
假设已插入: "apple", "orange", "grape"
位数组状态:
┌───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┐
│ 0 │ 1 │ 0 │ 1 │ 1 │ 1 │ 1 │ 0 │ 1 │ 0 │ 1 │ 1 │
└───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┘
0 1 2 3 4 5 6 7 8 9 10 11
查询 "banana" (未插入):
┌────────────────────────────────────────┐
│ h₁("banana") = 5 → bit[5] = 1 ✓ │
│ h₂("banana") = 8 → bit[8] = 1 ✓ │
│ h₃("banana") = 11 → bit[11] = 1 ✓ │
└────────────────────────────────────────┘
结果: 所有位都为 1 → "banana" 可能存在
实际: "banana" 未插入 → 这是假阳性!
graph TB
subgraph "假阳性原因"
A["元素 A 设置了位置 1, 5, 8"]
B["元素 B 设置了位置 3, 5, 11"]
C["元素 C 设置了位置 1, 8, 11"]
D["查询元素 D 需要 5, 8, 11"]
E["这些位置已被 A, B, C 恰好覆盖"]
F["导致 D 被误判为存在"]
end
A --> E
B --> E
C --> E
D --> E
E --> F
style F fill:#FFF3E0,stroke:#FF9800
数学原理
假阳性概率推导
设位数组大小为 m,插入 n 个元素,使用 k 个哈希函数。
推导过程
插入一个元素后,某一位仍为 0 的概率:(1 - 1/m)^k ≈ e^(-k/m)
插入 n 个元素后,某一位仍为 0 的概率:(1 - 1/m)^(nk) ≈ e^(-nk/m)
查询一个不存在的元素时,所有 k 位都为 1 的概率:
\[P_{fp} = \left(1 - e^{-kn/m}\right)^k\]
最优参数选择
最优哈希函数数量
当位数组大小 m 和元素数量 n 确定时,最优哈希函数数量为:
k = (m/n) × ln(2) ≈ 0.693 × (m/n)
最优位数组大小
给定预期元素数量 n 和可接受假阳性率 p 时:
m = -n × ln(p) / (ln(2))²
参数关系图
graph LR
subgraph "参数关系"
A["预期元素数 n"] --> C["计算最优 m 和 k"]
B["可接受假阳性率 p"] --> C
C --> D["位数组大小 m"]
C --> E["哈希函数数 k"]
end
subgraph "示例: n=100万, p=1%"
F["m ≈ 9.6M bits ≈ 1.2MB"]
G["k ≈ 7"]
end
D --> F
E --> G
空间效率对比
Text Only 存储 100 万元素,假阳性率 1%:
┌────────────────────────────────────────────────────┐
│ 数据结构 │ 空间占用 │ 相对大小 │
├────────────────────────────────────────────────────┤
│ 哈希表(整数) │ ~32 MB │ 100% │
│ 哈希表(字符串) │ ~50-100 MB │ 150-300% │
│ 布隆过滤器 │ ~1.2 MB │ 3.75% │
│ 位图(整数范围已知)│ ~1.2 MB │ 3.75% │
└────────────────────────────────────────────────────┘
布隆过滤器节省约 96% 的空间!
基本实现
C C++ Python Java Go Rust
C #include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <math.h>
#include <stdint.h>
typedef struct {
uint8_t * bits ;
int size ;
int hashCount ;
} BloomFilter ;
uint32_t murmur3_32 ( const char * key , int len , uint32_t seed ) {
const uint8_t * data = ( const uint8_t * ) key ;
uint32_t h1 = seed ;
const uint32_t c1 = 0xcc9e2d51 ;
const uint32_t c2 = 0x1b873593 ;
for ( int i = 0 ; i < len ; i ++ ) {
uint32_t k1 = data [ i ];
k1 *= c1 ;
k1 = ( k1 << 15 ) | ( k1 >> 17 );
k1 *= c2 ;
h1 ^= k1 ;
h1 = ( h1 << 13 ) | ( h1 >> 19 );
h1 = h1 * 5 + 0xe6546b64 ;
}
h1 ^= len ;
h1 ^= h1 >> 16 ;
h1 *= 0x85ebca6b ;
h1 ^= h1 >> 13 ;
h1 *= 0xc2b2ae35 ;
h1 ^= h1 >> 16 ;
return h1 ;
}
BloomFilter * bloomCreate ( int size , int hashCount ) {
BloomFilter * bf = ( BloomFilter * ) malloc ( sizeof ( BloomFilter ));
bf -> size = size ;
bf -> hashCount = hashCount ;
bf -> bits = ( uint8_t * ) calloc (( size + 7 ) / 8 , sizeof ( uint8_t ));
return bf ;
}
BloomFilter * bloomCreateOptimal ( int expectedItems , double falsePositiveRate ) {
double ln2 = log ( 2 );
int m = ( int )( - ( expectedItems * log ( falsePositiveRate )) / ( ln2 * ln2 ));
int k = ( int )(( m / expectedItems ) * ln2 );
if ( k < 1 ) k = 1 ;
if ( k > 32 ) k = 32 ;
return bloomCreate ( m , k );
}
static void setBit ( uint8_t * bits , int index ) {
bits [ index / 8 ] |= ( 1 << ( index % 8 ));
}
static int getBit ( uint8_t * bits , int index ) {
return ( bits [ index / 8 ] >> ( index % 8 )) & 1 ;
}
void bloomAdd ( BloomFilter * bf , const char * item ) {
int len = strlen ( item );
for ( int i = 0 ; i < bf -> hashCount ; i ++ ) {
uint32_t hash = murmur3_32 ( item , len , i );
int index = hash % bf -> size ;
setBit ( bf -> bits , index );
}
}
int bloomContains ( BloomFilter * bf , const char * item ) {
int len = strlen ( item );
for ( int i = 0 ; i < bf -> hashCount ; i ++ ) {
uint32_t hash = murmur3_32 ( item , len , i );
int index = hash % bf -> size ;
if ( ! getBit ( bf -> bits , index )) return 0 ;
}
return 1 ;
}
void bloomFree ( BloomFilter * bf ) {
free ( bf -> bits );
free ( bf );
}
C++ #include <vector>
#include <string>
#include <cmath>
#include <cstdint>
class BloomFilter {
private :
std :: vector < uint8_t > bits ;
int hashCount ;
int size ;
uint32_t getHash ( const std :: string & item , int i ) const {
uint32_t hash = i ;
for ( char c : item ) {
hash = hash * 31 + c ;
}
return hash ;
}
public :
BloomFilter ( int numBits , int numHashes )
: bits (( numBits + 7 ) / 8 , 0 ),
hashCount ( numHashes ),
size ( numBits ) {}
static BloomFilter create ( int expectedItems , double falsePositiveRate ) {
double ln2 = std :: log ( 2 );
int m = static_cast < int > ( - ( expectedItems * std :: log ( falsePositiveRate )) / ( ln2 * ln2 ));
int k = static_cast < int > (( m / expectedItems ) * ln2 );
k = std :: max ( 1 , std :: min ( k , 32 ));
return BloomFilter ( m , k );
}
void add ( const std :: string & item ) {
for ( int i = 0 ; i < hashCount ; i ++ ) {
uint32_t hash = getHash ( item , i );
int index = hash % size ;
bits [ index / 8 ] |= ( 1 << ( index % 8 ));
}
}
bool contains ( const std :: string & item ) const {
for ( int i = 0 ; i < hashCount ; i ++ ) {
uint32_t hash = getHash ( item , i );
int index = hash % size ;
if ( ! ( bits [ index / 8 ] & ( 1 << ( index % 8 )))) return false ;
}
return true ;
}
};
Python import math
from typing import List
class BloomFilter :
def __init__ ( self , size : int , hash_count : int ):
self . size = size
self . hash_count = hash_count
self . bits = [ 0 ] * (( size + 7 ) // 8 )
@classmethod
def create ( cls , expected_items : int , false_positive_rate : float ):
ln2 = math . log ( 2 )
m = int ( - ( expected_items * math . log ( false_positive_rate )) / ( ln2 * ln2 ))
k = int (( m / expected_items ) * ln2 )
k = max ( 1 , min ( k , 32 ))
return cls ( m , k )
def _hash ( self , item : str , seed : int ) -> int :
hash_val = seed
for c in item :
hash_val = hash_val * 31 + ord ( c )
return hash_val % self . size
def _set_bit ( self , index : int ):
self . bits [ index // 8 ] |= ( 1 << ( index % 8 ))
def _get_bit ( self , index : int ) -> bool :
return bool ( self . bits [ index // 8 ] & ( 1 << ( index % 8 )))
def add ( self , item : str ):
for i in range ( self . hash_count ):
index = self . _hash ( item , i )
self . _set_bit ( index )
def contains ( self , item : str ) -> bool :
for i in range ( self . hash_count ):
index = self . _hash ( item , i )
if not self . _get_bit ( index ):
return False
return True
Java public class BloomFilter {
private byte [] bits ;
private int size ;
private int hashCount ;
public BloomFilter ( int size , int hashCount ) {
this . size = size ;
this . hashCount = hashCount ;
this . bits = new byte [ ( size + 7 ) / 8 ] ;
}
public static BloomFilter create ( int expectedItems , double falsePositiveRate ) {
double ln2 = Math . log ( 2 );
int m = ( int )( - ( expectedItems * Math . log ( falsePositiveRate )) / ( ln2 * ln2 ));
int k = ( int )(( m / expectedItems ) * ln2 );
k = Math . max ( 1 , Math . min ( k , 32 ));
return new BloomFilter ( m , k );
}
private int getHash ( String item , int seed ) {
int hash = seed ;
for ( char c : item . toCharArray ()) {
hash = hash * 31 + c ;
}
return Math . abs ( hash % size );
}
public void add ( String item ) {
for ( int i = 0 ; i < hashCount ; i ++ ) {
int index = getHash ( item , i );
bits [ index / 8 ] |= ( 1 << ( index % 8 ));
}
}
public boolean contains ( String item ) {
for ( int i = 0 ; i < hashCount ; i ++ ) {
int index = getHash ( item , i );
if (( bits [ index / 8 ] & ( 1 << ( index % 8 ))) == 0 ) return false ;
}
return true ;
}
}
Go type BloomFilter struct {
bits [] byte
size int
hashCount int
}
func NewBloomFilter ( size , hashCount int ) * BloomFilter {
return & BloomFilter {
bits : make ([] byte , ( size + 7 ) / 8 ),
size : size ,
hashCount : hashCount ,
}
}
func NewBloomFilterOptimal ( expectedItems int , falsePositiveRate float64 ) * BloomFilter {
ln2 := math . Log ( 2 )
m := int ( - float64 ( expectedItems ) * math . Log ( falsePositiveRate ) / ( ln2 * ln2 ))
k := int ( float64 ( m ) / float64 ( expectedItems ) * ln2 )
if k < 1 {
k = 1
}
if k > 32 {
k = 32
}
return NewBloomFilter ( m , k )
}
func ( bf * BloomFilter ) getHash ( item string , seed int ) int {
hash := seed
for _ , c := range item {
hash = hash * 31 + int ( c )
}
if hash < 0 {
hash = - hash
}
return hash % bf . size
}
func ( bf * BloomFilter ) Add ( item string ) {
for i := 0 ; i < bf . hashCount ; i ++ {
index := bf . getHash ( item , i )
bf . bits [ index / 8 ] |= ( 1 << ( index % 8 ))
}
}
func ( bf * BloomFilter ) Contains ( item string ) bool {
for i := 0 ; i < bf . hashCount ; i ++ {
index := bf . getHash ( item , i )
if bf . bits [ index / 8 ] & ( 1 << ( index % 8 )) == 0 {
return false
}
}
return true
}
Rust pub struct BloomFilter {
bits : Vec < u8 > ,
size : usize ,
hash_count : usize ,
}
impl BloomFilter {
pub fn new ( size : usize , hash_count : usize ) -> Self {
BloomFilter {
bits : vec ! [ 0 ; ( size + 7 ) / 8 ],
size ,
hash_count ,
}
}
pub fn create_optimal ( expected_items : usize , false_positive_rate : f64 ) -> Self {
let ln2 = 2 f64 . ln ();
let m = ( - ( expected_items as f64 * false_positive_rate . ln ()) / ( ln2 * ln2 )) as usize ;
let k = (( m as f64 / expected_items as f64 ) * ln2 ) as usize ;
let k = k . max ( 1 ). min ( 32 );
Self :: new ( m , k )
}
fn get_hash ( & self , item : & str , seed : usize ) -> usize {
let mut hash = seed as u32 ;
for c in item . chars () {
hash = hash . wrapping_mul ( 31 ). wrapping_add ( c as u32 );
}
( hash as usize ) % self . size
}
pub fn add ( & mut self , item : & str ) {
for i in 0 .. self . hash_count {
let index = self . get_hash ( item , i );
self . bits [ index / 8 ] |= 1 << ( index % 8 );
}
}
pub fn contains ( & self , item : & str ) -> bool {
for i in 0 .. self . hash_count {
let index = self . get_hash ( item , i );
if self . bits [ index / 8 ] & ( 1 << ( index % 8 )) == 0 {
return false ;
}
}
true
}
}
布隆过滤器变体
1. 计数布隆过滤器(支持删除)
graph TB
subgraph "标准布隆过滤器"
A1["位数组: 0/1"] --> B1["不支持删除"]
B1 --> C1["空间最优"]
end
subgraph "计数布隆过滤器"
A2["计数数组: 0,1,2,..."] --> B2["支持删除"]
B2 --> C2["空间开销更大"]
end
style B2 fill:#E8F5E9,stroke:#4CAF50
C // 计数布隆过滤器
typedef struct {
uint8_t * counts ; // 计数数组(每项4位或8位)
int size ;
int hashCount ;
} CountingBloomFilter ;
CountingBloomFilter * cbfCreate ( int size , int hashCount ) {
CountingBloomFilter * cbf = malloc ( sizeof ( CountingBloomFilter ));
cbf -> size = size ;
cbf -> hashCount = hashCount ;
cbf -> counts = calloc ( size , sizeof ( uint8_t ));
return cbf ;
}
void cbfAdd ( CountingBloomFilter * cbf , const char * item ) {
int len = strlen ( item );
for ( int i = 0 ; i < cbf -> hashCount ; i ++ ) {
uint32_t hash = murmur3_32 ( item , len , i );
int index = hash % cbf -> size ;
// 防止溢出
if ( cbf -> counts [ index ] < 255 ) {
cbf -> counts [ index ] ++ ;
}
}
}
void cbfRemove ( CountingBloomFilter * cbf , const char * item ) {
int len = strlen ( item );
for ( int i = 0 ; i < cbf -> hashCount ; i ++ ) {
uint32_t hash = murmur3_32 ( item , len , i );
int index = hash % cbf -> size ;
if ( cbf -> counts [ index ] > 0 ) {
cbf -> counts [ index ] -- ;
}
}
}
int cbfContains ( CountingBloomFilter * cbf , const char * item ) {
int len = strlen ( item );
for ( int i = 0 ; i < cbf -> hashCount ; i ++ ) {
uint32_t hash = murmur3_32 ( item , len , i );
int index = hash % cbf -> size ;
if ( cbf -> counts [ index ] == 0 ) {
return 0 ;
}
}
return 1 ;
}
2. 布隆过滤器变体对比
graph TB
subgraph "布隆过滤器家族"
A["标准布隆过滤器"]
B["计数布隆过滤器"]
C["可扩展布隆过滤器"]
D["布隆计数器"]
E["压缩布隆过滤器"]
end
A --> A1["不支持删除"]
A --> A2["固定容量"]
A --> A3["空间最优"]
B --> B1["支持删除"]
B --> B2["空间开销 3-4倍"]
B --> B3["可能计数溢出"]
C --> C1["动态扩容"]
C --> C2["自动调整参数"]
C --> C3["适合未知数据量"]
D --> D1["支持频数查询"]
D --> D2["近似计数"]
E --> E1["压缩存储"]
E --> E2["适合传输"]
style A fill:#E3F2FD,stroke:#2196F3
style B fill:#E8F5E9,stroke:#4CAF50
style C fill:#FFF3E0,stroke:#FF9800
3. 可扩展布隆过滤器
C // 可扩展布隆过滤器 - 支持动态扩容
typedef struct {
BloomFilter ** filters ; // 多个布隆过滤器
int count ; // 过滤器数量
int baseSize ; // 基硝始始大小
double falsePositiveRate ;
} ScalableBloomFilter ;
ScalableBloomFilter * sbfCreate ( int initialSize , double fpRate ) {
ScalableBloomFilter * sbf = malloc ( sizeof ( ScalableBloomFilter ));
sbf -> filters = malloc ( sizeof ( BloomFilter * ) * 32 ); // 最多32层
sbf -> count = 1 ;
sbf -> baseSize = initialSize ;
sbf -> falsePositiveRate = fpRate ;
// 第一层使用基础大小
sbf -> filters [ 0 ] = bloomCreate ( initialSize ,
( int )( - log ( fpRate ) / log ( 2 )));
return sbf ;
}
void sbfAdd ( ScalableBloomFilter * sbf , const char * item ) {
// 如果当前层已满,创建新层
// 每层大小翻倍,假阳性率减半
// 这里简化处理,只添加到当前层
bloomAdd ( sbf -> filters [ sbf -> count - 1 ], item );
}
int sbfContains ( ScalableBloomFilter * sbf , const char * item ) {
// 检查所有层
for ( int i = 0 ; i < sbf -> count ; i ++ ) {
if ( bloomContains ( sbf -> filters [ i ], item )) {
return 1 ;
}
}
return 0 ;
}
哈希函数选择
哈希函数对比
Text Only ┌─────────────────────────────────────────────────────────────────┐
│ 哈希函数 │ 速度 │ 质量 │ 适用场景 │
├─────────────────────────────────────────────────────────────────┤
│ MurmurHash3 │ 快 │ 高 │ 推荐,通用场景 │
│ FNV-1a │ 很快 │ 中 │ 简单场景,代码简洁 │
│ xxHash │ 最快 │ 高 │ 性能敏感场景 │
│ CityHash │ 快 │ 高 │ Google推荐 │
│ SHA-256 │ 慢 │ 最高 │ 安全敏感场景(过度设计) │
│ DJB2 │ 很快 │ 中 │ 字符串哈希,简单实现 │
└─────────────────────────────────────────────────────────────────┘
多哈希函数生成技巧
C // 方法1: 使用双哈希技术(Double Hashing)
// 只需要两个独立哈希函数,生成 k 个哈希值
// h(i) = h1 + i * h2
void addWithDoubleHash ( BloomFilter * bf , const char * item ) {
uint32_t h1 = murmur3_32 ( item , strlen ( item ), 0 );
uint32_t h2 = murmur3_32 ( item , strlen ( item ), 1 );
for ( int i = 0 ; i < bf -> hashCount ; i ++ ) {
uint32_t hash = h1 + i * h2 ;
int index = hash % bf -> size ;
setBit ( bf -> bits , index );
}
}
// 方法2: 使用不同种子
void addWithSeeds ( BloomFilter * bf , const char * item ) {
for ( int i = 0 ; i < bf -> hashCount ; i ++ ) {
uint32_t hash = murmur3_32 ( item , strlen ( item ), i * 0x9e3779b9 );
int index = hash % bf -> size ;
setBit ( bf -> bits , index );
}
}
应用场景详解
1. 缓存穿透防护
sequenceDiagram
participant Client
participant API
participant BloomFilter
participant Cache
participant DB
Client->>API: 查询 key=x
API->>BloomFilter: contains(x)?
alt 布隆过滤器返回不存在
BloomFilter-->>API: false
API-->>Client: 返回空(避免查询DB)
else 布隆过滤器返回可能存在
BloomFilter-->>API: true
API->>Cache: get(x)
alt 缓存命中
Cache-->>API: 返回数据
else 缓存未命中
API->>DB: query(x)
DB-->>API: 返回数据
API->>Cache: set(x, data)
end
API-->>Client: 返回数据
end
C // 缓存穿透防护示例
typedef struct {
BloomFilter * bf ;
Cache * cache ;
Database * db ;
} CacheSystem ;
Data * query ( CacheSystem * sys , const char * key ) {
// 先查布隆过滤器
if ( ! bloomContains ( sys -> bf , key )) {
printf ( "%s 一定不存在,避免查询DB \n " , key );
return NULL ; // 避免查询数据库
}
// 查缓存
Data * data = cacheGet ( sys -> cache , key );
if ( data != NULL ) {
return data ;
}
// 查数据库
data = dbQuery ( sys -> db , key );
if ( data != NULL ) {
cacheSet ( sys -> cache , key , data );
}
return data ;
}
2. 网页爬虫 URL 去重
flowchart LR
A["发现新URL"] --> B{"布隆过滤器<br>已存在?"}
B -->|是| C["跳过"]
B -->|否| D["加入爬取队列"]
D --> E["爬取页面"]
E --> F["添加到布隆过滤器"]
C // URL 去重示例
typedef struct {
BloomFilter * visited ;
Queue * toCrawl ;
} WebCrawler ;
void crawl ( WebCrawler * crawler , const char * url ) {
// 检查是否已访问
if ( bloomContains ( crawler -> visited , url )) {
printf ( "URL 可能已访问: %s \n " , url );
return ;
}
// 爬取页面
Page * page = fetchPage ( url );
if ( page != NULL ) {
// 提取新URL
for ( int i = 0 ; i < page -> linkCount ; i ++ ) {
if ( ! bloomContains ( crawler -> visited , page -> links [ i ])) {
queuePush ( crawler -> toCrawl , page -> links [ i ]);
}
}
// 标记为已访问
bloomAdd ( crawler -> visited , url );
}
}
3. 垃圾邮件过滤
C // 垃圾邮件地址黑名单
typedef struct {
BloomFilter * blacklist ;
} SpamFilter ;
SpamFilter * spamFilterCreate ( int expectedEmails ) {
SpamFilter * filter = malloc ( sizeof ( SpamFilter ));
filter -> blacklist = bloomCreateOptimal ( expectedEmails , 0.001 ); // 0.1% 假阳性率
return filter ;
}
void addToBlacklist ( SpamFilter * filter , const char * email ) {
bloomAdd ( filter -> blacklist , email );
}
int isSpam ( SpamFilter * filter , const char * email ) {
if ( bloomContains ( filter -> blacklist , email )) {
// 可能是垃圾邮件
// 由于假阳性率很低(0.1%),可以再做精确检查
return 1 ;
}
return 0 ; // 一定不是垃圾邮件
}
4. 数据库查询优化
C // 避免不必要的磁盘 IO
typedef struct {
BloomFilter * indexFilter ;
StorageEngine * storage ;
} OptimizedDB ;
Record * query ( OptimizedDB * db , int key ) {
// 使用布隆过滤器作为一级索引
char keyStr [ 32 ];
sprintf ( keyStr , "%d" , key );
if ( ! bloomContains ( db -> indexFilter , keyStr )) {
printf ( "Key %d 不存在,避免磁盘IO \n " , key );
return NULL ;
}
// 可能存在,查询存储引擎
return storageQuery ( db -> storage , key );
}
5. 推荐系统去重
C // 用户已浏览内容过滤
typedef struct {
int userId ;
BloomFilter * viewed ;
} UserContext ;
int ** getRecommendations ( UserContext * user , ContentDB * db , int * count ) {
Content ** candidates = getAllContents ( db );
Content ** results = malloc ( sizeof ( Content * ) * MAX_RECOMMENDATIONS );
int resultCount = 0 ;
for ( int i = 0 ; i < db -> totalContents && resultCount < MAX_RECOMMENDATIONS ; i ++ ) {
char contentId [ 32 ];
sprintf ( contentId , "%d" , candidates [ i ] -> id );
// 过滤已浏览内容
if ( ! bloomContains ( user -> viewed , contentId )) {
results [ resultCount ++ ] = candidates [ i ];
}
}
* count = resultCount ;
return results ;
}
性能优化
1. 批量操作
C // 批量插入优化
void bloomAddBatch ( BloomFilter * bf , const char ** items , int count ) {
// 预计算所有哈希值
uint32_t * hashes = malloc ( count * bf -> hashCount * sizeof ( uint32_t ));
for ( int i = 0 ; i < count ; i ++ ) {
int len = strlen ( items [ i ]);
for ( int j = 0 ; j < bf -> hashCount ; j ++ ) {
hashes [ i * bf -> hashCount + j ] = murmur3_32 ( items [ i ], len , j ) % bf -> size ;
}
}
// 批量设置位
for ( int i = 0 ; i < count ; i ++ ) {
for ( int j = 0 ; j < bf -> hashCount ; j ++ ) {
setBit ( bf -> bits , hashes [ i * bf -> hashCount + j ]);
}
}
free ( hashes );
}
2. 位操作优化
C // 使用 64 位操作提高性能
typedef struct {
uint64_t * bits ; // 64 位整数数组
int size ;
int hashCount ;
} OptimizedBloomFilter ;
static inline void setBit64 ( uint64_t * bits , int index ) {
bits [ index / 64 ] |= ( 1ULL << ( index % 64 ));
}
static inline int getBit64 ( uint64_t * bits , int index ) {
return ( bits [ index / 64 ] >> ( index % 64 )) & 1 ;
}
3. 内存布局优化
Text Only 缓存友好的内存布局:
传统布局(可能的缓存不命中):
┌────────────────────────────────────────┐
│ bit[0] bit[1] bit[2] ... bit[m-1] │
└────────────────────────────────────────┘
优化建议:
1. 对齐到缓存行大小(64字节)
2. 预取将要访问的位
3. 使用 SIMD 指令批量检查
与其他数据结构对比
graph TB
subgraph "数据结构对比"
A["布隆过滤器<br>空间: O(m)<br>查询: O(k)<br>假阳性"]
B["哈希表<br>空间: O(n)<br>查询: O(1)<br>精确"]
C["位图<br>空间: O(max)<br>查询: O(1)<br>精确"]
D["Trie<br>空间: O(n×L)<br>查询: O(L)<br>精确"]
end
A --> A1["✓ 节省空间"]
A --> A2["✗ 可能误判"]
A --> A3["✗ 不支持删除"]
B --> B1["✓ 精确查询"]
B --> B2["✗ 空间开销大"]
B --> B3["✓ 支持删除"]
C --> C1["✓ 查询快"]
C --> C2["✗ 需要知道范围"]
C --> C3["✓ 空间紧凑"]
style A fill:#E3F2FD,stroke:#2196F3
style B fill:#E8F5E9,stroke:#4CAF50
适用场景选择
Text Only ┌─────────────────────────────────────────────────────────────────────┐
│ 场景 │ 推荐数据结构 │
├─────────────────────────────────────────────────────────────────────┤
│ 大数据集,允许假阳性 │ 布隆过滤器 ✓ │
│ 大数据集,需要精确查询 │ 哈希表 + 布隆过滤器(双层过滤) │
│ 整数范围已知 │ 位图 ✓ │
│ 需要前缀查询 │ Trie ✓ │
│ 需要删除操作 │ 计数布隆过滤器 或 Cuckoo Filter │
│ 数据量未知,可能增长 │ 可扩展布隆过滤器 ✓ │
└─────────────────────────────────────────────────────────────────────┘
复杂度分析
时间复杂度
操作
时间复杂度
说明
插入
O(k)
计算 k 个哈希值并设置位
查询
O(k)
计算 k 个哈希值并检查位
删除
O(k)
仅计数布隆过滤器支持
空间复杂度
\[空间 = \frac{m}{8} \text{ 字节} \approx \frac{-n \ln p}{(\ln 2)^2 \times 8} \text{ 字节}\]
实际性能测试
Text Only 测试环境: Intel i7-10700K, 32GB RAM
数据量: 100万元素
假阳性率: 1%
┌────────────────────────────────────────────────┐
│ 操作 │ 时间 (ms) │ QPS │
├────────────────────────────────────────────────┤
│ 插入 100万元素 │ 823 │ 1.2M/s │
│ 查询 100万元素 │ 745 │ 1.3M/s │
│ 查询不存在元素 │ 412 │ 2.4M/s │
└────────────────────────────────────────────────┘
空间占用: 1.2 MB (相比哈希表节省约 96%)
实际假阳性率: 0.98% (接近理论值 1%)
常见问题与陷阱
1. 哈希函数不独立
C // 错误示例:使用相同的哈希函数
void wrongImplementation ( BloomFilter * bf , const char * item ) {
uint32_t hash = hash1 ( item );
for ( int i = 0 ; i < bf -> hashCount ; i ++ ) {
// 错误:只用了同一个哈希值
setBit ( bf -> bits , hash % bf -> size );
}
}
// 正确示例:使用独立或派生的哈希值
void correctImplementation ( BloomFilter * bf , const char * item ) {
for ( int i = 0 ; i < bf -> hashCount ; i ++ ) {
uint32_t hash = murmur3_32 ( item , strlen ( item ), i );
setBit ( bf -> bits , hash % bf -> size );
}
}
2. 参数选择不当
Text Only 常见错误:
1. 哈希函数数量过多 (k > 20)
- 增加计算开销
- 降低假阳性率收益递减
2. 位数组太小 (m < n)
- 假阳性率接近 100%
- 失去使用意义
3. 忽略扩容需求
- 数据量超出预期
- 假阳性率急剧上升
3. 错误删除
C // 错误:在标准布隆过滤器中删除
void wrongDelete ( BloomFilter * bf , const char * item ) {
// 危险!会影响其他元素
for ( int i = 0 ; i < bf -> hashCount ; i ++ ) {
uint32_t hash = murmur3_32 ( item , strlen ( item ), i );
int index = hash % bf -> size ;
// 错误:直接清除位
bf -> bits [ index / 8 ] &= ~ ( 1 << ( index % 8 ));
}
}
// 正确:使用计数布隆过滤器
void correctDelete ( CountingBloomFilter * cbf , const char * item ) {
cbfRemove ( cbf , item ); // 使用专门的删除函数
}
参考资料