引言
相信大家在生活中,特別是最近的雙十一活動期間,會收到很多簡訊,而那些簡訊都有兩個特徵,第一個是幾乎都是垃圾簡訊,這個特點此處可以忽略不計,第二個特點是連結很短,比如下面這個:
我們知道,簡訊有些是有字數限制的,直接放一個帶滿各種引數的連結,不合適,另外一點是,不想暴露引數。好處無非以下:
- 太長的連結容易被限制長度
- 短連結看著簡潔,長連結看著容易懵
- 安全,不想暴露引數
- 可以統一連結轉換,當然也可以實現統計點選次數等操作
那背後的原理是什麼呢?怎麼實現的?讓你實現這樣的系統,你會怎麼設計呢?【來自於某鵝場面試官】
短連結的原理
短連結展示的邏輯
這裡最重要的知識點是重定向,先複習一下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
都已經實現了,不過計算還是比較耗時的,不如加個欄位存起來,於是資料庫變成了:
id | key | url |
---|---|---|
27095455234 | tzHLFw | https://gd.10086.cn/gmccapp/webpage/payPhonemoney/index.html?channel= |
但是這樣還是很容易被猜出這個id
和key
的對應關係,要是被遍歷訪問,那還是很不安全的,如果擔心,可以隨機將短連結的字元順序打亂,或者在適當的位置加上一些隨機生成的字元,比如第1,4,5
位是隨機字元,其他位置不變,只要我們計算的時候,將它對應的關係存到資料庫,我們就可以通過連線的key
找到對應的url
。(值得注意的是,key
必須是全域性唯一的,如果衝突,必須重新生成)
一般短連結都有過期時間,那麼我們也必須在資料庫裡面加上對應的欄位,訪問的時候,先判斷是否過期,過期則不給予重定向。
效能考慮
如果有很多短連結暴露出去了,資料庫裡面資料很多,這個時候可以考慮使用快取優化,生成的時候順便把快取寫入,然後讀取的時候,走快取即可,因為一般短連結和長連結的關係不會修改,即使修改,也是很低頻的事情。
如果系統的id
用完了怎麼辦?這種概率很小,如果真的發生,可以重用舊的已經失效的id
號。
如果被人瘋狂請求一些不存在的短連結怎麼辦?其實這就是快取穿透,快取穿透是指,快取和資料庫都沒有的資料,被大量請求,比如訂單號不可能為-1
,但是使用者請求了大量訂單號為-1
的資料,由於資料不存在,快取就也不會存在該資料,所有的請求都會直接穿透到資料庫。如果被惡意使用者利用,瘋狂請求不存在的資料,就會導致資料庫壓力過大,甚至垮掉。
針對這種情況,一般可以用布隆過濾器過濾掉不存在的資料請求,但是我們這裡id
本來就是遞增且有序的,其實我們範圍大致都是已知的,更加容易判斷,超出的肯定不存在,或者請求到的時候,快取裡面放一個空物件也是沒有問題的。
【作者簡介】:
秦懷,公眾號【秦懷雜貨店】作者,技術之路不在一時,山高水長,縱使緩慢,馳而不息。個人寫作方向:Java原始碼解析
,JDBC
,Mybatis
,Spring
,redis
,分散式
,劍指Offer
,LeetCode
等,認真寫好每一篇文章,不喜歡標題黨,不喜歡花裡胡哨,大多寫系列文章,不能保證我寫的都完全正確,但是我保證所寫的均經過實踐或者查詢資料。遺漏或者錯誤之處,還望指正。