Cuckoo Filter

Cuckoo Filter

布谷鸟过滤器是一种概率型数据结构,它的设计初衷是为了改进传统的布隆过滤器,主要特点是:它在保持布隆过滤器高空间效率和快速查询能力的同时,增加了对元素删除的支持,并且在某些情况下,其空间效率可能比布隆过滤器更高。

1. 布谷鸟过滤器的基本原理

布谷鸟过滤器基于布谷鸟哈希 (Cuckoo Hashing) 的概念。布谷鸟哈希是一种解决哈希冲突的技术,它允许每个元素有两个(或更多)可能的存储位置。当一个新元素要插入时,如果其首选位置已被占用,它会“踢走”占据该位置的现有元素,被踢走的元素则尝试移动到它的另一个备用位置,这个过程可能会链式地继续下去,直到所有元素找到合适的位置,或者达到最大“踢动”次数,此时需要重建哈希表或扩容。

布谷鸟过滤器借鉴了这种“踢动”的思想,但它存储的不是完整的元素,而是每个元素的指纹(fingerprint)

  • 指纹 (Fingerprint): 元素经过哈希计算后得到的一个短小的二进制值,代表了元素的某种“摘要”。
  • 桶 (Buckets): 过滤器内部由多个“桶”组成,每个桶可以存储固定数量的指纹(通常是 2 个或 4 个)。
  • 哈希函数: 通常使用两个哈希函数,h1(x)h2(x)。对于一个元素 x,它有两个可能的桶位置。h1(x) 指向第一个桶,而 h2(x) 则通过 h1(x)x 的指纹计算得到,指向第二个桶。这种设计使得 h2(x) 可以从 h1(x) 和指纹推导出来,从而在存储时只需保存指纹。

为什么说 h2(x) 则通过 h1(x)x 的指纹计算得到,指向第二个桶,这样设计有什么好处?

这种设计的好处主要体现在以下几个方面:

内存效率和空间利用率:

  • 减少哈希函数数量: 这种设计只需要一个“独立”的哈希函数来计算 \(h_1(x)\)\(h_2(x)\) 实际上是从 \(h_1(x)\) 和指纹派生出来的。这避免了需要两个完全独立的、高质量的哈希函数,简化了设计。
  • 利用指纹信息: 指纹本身是元素 \(x\) 的一个压缩表示。通过将其与 \(h_1(x)\) 结合,可以有效地利用指纹的熵,在不引入额外独立哈希函数的情况下,生成一个看起来足够随机的第二个位置。

避免冲突对称性(Symmetry Avoidance):

  • 如果使用两个独立的哈希函数 \(h_1(x)\)\(h_2(x)\),并且两个位置都存储了相同的指纹,那么在发生“踢出”(eviction)操作时,可能会出现一个问题:当元素 \(A\) 被从 \(h_1(A)\) 踢出并尝试移动到 \(h_2(A)\) 时,如果 \(h_2(A)\) 恰好等于另一个元素 \(B\)\(h_1(B)\),并且 \(A\)\(B\) 的指纹相同,那么踢出操作可能会陷入循环。

  • 通过 \(h_2(x) = h_1(x) \oplus \text{hash}(\text{fingerprint}(x))\) 这种设计,两个候选位置 \(h_1(x)\)\(h_2(x)\) 之间就建立了一种不对称的关联。如果一个元素 \(x\) 被踢出其当前位置 \(i\),它会尝试移动到另一个位置 \(j\),其中 \(j = i \oplus \text{hash}(\text{fingerprint}(x))\)。这意味着:

    • 从位置 \(h_1(x)\) 踢出时,它会尝试移动到 \(h_2(x)\)

    • 从位置 \(h_2(x)\) 踢出时,它会尝试移动到 \(h_1(x)\),因为

    • \[h_2(x) \oplus \text{hash}(\text{fingerprint}(x)) = (h_1(x) \oplus \text{hash}(\text{fingerprint}(x))) \oplus \text{hash}(\text{fingerprint}(x)) = h_1(x) \]

  • 这种设计保证了给定一个元素和其当前位置,其“替代”位置是唯一且确定的。它有效地打破了简单哈希冲突的对称性,从而提高了踢出过程的效率和稳定性,减少了陷入无限循环的风险。

工作流程:

  1. 添加元素 (Insert):
    • 计算元素 x 的指纹 f
    • 计算两个可能的桶位置:idx1 = h1(x)idx2 = h2(x)
    • 尝试插入:
      • 如果 idx1idx2 对应的桶中有空位,就将 f 插入到第一个有空位的桶中。
      • 如果两个桶都满了,布谷鸟过滤器会从 idx1idx2 中的一个桶中随机“踢出”一个已存在的指纹。被踢出的指纹会尝试移动到它的另一个备用位置。这个“踢动”过程会重复进行,直到所有指纹都找到位置,或者达到预设的最大踢动次数。如果达到最大踢动次数仍无法插入,过滤器可能需要扩容。
  2. 查询元素 (Lookup):
    • 计算元素 y 的指纹 f'
    • 计算两个可能的桶位置:idx1 = h1(y)idx2 = h2(y)
    • 检查:
      • 如果指纹 f' 存在于 idx1 桶中,或者存在于 idx2 桶中,那么布谷鸟过滤器判断元素 y “可能存在”
      • 如果 f' 在两个桶中都不存在,那么布谷鸟过滤器判断元素 y “一定不存在”
  3. 删除元素 (Delete):
    • 计算元素 z 的指纹 f''
    • 计算两个可能的桶位置:idx1 = h1(z)idx2 = h2(z)
    • 删除:
      • idx1 桶和 idx2 桶中查找指纹 f''
      • 如果找到,将其删除。

2. 相较于布隆过滤器的优势与劣势

优势:

  • 支持删除: 这是布谷鸟过滤器最重要的改进。由于每个指纹只可能存储在两个特定位置中的一个,所以可以精确地定位并移除它,而不会影响其他元素的判断。这使得它适用于需要动态更新集合的场景(例如缓存去重、会话管理)。
  • 更低的误报率(在某些情况下): 在相同空间利用率下,布谷鸟过滤器在达到较高负载因子时,误报率可能比布隆过滤器更低。
  • 更好的空间效率(在某些情况下): 特别是在低负载因子(即未填满)时,它通常比布隆过滤器更节省空间。当负载因子变高时,为了维持性能可能需要扩容。
  • 哈希函数的数量固定: 通常只需要两个哈希函数,简化了参数选择。

劣势:

  • 实现复杂度更高: 比布隆过滤器复杂,涉及到“踢动”和扩容机制。
  • 插入可能失败: 在极少数情况下,如果“踢动”链过长,或者达到最大踢动次数,插入操作可能会失败,需要扩容(重建整个过滤器),这会带来性能开销。
  • 仍然存在误报: 像布隆过滤器一样,布谷鸟过滤器也存在误报(因为指纹可能冲突),但不会有漏报。

3. Redis 中的布谷鸟过滤器

布谷鸟过滤器不是 Redis 的核心数据类型,而是通过 RedisBloom 模块提供。

常用命令 (RedisBloom):

  1. 创建布谷鸟过滤器: CF.RESERVE mycf 100000 0.001
    • mycf: 过滤器名称。
    • 100000: 期望容量。
    • 0.001: 期望的误报率。
    • 注意: 与布隆过滤器不同,CF.RESERVE 的容量和误报率参数位置可能不同,具体以模块文档为准。
  2. 添加元素: CF.ADD mycf "itemA" CF.ADD mycf "itemB"
  3. 检查元素是否存在: CF.EXISTS mycf "itemA" -> 1 (可能存在) CF.EXISTS mycf "itemC" -> 0 (一定不存在)
  4. 删除元素: CF.DEL mycf "itemA" -> 1 (删除成功) CF.EXISTS mycf "itemA" -> 0 (已被删除)

4. 典型应用场景

布谷鸟过滤器适用于布隆过滤器能处理的场景,并且额外适用于需要支持元素删除的场景:

  • 缓存去重: 维护一个缓存中已存在的 key 集合,当缓存过期或被逐出时,从过滤器中删除对应的 key,以避免缓存穿透。
  • 会话管理: 记录活跃的用户会话 ID,当会话过期时,可以从过滤器中移除。
  • 限制请求频率: 记录一段时间内某个 IP 地址或用户 ID 的请求,并支持过期和移除。
  • 黑白名单管理: 维护需要动态更新的黑名单或白名单。

总的来说,布谷鸟过滤器是布隆过滤器的一个强大替代品,特别是在需要动态增删元素的场景中,它提供了更好的灵活性和功能性。

reference

实现一个布谷鸟过滤器
Cuckoo Filter:设计与实现

posted @ 2025-06-14 23:14  光風霽月  阅读(52)  评论(0)    收藏  举报