你无法调整布隆过滤器的大小。以下是可行的做法。

发布: (2025年12月29日 GMT+8 00:34)
11 min read
原文: Dev.to

Source: Dev.to

请提供您希望翻译的正文内容(除代码块和 URL 之外的文本),我将按照要求保持原有的 Markdown 格式和技术术语进行简体中文翻译。

什么是 Bloom 过滤器?

Bloom 过滤器是一种 概率数据结构,使用位数组和哈希函数来减轻主数据库的负载。
你可能会想,为什么不直接使用像 Redis 这样的缓存系统。答案归结为 准确性内存效率 之间的权衡:

系统准确性内存效率
基本 Redis(或类似缓存)✅ 精确❌ 不够节省空间
Bloom 过滤器❓ 概率性的(可能出现误报)✅ 非常节省空间

规划 Bloom 过滤器

在使用 Bloom 过滤器之前,你必须决定:

  1. 位数组的大小 (m) – 一旦设定,之后无法更改。
  2. 哈希函数的数量 (k) – 同样在创建后固定。

这两个参数都取决于:

  • n – 预计要存储的项目数量。
  • p – 可接受的误报概率。

选择哈希函数的数量

哈希函数的数量不能太低(导致大量冲突),也不能太高(快速填满数组并且同样导致冲突)。
最佳值由以下公式给出:

[ k = \frac{m}{n},\ln 2 ]

在 Go 中计算最佳参数

package main

import (
	"math"
)

// CalculateOptimalParams returns the optimal bit‑array size (m) and
// number of hash functions (k) for a given item count (n) and false‑positive
// rate (p).
func CalculateOptimalParams(n int, p float64) (uint, uint) {
	// 1. Ideal bit‑array size (m)
	//    m = - (n * ln(p)) / (ln(2))²
	m := -float64(n) * math.Log(p) / math.Pow(math.Log(2), 2)

	// 2. Ideal number of hash functions (k)
	//    k = (m / n) * ln(2)
	k := (m / float64(n)) * math.Log(2)

	return uint(math.Ceil(m)), uint(math.Ceil(k))
}

func main() {
	n := 1000          // Expected number of items
	p := 0.01          // Desired false‑positive rate (1%)
	m, k := CalculateOptimalParams(n, p)

	// Print the recommended parameters
	println("Recommended Bit Array Size (m):", m)
	println("Recommended Hash Functions (k):", k)
}

Bloom 过滤器的核心逻辑

Bloom 过滤器回答的问题是 “这个项目可能存在于集合中吗?”

如果答案是 肯定不,我们可以跳过数据库查询。
如果答案是 可能,则必须在真实数据源中进行验证。

示例场景:用户名可用性

  1. 传统方法 – 执行 SQL 查询:

    SELECT 1 FROM users WHERE username = 'ayush' LIMIT 1;

    即使有索引,查询成本大约是 O(log n)

  2. Bloom 过滤器方法
    先检查过滤器。
    如果它返回 ,立即接受该用户名。
    如果它返回 可能,则回退到数据库查询。

这可以显著降低数据库负载。

Bloom Filter 的工作原理

Bloom Filter 由两个组件组成:

  1. Bit Array – 由 0 和 1 组成的数组(例如 [0,0,0,0,0])。
  2. Hash Functions – 将一个值映射到 Bit Array 中索引的确定性函数。

步骤演示

假设有一个 10 位 的 Bit Array,初始全部为 0

步骤操作结果 Bit Array
1. 添加 “Ayush”hash1("Ayush") = 2
hash2("Ayush") = 7
[0,0,1,0,0,0,0,1,0,0]
2. 检查 “Kumar”hash1("Kumar") = 3
hash2("Kumar") = 4
位 3 和位 4 为 0一定不存在
3. 检查 “Anand”(误报)hash1("Anand") = 2
hash2("Anand") = 7
两个位均为 1可能存在(实际不存在)→ 回退到数据库 (DB)

简单布隆过滤器的 Go 实现

package main

import (
	"fmt"
	"hash/fnv"
)

// BloomFilter holds the bit array and its size.
type BloomFilter struct {
	bitset []bool // 0/1 array
	size   int    // length of the array
}

// NewBloomFilter creates a filter with the given size.
func NewBloomFilter(size int) *BloomFilter {
	return &BloomFilter{
		bitset: make([]bool, size),
		size:   size,
	}
}

// hash1 – first hash function (FNV‑1a).
func (bf *BloomFilter) hash1(s string) int {
	h := fnv.New32a()
	h.Write([]byte(s))
	return int(h.Sum32()) % bf.size
}

// hash2 – second hash function (FNV‑1a with a salt).
func (bf *BloomFilter) hash2(s string) int {
	h := fnv.New32a()
	h.Write([]byte(s + "salt")) // simple salt to get a different hash
	return int(h.Sum32()) % bf.size
}

// Add inserts an item into the filter.
func (bf *BloomFilter) Add(item string) {
	i1 := bf.hash1(item)
	i2 := bf.hash2(item)
	bf.bitset[i1] = true
	bf.bitset[i2] = true
	fmt.Printf("Added: %s (indices %d, %d)\n", item, i1, i2)
}

// Exists checks whether an item *might* be in the set.
func (bf *BloomFilter) Exists(item string) bool {
	i1 := bf.hash1(item)
	i2 := bf.hash2(item)

	// Both bits must be true for a "maybe" answer.
	if bf.bitset[i1] && bf.bitset[i2] {
		return true // maybe
	}
	return false // definitely not
}

func main() {
	filter := NewBloomFilter(100)

	// 1. Add "Ayush"
	filter.Add("Ayush")

	// 2. Query "Ayush"
	fmt.Println("Does Ayush exist?", filter.Exists("Ayush")) // true (maybe)

	// 3. Query "Kumar"
	fmt.Println("Does Kumar exist?", filter.Exists("Kumar")) // false (definitely not)
}

实际应用

服务Bloom 过滤器的使用方式
Medium.com避免向用户推荐已经阅读过的文章。如果过滤器返回 “no”,文章会立即显示;如果返回 “maybe”,则通过数据库检查确认。
Google Chrome将 URL 与已知恶意站点列表进行比对。返回 “no” 时立即阻止访问;返回 “maybe” 时触发更深入的验证。
Cassandra、HBase、Redis(Bloom 模块)通过快速排除不存在的键,降低读密集型工作负载的磁盘 I/O。
分布式缓存(如 Memcached)通过预过滤不太可能存在的键,防止缓存未命中风暴。

要点

  • Bloom 过滤器以可控的低误报率换取 巨大的内存节省快速的成员检测
  • 它们是 写入一次(不支持删除)且在创建后 大小固定 的数据结构。
  • 使用上文公式正确地确定 mk 的大小,对于实现期望的错误率至关重要。

欢迎将示例 Go 代码移植到自己的项目中,调整哈希函数的数量,或用更强的哈希算法(如 MurmurHash、SipHash)替代简单的 FNV 哈希,以获得更好的分布效果。祝过滤愉快!

实践中的布隆过滤器

  • Web 浏览器 – 当 URL 太长或可疑时,Chrome 会将其存入布隆过滤器。

    • 访问时,Chrome 检查该过滤器。
    • 如果过滤器返回 “可能危险”,Chrome 会联系 Google 的服务器进行确认。
  • 数据库(Cassandra / PostgreSQL) – 在扫描硬盘块以查找行之前,数据库引擎会检查布隆过滤器。

    • 如果过滤器表明该行不存在,则可以避免昂贵的磁盘 I/O。

可伸缩布隆过滤器

布隆过滤器的大小(数组长度 m 和哈希函数数量 k)通常基于假设的最大项目数 n 和目标误报率 p 来选择。
当实际数据集超出原先假设时,需要对过滤器进行扩容。

可伸缩布隆过滤器 (SBF)

Redis Stack 实现了 堆叠布隆过滤器(也称为 可伸缩布隆过滤器)。
堆叠中每新增的过滤器都更大,并且 精度严格高于 前一个过滤器。

Source:

堆叠算法工作原理

A. 写入规则 – 添加数据

步骤操作
1确定 最新(活动)过滤器(例如,过滤器 3)。
2 将新元素(例如 “Ayush”)插入该活动过滤器。
3较旧的过滤器(过滤器 1、过滤器 2)保持 冻结——它们永不被修改。

为什么? 冻结的过滤器保留其原始的误报保证;只有最新的过滤器处理新插入。

B. 读取规则 – 检查数据

要回答查询 “Ayush 是否存在?”

  1. 检查过滤器 3 – 结果:(Ayush 不在最新的过滤器中)。
  2. 检查过滤器 2 – 结果:
  3. 检查过滤器 1 – 结果:(Ayush 是两个月前添加的)。

最终答案是所有检查的逻辑

Result = F3.Exists() || F2.Exists() || F1.Exists()

Naïve Stacking 的问题

  • 每增加一个过滤器,就会引入其自身的误报概率。
  • 如果每个过滤器的错误率为 1 %,两个过滤器的总体错误率约为 2 %,十个过滤器约为 10 %——这是一种不可接受的增长。

Redis 如何解决该问题

Redis 使每个后续过滤器都 更严格(误报率更低),比前一个过滤器更紧:

过滤器允许错误率
过滤器 10.1 %
过滤器 20.01 %(严格度提升 10 倍)
过滤器 30.001 %(严格度提升 100 倍)

由于后面的过滤器精度显著提升,即使堆叠 20 个过滤器,整体误报率仍远低于 1 %

要点

  • 布隆过滤器被广泛用于避免代价高昂的查找。
  • 当数据集增长时,可伸缩布隆过滤器(堆叠的、逐渐更严格的过滤器)让你在不重建整个结构的情况下保持低整体错误率。
  • Redis 的实现表明,逐步更紧的过滤器是实现实用、低错误率扩展的关键。
Back to Blog

相关文章

阅读更多 »

避免 UUIDv4 主键

引言 在过去的十年里,当使用 UUID Version 4^1 作为主键数据类型来处理数据库时,这些数据库通常表现不佳……

别再像2015年那样写API

我们已经进入2025年,仍有许多代码库把 API 视为简单的“返回 JSON 的端点”。如果你的 API 设计仍停留在基本的 CRUD 路由上,你正……