布隆过滤器(BloomFilter)

布隆过滤器是什么?

布隆过滤器(Bloom Filter)是一种空间效率极高的概率型数据结构,用来判断:一个元素一定不存在/可能存在,它不能100%确定元素存在,但可以100%确定元素不存在。

核心原理

  1. 初始化一个全0的比特数组(bit array)
  2. 插入元素:
  • 用K个不同的哈希函数计算元素的K个哈希值
  • 把比特数组对应下标的位置设为1
  1. 查询元素:
  • 同样用K个哈希函数计算下标
  • 如果任意一个下标是0,则可判断,这个元素一定不存在
  • 如果全部是1,则只能判断,这个元素可能存在

两大特性

布隆过滤器判断不存在的元素,则100%不存在;布隆过滤器判断存在的元素,则是可能存在的,假阳性,也可能是误判的,因为哈希冲突。

不支持的操作

  • 不支持删除元素,多个元素可能共享同一个比特位
  • 不支持动态扩容,扩容则需要重建过滤器

关键参数

实现布隆过滤器必须确定三个参数:

  1. m:比特数组长度,越大误判率越低
  2. k:哈希函数个数,太少误判高,太多速度慢
  3. n:预计存储的元素数量

最优公式

  • 最优哈系数:k = ln2 * m / n,约等于,0.7 * m / n
  • 误判率公式:p = (1 - e^(-kn/m)) ^ k

经验值

  • 想误判率 < 0.1% -> 每个元素分配10~14bit
  • 想误判率 < 0.01% -> 每个元素分配14~18bit

优缺点

优点

  • 极省内存:100万元素只需要约1.2MB空间
  • 速度极快:插入/查询都是O(k),k很小(3~10)
  • 无元素存储,只存比特位,隐私性好

缺点

  • 有假阳性误判
  • 不支持删除
  • 元素数量超过预估后,误判率会飙升

使用场景

  • 缓存穿透防护,判断key是否存在,不存在直接返回
  • 爬虫URL去重
  • 黑名单/白名单快速校验
  • 数据库查询前预过滤
  • 大数据去重

总结

【程序员都必须会的技术,面试必备【布隆过滤器详解】,Redis缓存穿透解决方案】https://www.bilibili.com/video/BV1zK4y1h7pA?vd_source=51bf4e3845fa5f000f98df6975f93695

布隆过滤器是为了解决缓存穿透的问题,本质是一个二进制数组,数组元素都是由0与1组成的,分别代表存在与不存在的关系,

如图,假设我们计算出最优比特数组长度为3,则一个元素"你好"的哈希值就分为了三个,Hash1、Hash2、Hash3,值分别为3 5 7,并映射到数组中的对应的位置上。

查询同理,我们需要根据元素去计算得到这个数据的哈希值,找到对应的位置,从而判断数据是否存在。

布隆过滤器元素的删除很难实现,因为不同的元素可能计算出的部分哈希值是相同的,这就导致这个位置上的1分别证明着不同的数据是存在的,如果单方面删除一个元素而导致这个位置上的数字变为0,这样就导致了另一个元素也变成了不存在。

golang实现布隆过滤器

包的命名与库的导入

复制代码
package bloom

import (
	"math"
	"math/big"

	"github.com/spaolacci/murmur3"
)

自定义实现布隆过滤器的结构体

复制代码
// SBloomFilter
// self-BloomFilter 自定义布隆过滤器结构体
type SBloomFilter struct {
	bitArray *big.Int // 比特数组
	m        uint     // 数组大小
	k        uint     // 哈希函数数量
}

其中,big.Int类型,用来存放比特位,是一个每个格子只能写0或1的数组

返回一个结构体实例,暴露结构体方法,实现面向接口编程

复制代码
// NewBloomFilter 创建布隆过滤器实例
// @param n 预计存入的元素数量
// @param p 期望的错误率
// @return *SBloomFilter
func NewBloomFilter(n uint, p float64) *SBloomFilter {
	// 计算最优数组长度m
	m := getOptimalBitArrayLength(n, p)
	// 计算最优哈希函数数量k
	k := getOptimalHashFunction(n, m)

	return &SBloomFilter{
		bitArray: big.NewInt(0),
		m:        m,
		k:        k,
	}
}

其中,n为预计存入的元素数量,比如预见向布隆过滤器中存入100万个短链接信息;p为期望的错误率,比如期望错误小于0.1%或0.01%

计算最优的比特数组长度,根据公式m = -n * ln(p) / (ln(2) * ln(2))

复制代码
// getOptimalBitArrayLength 计算最优数组长度m
// @param n 预计存入的元素数量
// @param p 期望的错误率
// @return uint 最优数组长度m
// 依据公式: m = -n * ln(p) / (ln(2) * ln(2))
func getOptimalBitArrayLength(n uint, p float64) uint {
	m := uint(-float64(n) * math.Log(p) / (math.Log(2) * math.Log(2)))
	if m == 0 {
		m = 1
	}
	return m
}

计算最优的哈希函数数量,根据公式k = m * ln(2) / n

复制代码
// getOptimalHashFunction 计算最优哈希函数数量k
// @param n 预计存入的元素数量
// @param m 最优数组长度m
// @return uint 最优哈希函数数量k
// 依据公式: k = m * ln(2) / n
func getOptimalHashFunction(n, m uint) uint {
	k := uint(float64(m) * math.Log(2) / float64(n))
	if k == 0 {
		k = 1
	}
	return k
}

计算一个元素对应的多个哈希位置

复制代码
// getHashPositions 计算元素的k个哈希下标
// @param data 元素数据
// @return []uint 元素的k个哈希下标
func (sbf *SBloomFilter) getHashPositions(data []byte) []uint {
	var positions []uint

	// 使用murmur3双哈希生成k个独立的哈希值
	hash1, hash2 := murmur3.Sum128(data)

	for i := uint(0); i < sbf.k; i++ {
		// 组合哈希 生成第i个哈希值
		combined := hash1 + uint64(i)*hash2
		// 取模得到比特位下标
		position := uint(combined % uint64(sbf.m))
		positions = append(positions, position)
	}
	return positions
}

因为,一个元素会被映射到k个不同的bit位置上,涉及到布隆过滤器规则,添加元素时,把k个位置全部设为1,查询元素时,检查k个位置是否都为1,如果全部是1,那只能说明这个元素是可能存在的,但是如果有一个位置为0,则说明这个元素是一定不存在的。

双哈希法

核心代码行:hash1, hash2 := murmur3.Sum128(data) 生成两个128位哈希值,即双哈希法

由于一个数据对应k个哈希值,这样我们就需要计算出k个不同的下标位置,就是写k个不同的哈希算法。但是缺点显而易见,需要写不同的计算哈希值的方法,代码量大,难以维护

因此双哈希值算法,算是一个比较简单、偷懒的算法,通过murmur3.Sum128方法计算出两个不同的哈希值,然后通过循环递增i,每次通过公式hash1 + uint64(i) * hash2逐个获得哈希值,当sbf.k为3时,hash1与hash2分别为100 200时,则得到的哈希值为100 300 500,取模得到比特数组下标,就是将哈希值映射到数组的下标上,与数据结构中的哈希表一样。

将元素添加到布隆过滤器中

复制代码
// AddBloomFilterElem 添加元素到布隆过滤器
// @param data 元素数据
func (sbf *SBloomFilter) AddBloomFilterElem(data []byte) {
	position := sbf.getHashPositions(data)
	for _, pos := range position {
		sbf.bitArray.SetBit(sbf.bitArray, int(pos), 1)
	}
}

添加到结构体中的字段中。

判断元素是否存在

复制代码
// IsExistElem 判断元素是否存在
// @param data 元素数据
// @return bool 元素是否存在
// false代表元素绝对不存在
// true代表元素可能存在
func (sbf *SBloomFilter) IsExistElem(data []byte) bool {
	positions := sbf.getHashPositions(data)
	for _, pos := range positions {
		if sbf.bitArray.Bit(int(pos)) == 0 {
			return false
		}
	}
	return true
}

原理就是,计算得到这个元素映射到哈希表中的位置,判断这些位置是否都是0

整体代码 部分优化

复制代码
package bloom

import (
	"fmt"
	"math"
	"math/big"
	"sync"

	"github.com/spaolacci/murmur3"
)

// SBloomFilter
// self-BloomFilter 自定义布隆过滤器结构体
type SBloomFilter struct {
	bitArray *big.Int     // 比特数组
	m        uint         // 数组大小
	k        uint         // 哈希函数数量
	mu       sync.RWMutex // 读写锁
}

// NewBloomFilter 创建布隆过滤器实例
// @param n 预计存入的元素数量
// @param p 期望的错误率
// @return *SBloomFilter
func NewBloomFilter(n uint, p float64) (*SBloomFilter, error) {
	if n == 0 || p <= 0 || p >= 1 {
		return nil, fmt.Errorf("布隆过滤器参数异常 初始化实例失败")
	}

	// 计算最优数组长度m
	m := getOptimalBitArrayLength(n, p)
	// 计算最优哈希函数数量k
	k := getOptimalHashFunction(n, m)

	return &SBloomFilter{
		bitArray: big.NewInt(0),
		m:        m,
		k:        k,
	}, nil
}

// getOptimalBitArrayLength 计算最优数组长度m
// @param n 预计存入的元素数量
// @param p 期望的错误率
// @return uint 最优数组长度m
// 依据公式: m = -n * ln(p) / (ln(2) * ln(2))
func getOptimalBitArrayLength(n uint, p float64) uint {
	m := math.Ceil(-float64(n)*math.Log(p) / (math.Ln2 * math.Ln2)) // 向上取整 避免低配位图
	if m == 0 {
		m = 1
	}
	return uint(m)
}

// getOptimalHashFunction 计算最优哈希函数数量k
// @param n 预计存入的元素数量
// @param m 最优数组长度m
// @return uint 最优哈希函数数量k
// 依据公式: k = m * ln(2) / n
func getOptimalHashFunction(n, m uint) uint {
	// 具体与math.Ceil需要进行比较一下
	k := math.Round(float64(m) * math.Ln2 / float64(n))  // 四舍五入 避免哈希函数数量为0  
	if k < 1 {
		k = 1
	}
	return uint(k)
}

// getHashPositions 计算元素的i个哈希下标
// @param data 元素数据
// @return []uint 元素的i个哈希下标
func (sbf *SBloomFilter) getHashPositions(data []byte) []uint {
	positions := make([]uint, 0, sbf.k) // 预分配容量 避免重复扩容

	// 使用murmur3双哈希生成2个独立的哈希值
	hash1, hash2 := murmur3.Sum128(data)

	for i := uint(0); i < sbf.k; i++ {
		// 后续自定义哈希值生成函数时 可以增大扰动因子 i -> i * i 让哈希值分布更均匀 避免哈希冲突增加
		combined := hash1 + uint64(i)*hash2
		// 取模得到比特位下标
		position := uint(combined % uint64(sbf.m))
		positions = append(positions, position)
	}
	return positions
}

// AddBloomFilterElem 添加元素到布隆过滤器
// @param data 元素数据
func (sbf *SBloomFilter) AddBloomFilterElem(data []byte) {
	sbf.mu.Lock() // 加写锁 防止并发修改bitArray
	defer sbf.mu.Unlock()
	
	if len(data) == 0 {  // 空数据保护
		return
	}
	
	// 后续拓展方向 记录每个数据的插入次数
	position := sbf.getHashPositions(data)
	for _, pos := range position {
		sbf.bitArray.SetBit(sbf.bitArray, int(pos), 1)
	}
}

// IsExistElem 判断元素是否存在
// @param data 元素数据
// @return bool 元素是否存在
// false代表元素绝对不存在
// true代表元素可能存在
func (sbf *SBloomFilter) IsExistElem(data []byte) bool {
	sbf.mu.RLock() // 加读锁 防止并发修改bitArray
	defer sbf.mu.RUnlock()
	
	if len(data) == 0 {  // 空数据保护
		return false
	}

	positions := sbf.getHashPositions(data)
	for _, pos := range positions {
		if sbf.bitArray.Bit(int(pos)) == 0 {
			return false
		}
	}
	return true
}
相关推荐
abcefg_h3 小时前
GORM——基础介绍与CRUD
开发语言·后端·golang
geovindu5 小时前
go:Decorator Pattern
开发语言·设计模式·golang·装饰器模式
anzhxu14 小时前
Go基础之环境搭建
开发语言·后端·golang
ILYT NCTR17 小时前
搭建Golang gRPC环境:protoc、protoc-gen-go 和 protoc-gen-go-grpc 工具安装教程
开发语言·后端·golang
叹一曲当时只道是寻常1 天前
memos-cli 安装与使用教程:将 Memos 笔记同步到本地并支持 AI 语义搜索
人工智能·笔记·golang
geovindu1 天前
go: Facade Pattern
设计模式·golang·外观模式
小众AI1 天前
Go 多账户 WebDAV 服务实现
golang
念何架构之路1 天前
图解defer
开发语言·后端·golang
我喜欢山,也喜欢海1 天前
Java和go在并发上的表现为什么不一样
java·python·golang