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) \]这种设计保证了给定一个元素和其当前位置,其“替代”位置是唯一且确定的。它有效地打破了简单哈希冲突的对称性,从而提高了踢出过程的效率和稳定性,减少了陷入无限循环的风险。
工作流程:
- 添加元素 (Insert):
- 计算元素
x
的指纹f
。 - 计算两个可能的桶位置:
idx1 = h1(x)
和idx2 = h2(x)
。 - 尝试插入:
- 如果
idx1
或idx2
对应的桶中有空位,就将f
插入到第一个有空位的桶中。 - 如果两个桶都满了,布谷鸟过滤器会从
idx1
或idx2
中的一个桶中随机“踢出”一个已存在的指纹。被踢出的指纹会尝试移动到它的另一个备用位置。这个“踢动”过程会重复进行,直到所有指纹都找到位置,或者达到预设的最大踢动次数。如果达到最大踢动次数仍无法插入,过滤器可能需要扩容。
- 如果
- 计算元素
- 查询元素 (Lookup):
- 计算元素
y
的指纹f'
。 - 计算两个可能的桶位置:
idx1 = h1(y)
和idx2 = h2(y)
。 - 检查:
- 如果指纹
f'
存在于idx1
桶中,或者存在于idx2
桶中,那么布谷鸟过滤器判断元素y
“可能存在”。 - 如果
f'
在两个桶中都不存在,那么布谷鸟过滤器判断元素y
“一定不存在”。
- 如果指纹
- 计算元素
- 删除元素 (Delete):
- 计算元素
z
的指纹f''
。 - 计算两个可能的桶位置:
idx1 = h1(z)
和idx2 = h2(z)
。 - 删除:
- 在
idx1
桶和idx2
桶中查找指纹f''
。 - 如果找到,将其删除。
- 在
- 计算元素
2. 相较于布隆过滤器的优势与劣势
优势:
- 支持删除: 这是布谷鸟过滤器最重要的改进。由于每个指纹只可能存储在两个特定位置中的一个,所以可以精确地定位并移除它,而不会影响其他元素的判断。这使得它适用于需要动态更新集合的场景(例如缓存去重、会话管理)。
- 更低的误报率(在某些情况下): 在相同空间利用率下,布谷鸟过滤器在达到较高负载因子时,误报率可能比布隆过滤器更低。
- 更好的空间效率(在某些情况下): 特别是在低负载因子(即未填满)时,它通常比布隆过滤器更节省空间。当负载因子变高时,为了维持性能可能需要扩容。
- 哈希函数的数量固定: 通常只需要两个哈希函数,简化了参数选择。
劣势:
- 实现复杂度更高: 比布隆过滤器复杂,涉及到“踢动”和扩容机制。
- 插入可能失败: 在极少数情况下,如果“踢动”链过长,或者达到最大踢动次数,插入操作可能会失败,需要扩容(重建整个过滤器),这会带来性能开销。
- 仍然存在误报: 像布隆过滤器一样,布谷鸟过滤器也存在误报(因为指纹可能冲突),但不会有漏报。
3. Redis 中的布谷鸟过滤器
布谷鸟过滤器不是 Redis 的核心数据类型,而是通过 RedisBloom 模块提供。
常用命令 (RedisBloom):
- 创建布谷鸟过滤器:
CF.RESERVE mycf 100000 0.001
mycf
: 过滤器名称。100000
: 期望容量。0.001
: 期望的误报率。- 注意: 与布隆过滤器不同,
CF.RESERVE
的容量和误报率参数位置可能不同,具体以模块文档为准。
- 添加元素:
CF.ADD mycf "itemA"
CF.ADD mycf "itemB"
- 检查元素是否存在:
CF.EXISTS mycf "itemA"
-> 1 (可能存在)CF.EXISTS mycf "itemC"
-> 0 (一定不存在) - 删除元素:
CF.DEL mycf "itemA"
-> 1 (删除成功)CF.EXISTS mycf "itemA"
-> 0 (已被删除)
4. 典型应用场景
布谷鸟过滤器适用于布隆过滤器能处理的场景,并且额外适用于需要支持元素删除的场景:
- 缓存去重: 维护一个缓存中已存在的 key 集合,当缓存过期或被逐出时,从过滤器中删除对应的 key,以避免缓存穿透。
- 会话管理: 记录活跃的用户会话 ID,当会话过期时,可以从过滤器中移除。
- 限制请求频率: 记录一段时间内某个 IP 地址或用户 ID 的请求,并支持过期和移除。
- 黑白名单管理: 维护需要动态更新的黑名单或白名单。
总的来说,布谷鸟过滤器是布隆过滤器的一个强大替代品,特别是在需要动态增删元素的场景中,它提供了更好的灵活性和功能性。