使用golang怎么实现一个布谷鸟过滤器

发布时间:2021-03-01 14:27:30 作者:戴恩恩
来源:亿速云 阅读:155

本文章向大家介绍使用golang怎么实现一个布谷鸟过滤器的基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

golang适合做什么

golang可以做服务器端开发,但golang很适合做日志处理、数据打包、虚拟机处理、数据库代理等工作。在网络编程方面,它还广泛应用于web应用、API应用等领域。

实现原理

简单工作原理

可以简单的把布谷鸟过滤器里面有两个 hash 表T1、T2,两个 hash 表对应两个 hash 函数H1、H2。

具体的插入步骤如下:

下面举一个例子来说明:

使用golang怎么实现一个布谷鸟过滤器

如果想要插入一个元素Z到过滤器里:

当 Z 插入完毕之后如下:

使用golang怎么实现一个布谷鸟过滤器

布谷鸟过滤器

布谷鸟过滤器和上面的实现原理结构是差不多的,不同的是上面的数组结构会存储整个元素,而布谷鸟过滤器中只会存储元素的几个 bit ,称作指纹信息。这里是牺牲了数据的精确性换取了空间效率。

上面的实现方案中,hash 表中每个槽位只能存放一个元素,空间利用率只有50%,而在布谷鸟过滤器中每个槽位可以存放多个元素,从一维变成了二维。论文中表示:

With k = 2 hash functions, the load factor α is 50% when the bucket size b = 1 (i.e., the hash table is directly mapped), but increases to 84%, 95% or 98% respectively using bucket size b = 2, 4 or 8.

也就是当有两个 hash 函数的时候,使用一维数组空间利用率只有50%,当每个槽位可以存放2,4,8个元素的时候,空间利用率就会飙升到 84%,95%,98%。

如下图,表示的是一个二维数组,每个槽位可以存放 4 个元素,和上面的实现有所不同的是,没有采用两个数组来存放,而是只用了一个:

使用golang怎么实现一个布谷鸟过滤器

说完了数据结构的改变,下面再说说位置计算的改变。

我们在上面简单实现的位置计算公式是这样做的:

p1 = hash2(x) % 数组长度
p2 = hash3(x) % 数组长度

而布谷鸟过滤器计算位置公式可以在论文中看到是这样:

f = fingerprint(x);
i1 = hash(x);
i2 = i1 ⊕ hash( f);

我们可以看到在计算位置 i2 的时候是通过 i1 和元素 X 对应的指纹信息取异或计算出来。指纹信息在上面已经解释过了,是元素 X 的几个 bit ,牺牲了一定精度,但是换取了空间。

那么这里为什么需要用到异或呢?因为这样可以用到异或的自反性: A ⊕ B ⊕ B = A ,这样就不需要知道当前的位置是 i1 还是 i2,只需要将当前的位置和 hash(f) 进行异或计算就可以得到另一个位置。

这里有个细节需要注意的是,计算 i2 的时候是需要先将元素 X 的 fingerprint 进行 hash ,然后才取异或,论文也说明了:

If the alternate location were calculated by “i⊕fingerprint” without hashing the fingerprint, the items kicked out from nearby buckets would land close to each other in the table, if the size of the fingerprint is small compared to the table size.

如果直接进行异或处理,那么很可能 i1 和 i2 的位置相隔很近,尤其是在比较小的 hash 表中,这样无形之中增加了碰撞的概率。

除此之外还有一个约束条件是布谷鸟过滤器强制数组的长度必须是 2 的指数,所以在布谷鸟过滤器中不需要对数组的长度取模,取而代之的是取 hash 值的最后 n 位。

如一个布谷鸟过滤器中数组的长度2^8即256,那么取 hash 值的最后 n 位即: hash & 255 这样就可以得到最终的位置信息。如下最后得到位置信息是 23 :

使用golang怎么实现一个布谷鸟过滤器

代码实现

数据结构

const bucketSize = 4
type fingerprint byte
// 二维数组,大小是4
type bucket [bucketSize]fingerprint

type Filter struct {
	// 一维数组
	buckets []bucket
	// Filter 中已插入的元素
	count  uint
	// 数组buckets长度中对应二进制包含0的个数
	bucketPow uint
}

在这里我们假定一个指纹 fingerprint 占用的字节数是 1byte ,每个位置有 4 个座位。

初始化

var (
	altHash = [256]uint{}
	masks = [65]uint{}
)

func init() {
	for i := 0; i < 256; i++ {
  // 用于缓存 256 个fingerprint的hash信息
		altHash[i] = (uint(metro.Hash74([]byte{byte(i)}, 1337)))
	}
	for i := uint(0); i <= 64; i++ {
  // 取 hash 值的最后 n 位
		masks[i] = (1 << i) - 1
	}
}

这个 init 函数会缓存初始化两个全局变量 altHash 和 masks。因为 fingerprint 长度是 1byte ,所以在初始化 altHash 的时候使用一个 256 大小的数组取缓存对应的 hash 信息,避免每次都需要重新计算;masks 是用来取 hash 值的最后 n 位,稍后会用到。

我们会使用一个 NewFilter 函数,通过传入过滤器可容纳大小来获取过滤器 Filter:

func NewFilter(capacity uint) *Filter {
 // 计算 buckets 数组大小
	capacity = getNextPow2(uint64(capacity)) / bucketSize
	if capacity == 0 {
		capacity = 1
	}
	buckets := make([]bucket, capacity)
	return &Filter{
		buckets: buckets,
		count:  0,
  // 获取 buckets 数组大小的二进制中以 0 结尾的个数
		bucketPow: uint(bits.TrailingZeros(capacity)),
	}
}

NewFilter 函数会通过 getNextPow2 将 capacity 调整到 2 的指数倍,如果传入的 capacity 是 9 ,那么调用 getNextPow2 后会返回 16;然后计算好 buckets 数组长度,实例化 Filter 返回;bucketPow 返回的是二进制中以 0 结尾的个数,因为 capacity 是 2 的指数倍,所以 bucketPow 是 capacity 二进制的位数减 1。

插入元素

func (cf *Filter) Insert(data []byte) bool {
	// 获取 data 的 fingerprint 以及 位置 i1
	i1, fp := getIndexAndFingerprint(data, cf.bucketPow)
	// 将 fingerprint 插入到 Filter 的 buckets 数组中
	if cf.insert(fp, i1) {
		return true
	}
	// 获取位置 i2
	i2 := getAltIndex(fp, i1, cf.bucketPow)
	// 将 fingerprint 插入到 Filter 的 buckets 数组中
	if cf.insert(fp, i2) {
		return true
	}
	// 插入失败,那么进行循环插入踢出元素
	return cf.reinsert(fp, randi(i1, i2))
}

func (cf *Filter) insert(fp fingerprint, i uint) bool {
 // 获取 buckets 中的槽位进行插入
	if cf.buckets[i].insert(fp) {
  // Filter 中元素个数+1
		cf.count++
		return true
	}
	return false
}

func (b *bucket) insert(fp fingerprint) bool {
 // 遍历槽位的 4 个元素,如果为空则插入
	for i, tfp := range b {
		if tfp == nullFp {
			b[i] = fp
			return true
		}
	}
	return false
}

下面看看 getIndexAndFingerprint 是如何获取 fingerprint 以及槽位 i1:

func getIndexAndFingerprint(data []byte, bucketPow uint) (uint, fingerprint) {
 // 将 data 进行hash
	hash := metro.Hash74(data, 1337)
 // 取 hash 的指纹信息
	fp := getFingerprint(hash)
	// 取 hash 高32位,对 hash 的高32位进行取与获取槽位 i1
	i1 := uint(hash>>32) & masks[bucketPow]
	return i1, fingerprint(fp)
}
// 取 hash 的指纹信息
func getFingerprint(hash uint64) byte {
	fp := byte(hash%255 + 1)
	return fp
}

getIndexAndFingerprint 中对 data 进行 hash 完后会对其结果取模获取指纹信息,然后再取 hash 值的高 32 位进行取与,获取槽位 i1。masks 在初始化的时候已经看过了, masks[bucketPow] 获取的二进制结果全是 1 ,用来取 hash 的低位的值。

假如初始化传入的 capacity 是1024,那么计算到 bucketPow 是 8,对应取到 masks[8] = (1 << 8) - 1 结果是 255 ,二进制是 1111,1111 ,和 hash 的高 32 取与 得到最后 buckets 中的槽位 i1 :

使用golang怎么实现一个布谷鸟过滤器

func getAltIndex(fp fingerprint, i uint, bucketPow uint) uint {
	mask := masks[bucketPow]
	hash := altHash[fp] & mask
	return i ^ hash
}

getAltIndex 中获取槽位是通过使用 altHash 来获取指纹信息的 hash 值,然后取异或后返回槽位值。需要注意的是,这里由于异或的特性,所以传入的不管是槽位 i1,还是槽位 i2 都可以返回对应的另一个槽位。

下面看看循环踢出插入 reinsert:

const maxCuckooCount = 500

func (cf *Filter) reinsert(fp fingerprint, i uint) bool {
 // 默认循环 500 次
	for k := 0; k < maxCuckooCount; k++ {
  // 随机从槽位中选取一个元素
		j := rand.Intn(bucketSize)
		oldfp := fp
  // 获取槽位中的值 
		fp = cf.buckets[i][j]
  // 将当前循环的值插入
		cf.buckets[i][j] = oldfp

		// 获取另一个槽位
		i = getAltIndex(fp, i, cf.bucketPow)
		if cf.insert(fp, i) {
			return true
		}
	}
	return false
}

这里会最大循环 500 次获取槽位信息。因为每个槽位最多可以存放 4 个元素,所以使用 rand 随机从 4 个位置中取一个元素踢出,然后将当次循环的元素插入,再获取被踢出元素的另一个槽位信息,再调用 insert 进行插入。

使用golang怎么实现一个布谷鸟过滤器

上图展示了元素 X 在插入到 hash 表的时候,hash 两次发现对应的槽位 0 和 3 都已经满了,那么随机抢占了槽位 3 其中一个元素,被抢占的元素重新 hash 之后插入到槽位 5 的第三个位置上。

查询数据

查询数据的时候,就是看看对应的位置上有没有对应的指纹信息:

func (cf *Filter) Lookup(data []byte) bool {
 // 获取槽位 i1 以及指纹信息
	i1, fp := getIndexAndFingerprint(data, cf.bucketPow)
 // 遍历槽位中 4 个位置,查看有没有相同元素
	if cf.buckets[i1].getFingerprintIndex(fp) > -1 {
		return true
	}
 // 获取另一个槽位 i2
	i2 := getAltIndex(fp, i1, cf.bucketPow)
 // 遍历槽位 i2 中 4 个位置,查看有没有相同元素
	return cf.buckets[i2].getFingerprintIndex(fp) > -1
}

func (b *bucket) getFingerprintIndex(fp fingerprint) int {
	for i, tfp := range b {
		if tfp == fp {
			return i
		}
	}
	return -1
}

删除数据

删除数据的时候,也只是抹掉该槽位上的指纹信息:

func (cf *Filter) Delete(data []byte) bool {
 // 获取槽位 i1 以及指纹信息
	i1, fp := getIndexAndFingerprint(data, cf.bucketPow)
 // 尝试删除指纹信息
	if cf.delete(fp, i1) {
		return true
	}
 // 获取槽位 i2
	i2 := getAltIndex(fp, i1, cf.bucketPow)
 // 尝试删除指纹信息
	return cf.delete(fp, i2)
}

func (cf *Filter) delete(fp fingerprint, i uint) bool {
 // 遍历槽位 4个元素,尝试删除指纹信息
	if cf.buckets[i].delete(fp) {
		if cf.count > 0 {
			cf.count--
		}
		return true
	}
	return false
}

func (b *bucket) delete(fp fingerprint) bool {
	for i, tfp := range b {
  // 指纹信息相同,将此槽位置空
		if tfp == fp {
			b[i] = nullFp
			return true
		}
	}
	return false
}

缺点

实现完布谷鸟过滤器后,我们不妨想一下,如果布谷鸟过滤器对同一个元素进行多次连续的插入会怎样?

那么这个元素会霸占两个槽位上的所有位置,最后在插入第 9 个相同元素的时候,会一直循环挤兑,直到最大循环次数,然后返回一个 false:

使用golang怎么实现一个布谷鸟过滤器

如果插入之前做一次检查能不能解决问题呢?这样确实不会出现循环挤兑的情况,但是会出现一定概率的误判情况。

由上面的实现我们可以知道,在每个位置里设置的指纹信息是 1byte,256 种可能,如果两个元素的 hash 位置相同,指纹相同,那么这个插入检查会认为它们是相等的导致认为元素已存在。

事实上,我们可以通过调整指纹信息的保存量来降低误判情况,如在上面的实现中,指纹信息是 1byte 保存8位信息误判概率是0.03,当指纹信息增加到 2bytes 保存16位信息误判概率会降低至 0.0001。

以上就是小编为大家带来的使用golang怎么实现一个布谷鸟过滤器的全部内容了,希望大家多多支持亿速云!

推荐阅读:
  1. 使用golang怎么实现一个LRU缓存淘汰算法
  2. 使用golang怎么实现一个分页算法

免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。

golang

上一篇:计算机中将cpu集成在一块芯片上所形成的元器件是什么

下一篇:计算机网络由几个部分组成

相关阅读

您好,登录后才能下订单哦!

密码登录
登录注册
其他方式登录
点击 登录注册 即表示同意《亿速云用户服务条款》