引言
相信大家在生活中,特别是最近的雙十一活動期間,會收到很多短信,而那些短信都有兩個特征,第一個是幾乎都是垃圾短信,這個特點此處可以忽略不計,第二個特點是連結很短,比如下面這個:

我們知道,短信有些是有字數限制的,直接放一個帶滿各種參數的連結,不合适,另外一點是,不想暴露參數。好處無非以下:
- 太長的連結容易被限制長度
- 短連結看着簡潔,長連結看着容易懵
- 安全,不想暴露參數
- 可以統一連結轉換,當然也可以實作統計點選次數等操作
那背後的原理是什麼呢?怎麼實作的?讓你實作這樣的系統,你會怎麼設計呢?【來自于某鵝場面試官】
短連結的原理
短連結展示的邏輯
這裡最重要的知識點是重定向,先複習一下
http
的狀态碼:
分類 | 含義 |
---|---|
1** | 伺服器收到請求,需要請求者繼續執行操作 |
2** | 成功,操作被成功接收并處理 |
3** | 重定向,需要進一步的操作以完成請求 |
4** | 用戶端錯誤,請求包含文法錯誤或無法完成請求 |
5** | 伺服器錯誤,伺服器在處理請求的過程中發生了錯誤 |
那麼以 3 開頭的狀态碼都是關于重定向的:
- 300:多種選擇,可以在多個位置存在
- 301:永久重定向,浏覽器會緩存,自動重定向到新的位址
- 302:臨時重定向,用戶端還是會繼續使用舊的URL
- 303:檢視其他的位址,類似于301
- 304:未修改。所請求的資源未修改,伺服器傳回此狀态碼時,不會傳回任何資源。
- 305:需要使用代理才能通路到資源
- 306:廢棄的狀态碼
- 307:臨時重定向,使用Get請求重定向
整個跳轉的流程:
- 1.使用者通路短連結,請求到達伺服器
- 2.伺服器将短連結裝換成為長連結,然後給浏覽器傳回重定向的狀态碼301/302
- 301永久重定向會導緻浏覽器緩存重定向位址,短連結系統統計通路次數會不正确
- 302臨時重定向可以解決次數不準的問題,但是每次都會到短連結系統轉換,伺服器壓力會變大。
- 3.浏覽器拿到重定向的狀态碼,以及真正需要通路的位址,重定向到真正的長連結上。
從下圖可以看出,确實連結被
302
重定向到新的位址上去,傳回的頭裡面有一個字段
Location
就是所要重定向的位址:
短連結怎麼設計的?
全局發号器
肯定我們第一點想到的是壓縮,像檔案壓縮那樣,壓縮之後再解壓還原到原來的連結,重定向到原來的連結,但是很不幸的是,這個是行不通的,你有見過什麼壓縮方式能把這麼長的數字直接壓縮到這麼短麼?事實上不可能。就像是
Huffman
樹,也隻能對那種重複字元較多的字元串壓縮時效率較高,像連結這種,可能帶很多參數,而且各種不規則的情況都有,直接壓縮算法不現實。
那
https://dx.10086.cn/tzHLFw
與
https://gd.10086.cn/gmccapp/webpage/payPhonemoney/index.html?channel=
之間的裝換是怎麼樣的呢?前面路徑不變,變化的是後面,也就是
tzHLFw
gmccapp/webpage/payPhonemoney/index.html?channel=
之間的轉換。
實際也很簡單,就是資料庫裡面的一條資料,一個
id
對應長連結(相當于全局的發号器,全局唯一的ID):
id | url |
---|---|
1 | https://gd.10086.cn/gmccapp/webpage/payPhonemoney/index.html?channel= |
這裡用到的,也就是我們之前說過的分布式全局唯一ID,如果我們直接用
id
作為參數,貌似也可以:
https://dx.10086.cn/1
,通路這個連結時,去資料庫查詢獲得真正的url,再重定向。
單機的唯一
ID
很簡單,用原子類
AtomicLong
就可以,但是分布式的就不行了,簡單點可以用
redis
,或者資料庫自增,或者可以考慮
Zookeeper
之類的。
id 轉換政策
但是直接用遞增的數字,有兩個壞處:
- 數字很大的時候,還是很長
- 遞增的數字,不安全,規律性太強了
明顯我們平時看到的連結也不是數字的,一般都是大小寫字母加上數字。為了縮短連結的長度,我們必須把
id
轉換掉,比如我們的短連結由
a-z
,
A-Z
0-9
組成,相當于
62
進制的數字,将
id
轉換成為
62
進制的數字:
public class ShortUrl {
private static final String BASE = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
public static String toBase62(long num) {
StringBuilder result = new StringBuilder();
do {
int i = (int) (num % 62);
result.append(BASE.charAt(i));
num /= 62;
} while (num > 0);
return result.reverse().toString();
}
public static long toBase10(String str) {
long result = 0;
for (int i = 0; i < str.length(); i++) {
result = result * 62 + BASE.indexOf(str.charAt(i));
}
return result;
}
public static void main(String[] args) {
// tzHLFw
System.out.println(toBase10("tzHLFw"));
System.out.println(toBase62(27095455234L));
}
}
id
轉
62
位的
key
或者
key
裝換成為
id
都已經實作了,不過計算還是比較耗時的,不如加個字段存起來,于是資料庫變成了:
key | ||
---|---|---|
27095455234 | tzHLFw |
但是這樣還是很容易被猜出這個
id
和
key
的對應關系,要是被周遊通路,那還是很不安全的,如果擔心,可以随機将短連結的字元順序打亂,或者在适當的位置加上一些随機生成的字元,比如第
1,4,5
位是随機字元,其他位置不變,隻要我們計算的時候,将它對應的關系存到資料庫,我們就可以通過連接配接的
key
找到對應的
url
。(值得注意的是,
key
必須是全局唯一的,如果沖突,必須重新生成)
一般短連結都有過期時間,那麼我們也必須在資料庫裡面加上對應的字段,通路的時候,先判斷是否過期,過期則不給予重定向。
性能考慮
如果有很多短連結暴露出去了,資料庫裡面資料很多,這個時候可以考慮使用緩存優化,生成的時候順便把緩存寫入,然後讀取的時候,走緩存即可,因為一般短連結和長連結的關系不會修改,即使修改,也是很低頻的事情。
如果系統的
id
用完了怎麼辦?這種機率很小,如果真的發生,可以重用舊的已經失效的
id
号。
如果被人瘋狂請求一些不存在的短連結怎麼辦?其實這就是緩存穿透,緩存穿透是指,緩存和資料庫都沒有的資料,被大量請求,比如訂單号不可能為
-1
,但是使用者請求了大量訂單号為
-1
的資料,由于資料不存在,緩存就也不會存在該資料,所有的請求都會直接穿透到資料庫。如果被惡意使用者利用,瘋狂請求不存在的資料,就會導緻資料庫壓力過大,甚至垮掉。
針對這種情況,一般可以用布隆過濾器過濾掉不存在的資料請求,但是我們這裡
id
本來就是遞增且有序的,其實我們範圍大緻都是已知的,更加容易判斷,超出的肯定不存在,或者請求到的時候,緩存裡面放一個空對象也是沒有問題的。
【作者簡介】:
秦懷,公衆号【秦懷雜貨店】作者,技術之路不在一時,山高水長,縱使緩慢,馳而不息。個人寫作方向:
Java源碼解析
,
JDBC
Mybatis
Spring
redis
分布式
劍指Offer
LeetCode
等,認真寫好每一篇文章,不喜歡标題黨,不喜歡花裡胡哨,大多寫系列文章,不能保證我寫的都完全正确,但是我保證所寫的均經過實踐或者查找資料。遺漏或者錯誤之處,還望指正。
劍指Offer全部題解PDF
2020年我寫了什麼?
開源程式設計筆記