布隆过滤器
1、场景说明
垃圾邮件过滤系统
在一个大型的电子邮件服务提供商中,每天需要处理数以亿计的邮件。为了维护用户的安全和减少垃圾邮件的干扰,系统需要高效地识别并过滤掉来自黑名单中的垃圾邮件发送者的邮件。假设该服务提供商维护了一个包含10亿个黑名单email地址的数据库。
问题:如果每次接收邮件时都直接查询这个庞大的数据库,将会消耗大量的计算资源和时间,严重影响邮件的接收和过滤速度。
在线内容平台的用户评论审核
一个大型在线内容平台,如社交媒体或新闻网站,每天都会接收到海量的用户评论。为了维护平台的良好秩序,需要对包含恶意言论、广告链接或来自已知恶意用户的评论进行过滤。假设平台维护了一个包含10亿个恶意用户ID或email地址的黑名单。
问题:直接查询如此庞大的黑名单数据库将极大影响评论的审核速度和用户体验。
网络安全领域的IP地址黑名单
在网络安全领域,为了防范恶意攻击和流量,需要维护一个包含恶意IP地址的黑名单。假设这个黑名单包含了10亿个IP地址。
问题:当网络流量巨大时,如何高效地过滤掉来自黑名单中IP地址的数据包,以防止它们对系统造成损害?
2、解决方案说明
2.1 使用布隆过滤器
方案描述:
对于所有提到的场景,都可以采用布隆过滤器来优化黑名单的查询效率。
- 构建布隆过滤器:首先,将黑名单中的所有元素(如email地址、用户ID、IP地址)通过多个哈希函数映射到布隆过滤器的一个大型位数组中,并将这些位置上的值设为1。
- 查询过程:当需要判断一个新元素是否可能存在于黑名单中时,同样使用该元素通过相同的哈希函数计算得到的索引位置,并检查这些位置上的值是否都为1。如果所有位置都为1,则判断该元素可能存在于黑名单中(注意这里存在误判的可能性);如果有任何一个位置为0,则确定该元素不在黑名单中。
优点
- 空间效率高:相比于传统的数据结构如哈希表(Hash Table)或集合(Set),布隆过滤器在表示大量元素时能够显著减少存储空间的使用。这是因为布隆过滤器不直接存储元素本身,而是利用位数组(Bit Array)和多个哈希函数来标记元素的存在性。
- 查询时间快:布隆过滤器的查询时间复杂度通常接近O(1),即常数时间复杂度。这意味着无论集合中有多少元素,查询操作所需的时间都保持相对稳定,非常适合处理大规模数据集的快速检索问题。
- 保密性强:由于布隆过滤器不存储元素本身,而只是通过哈希函数将元素映射到位数组中的特定位置,因此它在某些对保密性要求较高的应用场景中具有优势。
缺点
- 存在误判率:布隆过滤器的最大缺点是其存在一定的误判率。即,即使某个元素实际上不在集合中,布隆过滤器也可能因为哈希冲突而错误地判断该元素存在。虽然可以通过增加哈希函数的数量或位数组的大小来降低误判率,但无法完全消除。
- 无法获取元素本身:布隆过滤器只能判断元素是否可能存在于集合中,而无法获取元素本身的值。这意味着它不能用于需要检索元素具体内容的场景。
- 删除元素困难:由于布隆过滤器使用位数组来标记元素的存在性,并且多个元素可能共享相同的位位置,因此删除集合中的元素变得非常困难。目前有一些变种的布隆过滤器支持元素的删除操作,但通常需要额外的存储空间和计算开销。
2.2 分布式缓存 + 数据库
方案描述(不直接使用布隆过滤器,但同样高效):
- 使用分布式缓存:将黑名单中的元素存储在一个高性能的分布式缓存系统中(如Redis、Memcached等)。这样可以在内存中快速完成查询,而无需每次都访问磁盘上的数据库。
- 缓存更新策略:定期或根据需求更新缓存中的黑名单数据,确保缓存中的数据与数据库中的数据保持一致。
- 查询过程:当需要判断一个元素是否在黑名单中时,首先查询分布式缓存。如果缓存中存在该元素,则直接返回结果;如果缓存中不存在,则再查询数据库,并将查询结果更新到缓存中。
优点
- 提高性能:分布式缓存能够显著减少数据库的访问次数,因为缓存系统通常具有更快的响应速度和更高的并发处理能力。对于读多写少的应用场景,分布式缓存能够大幅提升数据访问性能。
- 减轻数据库压力:通过将高频访问的数据存储在缓存中,可以减轻数据库的负担,避免数据库因为过多的访问请求而性能下降或宕机。
- 可扩展性:分布式缓存系统通常具有良好的可扩展性,可以通过增加缓存节点来应对不断增长的数据访问需求。
- 高可用性:分布式缓存系统通常采用冗余部署和故障转移机制,即使某个缓存节点出现故障,也能够保证数据的可用性和系统的稳定性。
缺点
- 数据一致性问题:分布式缓存和数据库之间的数据同步可能存在延迟,导致缓存中的数据与数据库中的数据不一致。这需要通过合适的数据同步策略和缓存失效策略来解决。
- 缓存击穿:当缓存中没有数据(缓存未命中)而用户大量并发请求同一个数据时,这些请求都会穿透到数据库,造成数据库压力过大甚至宕机。这可以通过设置热点数据永不过期、加互斥锁或布隆过滤器等方式来避免。
- 缓存雪崩:当大量缓存数据同时失效或缓存系统出现故障时,所有请求都会直接打到数据库上,造成数据库压力过大甚至宕机。这可以通过设置缓存过期时间随机化、缓存预热和降级策略等方式来避免。
- 成本较高:分布式缓存系统需要额外的硬件资源来部署和维护,同时还需要专业的技术人员进行管理和优化。此外,缓存数据的网络传输也需要消耗一定的带宽资源。
- 复杂度增加:引入分布式缓存后,系统的架构变得更加复杂,需要处理缓存和数据库之间的数据同步、缓存失效和更新等问题。同时,还需要对缓存的性能和可用性进行监控和调优。
2.3 Trie树(前缀树) + 数据库
方案描述(针对特定场景,如email或IP地址黑名单):
- 构建Trie树:对于email地址或IP地址这样的数据,可以利用Trie树(前缀树)来优化查询效率。Trie树可以存储字符串的公共前缀,从而减少不必要的比较次数。
- 存储与查询:将黑名单中的所有email地址或IP地址构建成Trie树,并将树结构存储在内存中或高效的数据结构中。查询时,只需遍历Trie树,即可快速判断一个元素是否存在于黑名单中。
- 数据库备份:虽然Trie树提供了快速的查询效率,但为了数据的持久化和完整性,还需要将黑名单数据存储在数据库中。Trie树中的数据可以定期从数据库中同步更新。
优点
- 高效查询
- Trie树的核心优势在于其高效的字符串查询能力。通过利用字符串的公共前缀,Trie树能够显著减少无谓的字符串比较,从而加快查询速度。
- 节省空间(相对字符串存储)
- Trie树仅存储字符串中的字符和必要的指针,避免了存储整个字符串的冗余。这对于存储大量具有公共前缀的字符串特别有效。
- 自动完成和前缀匹配
- Trie树非常适合实现自动完成和前缀匹配功能。在搜索引擎、文本编辑器、命令行界面等应用中,这种功能可以显著提高用户体验。
- 灵活性和可扩展性
- Trie树的结构使其能够轻松支持字符串的插入、删除和修改操作,这对于需要频繁更新数据的场景非常有用。
- 同时,Trie树还可以与不同的数据库技术结合使用,以适应不同的应用场景和数据需求。
- 数据排序和去重
- Trie树可以对插入的字符串进行自动排序,并有效去除重复项。这有助于保持数据的一致性和准确性。
缺点
- 实现复杂度较高
- Trie树的实现相对复杂,需要处理节点的插入、删除、查询等多种操作。同时,为了保持树的平衡和性能,还需要进行额外的优化和调整。
- 与数据库的整合难度
- 将Trie树与数据库结合使用需要解决数据同步、一致性保证、查询优化等多个问题。这需要开发人员具备较高的技术水平和丰富的经验。
- 性能瓶颈
- 在大规模数据集上,Trie树的性能可能会受到内存限制和查询复杂度的影响。此外,如果数据库查询优化不当,也可能成为整个系统的性能瓶颈。
- 对短字符串效率不高
- 当处理的字符串较短时,Trie树的效率优势可能不明显。因为短字符串的公共前缀较少,Trie树在减少字符串比较方面的效果有限。
3、布隆过滤器图解
4、手写简单demo
import java.util.BitSet;/*** 布隆过滤器 demo*/
public class BloomFilterDemo {int size;BitSet bits;public BloomFilterDemo(int size){this.size = size;bits = new BitSet(size);}public void add(String data){int hash_1 = hash_1(data);int hash_2 = hash_2(data);int hash_3 = hash_3(data);bits.set(hash_1);bits.set(hash_2);bits.set(hash_3);}public boolean find(String data){int hash_1 = hash_1(data);int hash_2 = hash_2(data);int hash_3 = hash_3(data);return bits.get(hash_1) && bits.get(hash_2) && bits.get(hash_3);}/*** hash算法1* @param key* @return*/public int hash_1(String key) {int hash = 0;int i;for (i = 0; i < key.length(); ++i) {hash = 33 * hash + key.charAt(i);}return Math.abs(hash) % size;}/*** hash算法2* @param key* @return*/public int hash_2(String key) {final int p = 16777619;int hash = (int) 2166136261L;for (int i = 0; i < key.length(); i++) {hash = (hash ^ key.charAt(i)) * p;}hash += hash << 13;hash ^= hash >> 7;hash += hash << 3;hash ^= hash >> 17;hash += hash << 5;return Math.abs(hash) % size;}/*** hash算法3* @param key* @return*/public int hash_3(String key) {int hash, i;for (hash = 0, i = 0; i < key.length(); ++i) {hash += key.charAt(i);hash += (hash << 10);hash ^= (hash >> 6);}hash += (hash << 3);hash ^= (hash >> 11);hash += (hash << 15);return Math.abs(hash) % size;}public static void main(String[] args) {BloomFilterDemo bloomFilterDemo = new BloomFilterDemo(Integer.MAX_VALUE); // 21亿数据bloomFilterDemo.add("百度");bloomFilterDemo.add("谷歌");bloomFilterDemo.add("Edge");System.out.println("find [百度] = " + bloomFilterDemo.find("百度"));System.out.println("find [火狐] = " + bloomFilterDemo.find("火狐"));System.out.println("find [谷歌] = " + bloomFilterDemo.find("谷歌"));}
}
5、谷歌布隆过滤器
<dependency><groupId>com.google.guava</groupId><artifactId>guava</artifactId><version>33.0.0-jre</version>
</dependency>
import com.google.common.hash.BloomFilter;
import com.google.common.hash.Funnels;public class GoogleBloomFilterTest {public static void main(String[] args) {// 预估元素个数int size = 10000000;// 误判率double error_rate = 0.001;// 创建布隆过滤器BloomFilter<Integer> bloomFilter = BloomFilter.create(Funnels.integerFunnel(), size, error_rate);long start = System.currentTimeMillis();for(int i = 0 ; i < 10000000 ; i ++) {bloomFilter.put(i);}System.out.println((System.currentTimeMillis() - start) + ":ms");int t = 0 ;for(int i = 20000000 ; i < 30000000 ; i++) {if(bloomFilter.mightContain(i)) { //表示存在t++;}}System.out.println("误判的个数:" + t);}
}
执行结果
5327:ms
误判的个数:10060