5.Guava 布隆过滤器去重
布隆过滤器(Bloom Filter)是1970年由布隆提出的。它实际上是一个很长的二进制向量和一系列随机映射函数。布隆过滤器可以用于检索一个元素是否在一个集合中。它的优点是空间效率和查询时间都远远超过一般的算法,缺点是有一定的误识别率和删除困难。
布隆过滤器的核心实现是一个超大的位数组和几个哈希函数,假设位数组的长度为 m,哈希函数的个数为 k。
以上图为例,具体的操作流程:假设集合里面有 3 个元素 {x, y, z},哈希函数的个数为 3。首先将位数组进行初始化,将里面每个位都设置位 0。对于集合里面的每一个元素,将元素依次通过 3 个哈希函数进行映射,每次映射都会产生一个哈希值,这个值对应位数组上面的一个点,然后将位数组对应的位置标记为 1,查询 W 元素是否存在集合中的时候,同样的方法将 W 通过哈希映射到位数组上的 3 个点。如果 3 个点的其中有一个点不为 1,则可以判断该元素一定不存在集合中。反之,如果 3 个点都为 1,则该元素可能存在集合中。注意:此处不能判断该元素是否一定存在集合中,可能存在一定的误判率。可以从图中可以看到:假设某个元素通过映射对应下标为 4、5、6 这 3 个点。虽然这 3 个点都为 1,但是很明显这 3 个点是不同元素经过哈希得到的位置,因此这种情况说明元素虽然不在集合中,也可能对应的都是 1,这是误判率存在的原因。
我们可以借助 Google 提供的 Guava 框架来操作布隆过滤器,实现我们先在 pom.xml 中添加 Guava 的引用,配置如下:
<!-- 添加 Guava 框架 --> <!-- https://mvnrepository.com/artifact/com.google.guava/guava --> <dependency> <groupId>com.google.guava</groupId> <artifactId>guava</artifactId> <version>28.2-jre</version> </dependency>
URL 判重的实现代码:
public class URLRepeat { // 待去重 URL public static final String[] URLS = { "www.apigo.cn", "www.baidu.com", "www.apigo.cn" }; public static void main(String[] args) { // 创建一个布隆过滤器 BloomFilter<String> filter = BloomFilter.create( Funnels.stringFunnel(Charset.defaultCharset()), 10, // 期望处理的元素数量 0.01); // 期望的误报概率 for (int i = 0; i < URLS.length; i++) { String url = URLS[i]; if (filter.mightContain(url)) { // 用重复的 URL System.out.println("URL 已存在了:" + url); } else { // 将 URL 存储在布隆过滤器中 filter.put(url); } } } }
以上程序的执行结果为:
URL 已存在了:www.apigo.cn
6.Redis 布隆过滤器去重
除了 Guava 的布隆过滤器,我们还可以使用 Redis 的布隆过滤器来实现 URL 判重。在使用之前,我们先要确保 Redis 服务器版本大于 4.0(此版本以上才支持布隆过滤器),并且开启了 Redis 布隆过滤器功能才能正常使用。
以 Docker 为例,我们来演示一下 Redis 布隆过滤器安装和开启,首先下载 Redis 的布隆过器,然后再在重启 Redis 服务时开启布隆过滤器,如下图所示:
布隆过滤器使用布隆过滤器正常开启之后,我们先用 Redis 的客户端 redis-cli 来实现一下布隆过滤器 URL 判重了,实现命令如下:
在 Redis 中,布隆过滤器的操作命令不多,主要包含以下几个:
- bf.add 添加元素;
- bf.exists 判断某个元素是否存在;
- bf.madd 添加多个元素;
- bf.mexists 判断多个元素是否存在;
- bf.reserve 设置布隆过滤器的准确率。
接下来我们使用代码来演示一下 Redis 布隆过滤器的使用:
import redis.clients.jedis.Jedis; import utils.JedisUtils; import java.util.Arrays; public class BloomExample { // 布隆过滤器 key private static final String _KEY = "URLREPEAT_KEY"; // 待去重 URL public static final String[] URLS = { "www.apigo.cn", "www.baidu.com", "www.apigo.cn" }; public static void main(String[] args) { Jedis jedis = JedisUtils.getJedis(); for (int i = 0; i < URLS.length; i++) { String url = URLS[i]; boolean exists = bfExists(jedis, _KEY, url); if (exists) { // 重复的 URL System.out.println("URL 已存在了:" + url); } else { bfAdd(jedis, _KEY, url); } } } /** * 添加元素 * @param jedis Redis 客户端 * @param key key * @param value value * @return boolean */ public static boolean bfAdd(Jedis jedis, String key, String value) { String luaStr = "return redis.call('bf.add', KEYS[1], KEYS[2])"; Object result = jedis.eval(luaStr, Arrays.asList(key, value), Arrays.asList()); if (result.equals(1L)) { return true; } return false; } /** * 查询元素是否存在 * @param jedis Redis 客户端 * @param key key * @param value value * @return boolean */ public static boolean bfExists(Jedis jedis, String key, String value) { String luaStr = "return redis.call('bf.exists', KEYS[1], KEYS[2])"; Object result = jedis.eval(luaStr, Arrays.asList(key, value), Arrays.asList()); if (result.equals(1L)) { return true; } return false; } }
以上程序的执行结果为:
URL 已存在了:www.apigo.cn
总结
本文介绍了 6 种 URL 去重的方案,其中 Redis Set、Redis 布隆过滤器、数据库和唯一索引这 4 种解决方案适用于分布式系统,如果是海量的分布式系统,建议使用 Redis 布隆过滤器来实现 URL 去重,如果是单机海量数据推荐使用 Guava 的布隆器来实现 URL 去重。