短連結設計和思考

什麼是長連結、什麼是短連結?

https://www。toutiao。com/a7011319993128337953 這個地址屬於長連結。當然這個只是示例,真實場景會帶有各種引數。

https://reurl。cc/EZp7gn 這個地址是上面的長連結地址經過處理得到的,屬於短連結。

有了長連結為啥需要短連結呢?

以傳送營銷簡訊為例子,每一條簡訊字元是有上限的。

如果使用長連結很容易超過單條簡訊上限,將變成兩條簡訊,成本增加,使用者體驗也很差。

如果使用短連結,將大大減少字元數目,由於連結字元的變少,文字部分可以變多。

當然了,短連結除了經常使用在營銷簡訊場景,還被廣泛應用於生成二維碼。

短連結如何跳轉到長連結?

假設使用者收到營銷簡訊,之後點選營銷簡訊裡面的短連結,是如何跳轉到長連結的呢?

主要為三步:

一、客戶端訪問短連結地址,短連結伺服器校驗是否是自己頒發的地址。

二、短連結伺服器識別出是自己頒發的地址,302重定向到長連結地址。

三、客戶端獲取到長連結地址進行訪問。

長連結如何生成短連線?

1 雜湊演算法生成短連線

雜湊演算法可以將一個不管多長的字串,轉化成一個長度固定的雜湊值。

其中比較著名並且應用廣泛的一個雜湊演算法,那就是 MurmurHash 演算法。該演算法計算速度快和衝突機率小。

https://www。toutiao。com/a7011319993128337953 透過 32bits MurmurHash 演算法得到雜湊值 445113871。

將雜湊值拼接域名很容易得到,短網址:https://reurl。cc/EZp7gn

由於 Google 的 guava 包有 MurmurHash 演算法的實現。這塊附上 guava 的依賴。

com。google。guava guava 19。0

能不能讓短網址更短呢?

可以的,445113871 是十進位制表示,我們可以選擇更高進製表示。業內用得最多的是62進位制。

短連結設計和思考

為什麼要用62進位制?64進位制不行麼?

62進位制只含數字+小寫字母+大寫字母;64進位制會含有“/,+”這樣的符號,不符合正常URL的字元。而且如果是6位62進位制的話,能有560億種組合,滿足業務需求。

我們都知道在使用雜湊演算法的時候有可能會出現衝突。所謂的衝突就是不同的長連結地址雜湊會生成相同的短連結。使用者透過短連結不知道訪問哪個長連結。

如何解決雜湊衝突問題呢?

用 MySQL 儲存短連結和長連結的對映關係,短連結欄位加上唯一索引。

一 當有一個新長連結需要生成短連結的時候,透過 MurmurHash 演算法,62進位制轉換,生成短連結。

二 在對映表中透過短連結查詢,如果沒有找到短連結,將短連結和長連結對映關係寫入表中。

三 如果對映表有記錄,將對映表中得到的長連結和新長連結進行對比,如果相同。直接使用短連結。

四 如果對映表得到的長連結和新長連結不同,出現了雜湊衝突。這個時候我們可以給新長連結拼接特定字元再進行雜湊。

五 這個特定字元可以自定義,比如[short_url],需要注意的是資料庫寫入的加上特定字元的,給客戶端返回的時候需要去掉特定字元。

2 ID生成器生成短連結

我們維護一個 ID 自增生成器。它可以生成 1、2、3…這樣自增的整數 ID。

一 當短連結服務接收到一個長連結轉成短連結請求之後,它先從 ID 生成器中取一個 id。

二 將 id 轉化成 62 進位制,將得到的結果拼接到短連結域名後面,得到最終的短連結。

三 將生成的短連結和對應的長連結儲存到資料庫中。

這塊有一個問題,假設相同的長連結兩次來生成短連結,這塊會生成兩個不同的短連結。

從使用者的角度來看,是不影響使用者使用的,因為不管是哪個短連結,最終都是一個長連結。

從儲存的角度來看,如果請求N次,這塊的資料儲存壓力是很大的,而且很多都是髒資料。需要新增唯一索引。

10進位制和62進位制轉換程式碼

import org。apache。commons。lang3。StringUtils;public class ConversionUtils { /** * 初始化 62 進位制資料,索引位置代表字元的數值,比如 A代表10,z代表61等 */ private static String chars = “0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz”; private static int scale = 62; /** * 將數字轉為62進位制 * * @param num Long 型數字 * @param length 轉換後的字串長度,不足則左側補0 * @return 62進位制字串 */ public static String encode(long num, int length) { StringBuilder sb = new StringBuilder(); int remainder = 0; while (num > scale - 1) { /** * 對 scale 進行求餘,然後將餘數追加至 sb 中,由於是從末位開始追加的,因此最後需要反轉(reverse)字串 */ remainder = Long。valueOf(num % scale)。intValue(); sb。append(chars。charAt(remainder)); num = num / scale; } sb。append(chars。charAt(Long。valueOf(num)。intValue())); String value = sb。reverse()。toString(); return StringUtils。leftPad(value, length, ‘0’); } /** * 62進位制字串轉為數字 * * @param str 編碼後的62進位制字串 * @return 解碼後的 10 進位制字串 */ public static long decode(String str) { /** * 將 0 開頭的字串進行替換 */ str = str。replace(“^0*”, “”); long num = 0; int index = 0; for (int i = 0; i < str。length(); i++) { /** * 查詢字元的索引位置 */ index = chars。indexOf(str。charAt(i)); /** * 索引位置代表字元的數值 */ num += (long) (index * (Math。pow(scale, str。length() - i - 1))); } return num; }}