面試官說:你來設計一個短連結生成系統吧

第十六封發表於2021-12-04

引言

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

20211110222405

我們知道,簡訊有些是有字數限制的,直接放一個帶滿各種引數的連結,不合適,另外一點是,不想暴露引數。好處無非以下:

  • 太長的連結容易被限制長度
  • 短連結看著簡潔,長連結看著容易懵
  • 安全,不想暴露引數
  • 可以統一連結轉換,當然也可以實現統計點選次數等操作

那背後的原理是什麼呢?怎麼實現的?讓你實現這樣的系統,你會怎麼設計呢?【來自於某鵝場面試官】

短連結的原理

短連結展示的邏輯

這裡最重要的知識點是重定向,先複習一下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/tzHLFwhttps://gd.10086.cn/gmccapp/webpage/payPhonemoney/index.html?channel=之間的裝換是怎麼樣的呢?前面路徑不變,變化的是後面,也就是tzHLFwgmccapp/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));
    }
}

id62位的key 或者key裝換成為id都已經實現了,不過計算還是比較耗時的,不如加個欄位存起來,於是資料庫變成了:

id key url
27095455234 tzHLFw https://gd.10086.cn/gmccapp/webpage/payPhonemoney/index.html?channel=

但是這樣還是很容易被猜出這個idkey的對應關係,要是被遍歷訪問,那還是很不安全的,如果擔心,可以隨機將短連結的字元順序打亂,或者在適當的位置加上一些隨機生成的字元,比如第1,4,5 位是隨機字元,其他位置不變,只要我們計算的時候,將它對應的關係存到資料庫,我們就可以通過連線的key找到對應的url。(值得注意的是,key必須是全域性唯一的,如果衝突,必須重新生成)

一般短連結都有過期時間,那麼我們也必須在資料庫裡面加上對應的欄位,訪問的時候,先判斷是否過期,過期則不給予重定向。

效能考慮

如果有很多短連結暴露出去了,資料庫裡面資料很多,這個時候可以考慮使用快取優化,生成的時候順便把快取寫入,然後讀取的時候,走快取即可,因為一般短連結和長連結的關係不會修改,即使修改,也是很低頻的事情。

如果系統的id用完了怎麼辦?這種概率很小,如果真的發生,可以重用舊的已經失效的id號。

如果被人瘋狂請求一些不存在的短連結怎麼辦?其實這就是快取穿透,快取穿透是指,快取和資料庫都沒有的資料,被大量請求,比如訂單號不可能為-1,但是使用者請求了大量訂單號為-1的資料,由於資料不存在,快取就也不會存在該資料,所有的請求都會直接穿透到資料庫。如果被惡意使用者利用,瘋狂請求不存在的資料,就會導致資料庫壓力過大,甚至垮掉。

針對這種情況,一般可以用布隆過濾器過濾掉不存在的資料請求,但是我們這裡id本來就是遞增且有序的,其實我們範圍大致都是已知的,更加容易判斷,超出的肯定不存在,或者請求到的時候,快取裡面放一個空物件也是沒有問題的。

【作者簡介】
秦懷,公眾號【秦懷雜貨店】作者,技術之路不在一時,山高水長,縱使緩慢,馳而不息。個人寫作方向:Java原始碼解析JDBCMybatisSpringredis分散式劍指OfferLeetCode等,認真寫好每一篇文章,不喜歡標題黨,不喜歡花裡胡哨,大多寫系列文章,不能保證我寫的都完全正確,但是我保證所寫的均經過實踐或者查詢資料。遺漏或者錯誤之處,還望指正。

劍指Offer全部題解PDF

2020年我寫了什麼?

開源程式設計筆記

相關文章