1. 程式人生 > >URL 去重的 6 種方案!(附詳細實現程式碼)

URL 去重的 6 種方案!(附詳細實現程式碼)

URL 去重在我們日常工作中和麵試中很常遇到,比如這些: ![mj.png](https://cdn.nlark.com/yuque/0/2020/png/92791/1598431969308-51cbe384-f054-4f68-a2ba-f0036bc3cbd4.png#align=left&display=inline&height=674&margin=%5Bobject%20Object%5D&name=mj.png&originHeight=674&originWidth=1081&size=157864&status=done&style=none&width=1081) 可以看出,包括阿里,網易雲、優酷、作業幫等知名網際網路公司都出現過類似的面試題,而且和 URL 去重比較類似的,如 IP 黑/白名單判斷等也經常出現在我們的工作中,所以我們本文就來“盤一盤”URL 去重的問題。 ### URL 去重思路 在不考慮業務場景和資料量的情況下,我們可以使用以下方案來實現 URL 的重複判斷: 1. 使用 Java 的 Set 集合,根據新增時的結果來判斷 URL 是否重複(新增成功表示 URL 不重複); 1. 使用 Redis 中的 Set 集合,根據新增時的結果來判斷 URL 是否重複; 1. 將 URL 都儲存在資料庫中,再通過 SQL 語句判斷是否有重複的 URL; 1. 把資料庫中的 URL 一列設定為唯一索引,根據新增時的結果來判斷 URL 是否重複; 1. 使用 Guava 的布隆過濾器來實現 URL 判重; 1. 使用 Redis 的布隆過濾器來實現 URL 判重。 以上方案的具體實現如下。 ### URL 去重實現方案 #### 1.使用 Java 的 Set 集合判重 Set 集合天生具備不可重複性,使用它只能儲存值不相同的元素,如果值相同新增就會失敗,因此我們可以通過新增 Set 集合時的結果來判定 URL 是否重複,實現程式碼如下: ```java public class URLRepeat { // 待去重 URL public static final String[] URLS = { "www.apigo.cn", "www.baidu.com", "www.apigo.cn" }; public static void main(String[] args) { Set set = new HashSet(); for (int i = 0; i < URLS.length; i++) { String url = URLS[i]; boolean result = set.add(url); if (!result) { // 重複的 URL System.out.println("URL 已存在了:" + url); } } } } ``` 程式的執行結果為: > URL 已存在了:www.apigo.cn 從上述結果可以看出,使用 Set 集合可以實現 URL 的判重功能。 #### 2.Redis Set 集合去重 使用 Redis 的 Set 集合的實現思路和 Java 中的 Set 集合思想思路是一致的,都是利用 Set 的不可重複性實現的,我們先使用 Redis 的客戶端 redis-cli 來實現一下 URL 判重的示例: ![image.png](https://cdn.nlark.com/yuque/0/2020/png/92791/1598435436806-c233ce2f-621c-4d88-994b-dec0919c3eb7.png#align=left&display=inline&height=128&margin=%5Bobject%20Object%5D&name=image.png&originHeight=256&originWidth=856&size=34664&status=done&style=none&width=428) 從上述結果可以看出,當新增成功時表示 URL 沒有重複,但新增失敗時(結果為 0)表示此 URL 已經存在了。 我們再用程式碼的方式來實現一下 Redis 的 Set 去重,實現程式碼如下: ```java // 待去重 URL public static final String[] URLS = { "www.apigo.cn", "www.baidu.com", "www.apigo.cn" }; @Autowired RedisTemplate redisTemplate; @RequestMapping("/url") public void urlRepeat() { for (int i = 0; i < URLS.length; i++) { String url = URLS[i]; Long result = redisTemplate.opsForSet().add("urlrepeat", url); if (result == 0) { // 重複的 URL System.out.println("URL 已存在了:" + url); } } } ``` 以上程式的執行結果為: >
URL 已存在了:www.apigo.cn 以上程式碼中我們藉助了 Spring Data 中的 `RedisTemplate` 實現的,在 Spring Boot 專案中要使用 `RedisTemplate` 物件我們需要先引入 `spring-boot-starter-data-redis` 框架,配置資訊如下: ```xml org.springframework.boot spring-boot-starter-data-redis ``` 然後需要再專案中配置 Redis 的連線資訊,在 application.properties 中配置如下內容: ```xml spring.redis.host=127.0.0.1 spring.redis.port=6379 #spring.redis.password=123456 # Redis 伺服器密碼,有密碼的話需要配置此項 ``` 經過以上兩個步驟之後,我們就可以在 Spring Boot 的專案中正常的使用 `RedisTemplate` 物件來操作 Redis 了。 ![image.png](https://cdn.nlark.com/yuque/0/2020/png/92791/1599035814465-aef0a172-647c-4a51-8112-352bcd925188.png#align=left&display=inline&height=224&margin=%5Bobject%20Object%5D&name=image.png&originHeight=448&originWidth=1157&size=48376&status=done&style=none&width=578.5) #### 3.資料庫去重 我們也可以藉助資料庫實現 URL 的重複判斷,首先我們先來設計一張 URL 的儲存表,如下圖所示: ![image.png](https://cdn.nlark.com/yuque/0/2020/png/92791/1599027426946-5b30b22a-a56d-436d-8354-abd4dfbfe2ff.png#align=left&display=inline&height=156&margin=%5Bobject%20Object%5D&name=image.png&originHeight=312&originWidth=364&size=24629&status=done&style=none&width=182) 此表對應的 SQL 如下: ```sql /*==============================================================*/ /* Table: urlinfo */ /*==============================================================*/ create table urlinfo ( id int not null auto_increment, url varchar(1000), ctime date, del boolean, primary key (id) ); /*==============================================================*/ /* Index: Index_url */ /*==============================================================*/ create index Index_url on urlinfo ( url ); ``` 其中 `id` 為自增的主鍵,而 `url`  欄位設定為索引,設定索引可以加快查詢的速度。 我們先在資料庫中新增兩條測試資料,如下圖所示: ![image.png](https://cdn.nlark.com/yuque/0/2020/png/92791/1599028194479-707349eb-326c-465e-afb3-c7f06d08cc33.png#align=left&display=inline&height=75&margin=%5Bobject%20Object%5D&name=image.png&originHeight=149&originWidth=569&size=14402&status=done&style=none&width=284.5) 我們使用 SQL 語句查詢,如下圖所示: ![image.png](https://cdn.nlark.com/yuque/0/2020/png/92791/1599028326635-99193677-0431-46fe-adf0-bbdb0a3d1af2.png#align=left&display=inline&height=138&margin=%5Bobject%20Object%5D&name=image.png&originHeight=275&originWidth=762&size=20354&status=done&style=none&width=381) 如果結果大於 0 則表明已經有重複的 URL 了,否則表示沒有重複的 URL。 #### 4.唯一索引去重 我們也可以使用資料庫的唯一索引來防止 URL 重複,它的實現思路和前面 Set 集合的思想思路非常像。 首先我們先為欄位 URL 設定了唯一索引,然後再新增 URL 資料,如果能新增成功則表明 URL 不重複,反之則表示重複。 建立唯一索引的 SQL 實現如下: ```sql create unique index Index_url on urlinfo ( url ); ``` #### 5.Guava 布隆過濾器去重 布隆過濾器(Bloom Filter)是1970年由布隆提出的。它實際上是一個很長的二進位制向量和一系列隨機對映函式。布隆過濾器可以用於檢索一個元素是否在一個集合中。它的優點是空間效率和查詢時間都遠遠超過一般的演算法,缺點是有一定的誤識別率和刪除困難。 布隆過濾器的核心實現是一個超大的位陣列和幾個雜湊函式,假設位陣列的長度為 m,雜湊函式的個數為 k。 ![](https://cdn.nlark.com/yuque/0/2020/png/92791/1599029518111-9d3b9b6d-afa5-4094-86f1-49f47ac5c64b.png#align=left&display=inline&height=344&margin=%5Bobject%20Object%5D&originHeight=344&originWidth=952&size=0&status=done&style=none&width=952) 以上圖為例,具體的操作流程:假設集合裡面有 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 的引用,配置如下: ```xml
com.google.guava guava 28.2-jre ``` URL 判重的實現程式碼: ```java 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 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 服務時開啟布隆過濾器,如下圖所示: ![image.png](https://cdn.nlark.com/yuque/0/2020/png/92791/1599019177762-9b196496-5904-4250-9ef7-4ed33ed02eb8.png#align=left&display=inline&height=395&margin=%5Bobject%20Object%5D&name=image.png&originHeight=789&originWidth=1386&size=163950&status=done&style=none&width=693) **布隆過濾器使用** 布隆過濾器正常開啟之後,我們先用 Redis 的客戶端 redis-cli 來實現一下布隆過濾器 URL 判重了,實現命令如下: ![image.png](https://cdn.nlark.com/yuque/0/2020/png/92791/1599018770213-7810064c-5cd8-4c35-ab05-16c9f2c75515.png#align=left&display=inline&height=168&margin=%5Bobject%20Object%5D&name=image.png&originHeight=336&originWidth=965&size=70116&status=done&style=none&width=482.5) > 在 Redis 中,布隆過濾器的操作命令不多,主要包含以下幾個: > - bf.add 新增元素; > - bf.exists 判斷某個元素是否存在; > - bf.madd 新增多個元素; > - bf.mexists 判斷多個元素是否存在; > - bf.reserve 設定布隆過濾器的準確率。 接下來我們使用程式碼來演示一下 Redis 布隆過濾器的使用: ```java 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 去重**。