尝试使用goroutines优化布谷过滤器

      布谷过滤器的基础知识 可以参考 布谷过滤器 这篇文章的内容学习  简单的讲

布谷过滤器(Cuckoo Filter)是一种空间效率高且支持快速插入和查找的概率数据结构,主要用于集合的成员检测。它是基于 Cuckoo Hashing(布谷哈希)的原理,具有以下几个关键特性:

1. 基本概念

  • 成员检测:布谷过滤器用于判断一个元素是否在一个集合中。它可以返回“可能在集合中”或“肯定不在集合中”的结果。
  • 空间效率:相较于传统的哈希表,布谷过滤器在存储相同数量的元素时占用更少的内存。

2. 数据结构

  • 桶(Buckets):布谷过滤器将元素存储在多个桶中,每个桶可以存储多个元素。
  • 哈希函数:使用两个或多个哈希函数来计算元素的存储位置,确保元素可以在多个位置存储。

3. 插入操作

  • 当插入一个元素时,布谷过滤器首先计算该元素的两个哈希值,确定两个可能的桶。
  • 如果其中一个桶未满,则将元素插入该桶。
  • 如果两个桶都已满,则会“踢出”一个现有元素,并尝试将其插入到另一个桶中。这一过程可能会递归进行,直到成功插入或达到最大重试次数。

4. 查找操作

  • 查找一个元素时,布谷过滤器计算该元素的哈希值,并检查对应的桶。
  • 如果在任何一个桶中找到该元素,则返回“可能在集合中”;如果在两个桶中都未找到,则返回“肯定不在集合中”。

5. 优点

  • 高效性:插入和查找操作的平均时间复杂度为O(1)。
  • 动态扩展:可以在负载因子达到一定阈值时动态扩展,增加桶的数量。

6. 缺点

  • 假阳性:布谷过滤器可能会错误地报告某个元素在集合中(假阳性),但不会错误地报告某个元素不在集合中(假阴性)。
  • 内存使用:虽然比传统哈希表更节省空间,但在高负载情况下,内存使用仍然可能较高           

综上所述 我们可以发现 布谷过滤器是一个内存密集型的数据结构 在实际应用中 布谷过滤器常常使用在 路由 数据库索引 缓存中  这些涉及到高并发请求、数据处理和实时响应的场景都可以通过协程进行优化。使用协程可以提高系统的并发处理能力,减少响应时间,从而提升整体性能和用户体验。

因此 我们也许可以考虑 使用并发编程的技巧 实现对他的优化

在下使用的是go语言的goroutines 尝试优化布谷过滤器 因为本人学艺不精 所写代码必然漏洞百出 但也希望能抛砖引玉 因为go语言属于比较小众的语言 故我会一句一句分析这些代码

package main

import (
    "crypto/sha256"
    "fmt"
    "sync"
)

 首先 是导入使用的包 sha256用以计算哈希值 fmt用以格式化输出 sync是核心 用于控制并发编程

实现互斥锁和等待组。这些工具可以帮助管理多个 goroutine 的并发执行,确保数据的一致性和安全性

const (
    bucketsize   = 20
    start = 4
    load   = 0.75 
)

定义常量   bucketsize 每个桶存储的最大元素数量 start 初始化时桶的数量 load 承载系数 当元素的数量达到桶负载量的75%时 触发动态扩展

​
type cuckoofilter struct {
    buckets [][]string
    mu      sync.RWMutex
    count   int 
}

​

建立cuckoofilter结构体 包括 buckets 一个二维切片,表示存储元素的桶   mu 读写互斥锁,用于保护对过滤器的并发访问  count 用以统计桶内元素数量

func main() {
    cf := Newone()

    var wg sync.WaitGroup
    values := []string{
        .....................一大堆元素
    }

    for _, value := range values {
        wg.Add(1)
        go func(val string) {
            defer wg.Done()
            if cf.firstinsert(val) {
                fmt.Printf("插入: %s\n", val)
            } else {
                fmt.Printf("插入失败: %s\n", val)
            }
        }(value)
    }

    wg.Wait()

  
    for _, value := range values {
        wg.Add(1)
        go func(val string) {
            defer wg.Done()
            if cf.Lookup(val) {
                fmt.Printf("找到: %s\n", val)
            } else {
                fmt.Printf("查找失败: %s\n", val)
            }
        }(value)
    }

    wg.Wait()
    for _, value := range values {
        wg.Add(1)
        go func(val string) {
            defer wg.Done()
            if cf.remove(val) {
                fmt.Printf("删除: %s\n", val)
            } else {
                fmt.Printf("删除失败: %s\n", val)
            }
        }(value)
    }

    wg.Wait()

    for _, value := range values {
        wg.Add(1)
        go func(val string) {
            defer wg.Done()
            if cf.Lookup(val) {
                fmt.Printf("在删除后仍能找到: %s\n", val)
            } else {
                fmt.Printf("在删除后当然找不到: %s\n", val)
            }
        }(value)
    }

    wg.Wait()
}

主函数 我们先看他 然后再看别的  我们也是在此处实现goroutines优化   首先创建一个 `sync.WaitGroup` 用于等待所有 goroutine 完成 保证安全性      而后 我们对values中的各个元素进行遍历 每个元素开一个协程       defer wg.Done():在 goroutine 完成时调用,减少等待组的计数  wg.wait 会阻塞主 goroutine,直到所有的插入操作完成(即所有的 goroutine 都调用了 wg.Done())。  查找和插入 删除 删除后查找 的结构是一样的所以在下就不赘述了    

接下来 是具体的 实现布谷过滤器的代码 (前方大量下饭代码预警)

func Newone() *cuckoofilter {
    buckets := make([][]string, start)
    for i := range buckets {
        buckets[i] = make([]string, 0, bucketsize)
    }
    return &cuckoofilter{buckets: buckets}
}

创建并返回一个新的 Cuckoo Filter 实例。初始化 buckets,为每个桶分配一个空的字符串切片,容量为 bucketsize。

func (cf *cuckoofilter) firstinsert(value string) bool {
    cf.mu.Lock()
    defer cf.mu.Unlock()

    if float64(cf.count)/float64(len(cf.buckets)*bucketsize) > load {
        fmt.Printf("扩大\n")
        cf.expand() 
    }

    return cf.trueinsert(value, 0)
}


func (cf *cuckoofilter) trueinsert(value string, count int) bool {
    if count >= 10 { // 防止无限循环
        return false
    }

    index1 := cf.hash1(value)
    index2 := cf.hash2(value)

    if len(cf.buckets[index1]) < bucketsize {
        cf.buckets[index1] = append(cf.buckets[index1], value)
        cf.count++
        fmt.Printf("%s插入到了%d\n", value, index1)
        return true
    }

    if len(cf.buckets[index2]) < bucketsize {
        cf.buckets[index2] = append(cf.buckets[index2], value)
        cf.count++
        fmt.Printf("%s插入到了%d\n", value, index2)
        return true
    }

    evicted := cf.buckets[index1][0]
    cf.buckets[index1][0] = value
    fmt.Printf("将%s从%d移动\n", evicted, index1)

    if cf.insert(evicted, count+1) {
        return true
    }

    return false
}

这一大段为插入函数 各位应该会疑惑 为什么插入函数要分成两个来写 因为我在主函数中使用了协程的技巧 而插入操作不仅仅是主函数 我们在后面动态扩展的时候 需要将old 内的数据复制到new 里面 而复制的这一步 没有使用协程 如果仍然直接使用firstinsert 会死锁 所以分成两个来写   我们先使用互斥锁 mu 来确保线程安全,防止并发写入 如果当前元素数量与桶的容量比超过75%,则动态扩展桶的容量  而后调用trueinsert 进行真正的插入 设定10次为上限  防止无限循环 (其实如果发现达到十次上限 就需要进行扩容了 但是后文会讲为什么做不到(因为我水平太低了))

使用 hash1 和 hash2 函数计算两个桶的索引。 

func (cf *cuckoofilter) hash1(value string) int {
    h := sha256.Sum256([]byte(value))
    return int(h[0]) % len(cf.buckets)
}

func (cf *cuckoofilter) hash2(value string) int {
    h := sha256.Sum256([]byte(value))
    return (int(h[0]) + int(h[1])) % len(cf.buckets) 
}

判断主桶和备用桶是否已满 如果主桶满了 则将元素插入第二个桶。如果两个桶都已满,则踢出第一个桶中的第一个元素,并将新元素插入。递归插入被踢出的元素:尝试将被踢出的元素插入到 Cuckoo Filter 中。 

if len(cf.buckets[index1]) < bucketsize {
        cf.buckets[index1] = append(cf.buckets[index1], value)
        cf.count++
        fmt.Printf("%s插入到了%d\n", value, index1)
        return true
    }

    if len(cf.buckets[index2]) < bucketsize {
        cf.buckets[index2] = append(cf.buckets[index2], value)
        cf.count++
        fmt.Printf("%s插入到了%d\n", value, index2)
        return true
    }

    evicted := cf.buckets[index1][0]
    cf.buckets[index1][0] = value
    fmt.Printf("将%s从%d移动\n", evicted, index1)

接下来 是 查找操作

func (cf *cuckoofilter) Lookup(value string) bool {
    cf.mu.RLock()
    defer cf.mu.RUnlock()

    index1 := cf.hash1(value)
    index2 := cf.hash2(value)

    for _, v := range cf.buckets[index1] {
        if v == value {
            fmt.Printf("在桶%d找到%s\n", value, index1)
            return true
        }
    }

    for _, v := range cf.buckets[index2] {
        if v == value {
            fmt.Printf("在桶%d找到%s\n", value, index2)
            return true
        }
    }

    fmt.Printf("哪里都找不到%s\n", value)
    return false
}

布谷鸟过滤器的查找过程很简单,给定一个项x,算法首先根据上述插入公式,计算x的指纹和两个候选桶。然后读取这两个桶:如果两个桶中的任何现有指纹匹配,则布谷鸟过滤器返回true,否则过滤器返回false。此时,只要不发生桶溢出,就可以确保没有假阴性

动态扩展(在下在测试的时候 发现 一旦调用这个函数 对过滤器动态扩展 准确度就会下载到可怕的程度 但是因为我学艺不精 不太清楚是哪里出了差错(感觉逻辑应该是对的) 所以写出来 权当抛砖引玉 求大佬指导)(目前的解决方法是先大概确定要插入的元素总数 而后直接提取设定好bucketsizes的大小)  

​
func (cf *cuckoofilter) expand() {
    old := cf.buckets
    news := make([][]string, len(old)*2)
    for i := range news {
        news[i] = make([]string, 0, bucketsize)
    }

    // 重新哈希现有元素
    for _, bucket := range old {
        for _, value := range bucket {
            cf.trueinsert(value, 0) // 重新插入到新桶中
        }
    }

    cf.buckets = news // 更新桶为新桶
}

​

将旧的桶存储在 old 中,创建一个新的桶切片 news,其长度是旧桶的两倍。

重新哈希:遍历旧桶中的所有元素,通过trueinsert(如果使用firstinsert 会出现协程死锁的错误) 将它们重新插入到新的桶中。

更新桶:将 cf.buckets 更新为新的桶。

插入 查找 扩展 这样几个基础功能就完成了(存疑)  但是既然是cuckoofilter了 我们还得把删除功能也实现了  

func (cf *cuckoofilter) remove(value string) bool {
    cf.mu.Lock()
    defer cf.mu.Unlock()

    index1 := cf.hash1(value)
    index2 := cf.hash2(value)

   
    for i, v := range cf.buckets[index1] {
        if v == value {
            cf.buckets[index1] = append(cf.buckets[index1][:i], cf.buckets[index1][i+1:]...)
            cf.count--
            fmt.Printf("将%s从%d删除\n", value, index1)
            return true
        }
    }

  
    for i, v := range cf.buckets[index2] {
        if v == value {
            cf.buckets[index2] = append(cf.buckets[index2][:i], cf.buckets[index2][i+1:]...)
            cf.count--
            fmt.Printf("将%s从%d删除\n", value, index2)
            return true
        }
    }

    fmt.Printf("我没找到%s删个毛\n", value)
    return false
}

删除的过程也很简单,检查给定项的两个候选桶;如果任何桶中的指纹匹配,则从该桶中删除匹配指纹的一份副本

好了 这样大概就写完了 在下是个小小编程萌新 这也是第一篇博客 所写代码必然充满下饭操作 求大佬多多指教 (求大佬骂轻点 ,我是下水道鼠鼠 ,骂我的话我就哭给你看(ಥ﹏ಥ)  )

  • 11
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值