短連結服務系統開發
最近上了一個比較大的系統,基於訊息推送的需要,花了點時間做了一個短鏈服務,實現思路其實很簡單,這裡簡單介紹下實現細節,以及一些優化過程。
功能簡單描述
功能很簡單,實現將長網址縮短的功能,如:

為什麼要轉短鏈?因為要控制每條簡訊的字數,對於公司來說,簡訊裡面的字可都是錢呀。
為什麼不用 t.cn,url.cn 等短鏈服務呢,它們生成的連結不是更短嗎?是的,它們確實能實現更短的連結,可是要收錢的,而且這裡面充滿了商業資料呀。
短鏈服務總的來說,就做兩件事:
將長連結變為短連結,當然是越短越好
使用者點選短連結的時候,實現自動跳轉到原來的長連結
長鏈轉短鏈
在轉短鏈的時候,我們其實就是要將一個長長的連結對映為只有 4 到 7 個字母的字串。這裡我用了 SQL/">MySQL 來儲存,存放 short_key 和 original_url 的記錄。
資料表很簡單,最主要的列有以下幾個:
id: 邏輯主鍵,BIGINT
short_key: 短鏈中的字串,域名部分一般不需要加進去, 加入唯一索引 unique
original_url: 原長網址,限 256 字元
另外,基於業務需要,可以加入業務標識 biz、過期時間 expire_time 等。
在生成 key 的時候,一種最簡單的實現方式是使用隨機字串,因為是隨機碼,所以可能會出現失敗,通常就需要重試。隨著記錄越來越多,就越容易發生 key 重複的情況,這種方案顯然不適合資料量大的場景。
我們不容易保證我們隨機生成的 key 不重複,但是我們容易實現的就是 id 不重複,我們只要想個辦法把 id 和 key 一一對應起來就可以了。
單表場景,直接使用資料庫自增 id 就能實現 id 唯一。多庫多表,大家肯定都有一個全域性發號器來生成唯一 id。
直接將 id 放在短鏈上可以嗎?這樣就不需要使用 key 了。功能上是沒有問題的,不過問題就是還是會太長,然後由於 id 通常都是基本自增的,會引發很多問題,如被別人用一個簡單的指令碼給遍歷出來。
接下來,我們討論怎麼將 id 變為 key。
在短鏈中,我們通常可以使用的字元有 a-z、A-Z 和 0-9 共 62 個字元,所以,接下來,我們其實就是要將 10 進位制的 id 轉換為 62 進位制 的字串。
轉換方法很簡單,大家都學過二進位制和十進位制之間的轉換,這裡貼下簡單的實現:

這樣,十進位制的 id 總是能生成一個唯一的 key,同樣地,我們也可以通過 key 還原出 id。
在分庫分表的時候,我們可以選擇使用 id 來做分表鍵,也可以使用 key 來做分表鍵。如果是使用 id 的話,因為前端過來都是 key,所以需要先將 key 轉換為 id。 這裡我們將使用 key 做分表鍵 。
本文不會用到 62 進位制轉 10 進位制,不過也貼出來讓大家參考下吧:

短鏈轉長鏈
這一步非常簡單,使用者點選我們發給他們的簡訊中的短鏈,請求傳送到我們的解析系統中,我們根據 key 到資料庫中找原來的長連結,然後做個 302 跳轉就可以了。
這裡貼下 Spring MVC 的程式碼:

細節優化
1、加入隨機碼
62 進位制用更短的字串能表示更大的數,使得我們可以使用更少的字元,同時不會讓使用者直接知道我們的 id 大小,但是稍微懂一點技術的,很容易就能將 62 進位制轉換為 10 進位制,在行家眼裡,和直接使用 id 沒什麼區別。
下面,我們就來優化這部分。
首先,上面的程式碼中,我們可以打亂這個 BASE 字串,因為如果不打亂的話,那麼 62 進制中就會有 XXb = XXa + 1,如 10 進位制的 999998 和 999999 轉換為 62進位制以後,分別為 4C90 和 4C91,大家是不是發現有點不妥。
接下來,我們可以考慮加隨機字串,如固定在開頭或結尾加 2 位隨機字串,不過這樣的話,就會使得我們的短鏈活生生地加了 2 位。
這裡簡單介紹下我的做法,使得生成的 key 不那麼有規律,不那麼容易被遍歷出來。

我們得到 id 以後,先在其二進位制表示的固定位置插入隨機位。如上圖所示,從低位開始,每 5 位後面插入一個隨機位,直到高位都是 0 就不再插入。
一定要對每個 id 進行一樣的處理,一開始就確定下來固定的位置,如可以每 4 位插一個隨機位,也可以在固定第 10 位、第 17 位、第 xx 位等,這樣才能保證演算法的安全性:兩個不一樣的數,在固定位置都插入隨機位,結果一定不一樣。
由於我們會 浪費 掉一些位,所以最大可以表示的數會受影響,不過 64 位的 long 值是一個很大的數,是允許我們奢侈浪費一些的。
還有,前面提到高位為 0 就不再插入,那是為了不至於一開始就往高位插入了 1 導致我們剛開始的值就特別大,轉換出來需要更長的字串。
這裡我貼下我的插入隨機位實現:

這樣,我們 10 進位制的 999998 和 999999 就可能被轉換為 16U06 和 XpJX。因為有隨機位的存在,所以會有好幾種可能。到這裡,是不是覺得生成出來的字串就好多了,相鄰的兩個數出來的兩個字串沒什麼規律了。
另外,建議 id 從一箇中等模式的大小開始,如 100w,而不是從 1 開始,這個應該很好理解。
2、加入快取
為了提高效率,我們應該使用適當的快取,在系統中,我分別使用了一個讀快取和一個寫快取。
通常,我們使用讀快取 (key => originalUrl) 可以獲得很多好處,大家想想,如果我們往一批使用者的手機發送同一個短鏈,可能大家都是在收到簡訊的幾分鐘內開啟連結的,這個時候讀快取就能大大提高讀效能。
至於寫請求,介面來了一個 originalUrl,我們不能去資料庫中查詢是否已經有這條記錄,所以兩條一模一樣的連結我們會生成兩個不一樣的短連結,當然,通常我們也是允許這種情況的。
這裡我指的是在分庫分表的場景中,我們只能使用 key 來查詢,已經不支援使用 original_url 進行資料庫查找了。
由於存在短時間內使用兩條一模一樣的長連結拿過來轉短鏈的情況,所以我們可以維護一個寫快取 (originalUrl => key),這裡使用 originalUrl 做鍵,如設定最大允許快取最近 10000 條,過期時間 1 小時,根據自己實際情況來設定即可。這裡寫快取能不能提高效率,取決於我們的業務。
由於生成短鏈的介面一般是提供給其他各個業務系統使用的,所以其實可以由呼叫方來決定是否要使用寫快取,這樣能得到最好的效果。如果呼叫方知道自己接下來需要批量轉換的長鏈是不會重複的,那麼呼叫方可以設定不使用快取,而對於一般性的場景,預設開啟寫快取。
3、資料庫大小寫
這裡再提最後一點,也是我自己踩的坑,有點低階失誤了。一定要檢查下自己的資料表是不是大小寫敏感的。
在大小寫不敏感的情況下,3rtX 和 3Rtx 被認為是相同的。
解決辦法如下,設定列為 utf8_bin:

效能分析
這個系統非常簡單,效能瓶頸其實都集中在資料庫中,前面我們也說了可以通過快取來適當提高效能。
這裡,我們不考慮快取,來看下應該怎麼設計資料庫和表。
首先,我們應該預估一個適當的量,如按照自己的業務規模,預估接下來 2 年或更長時間,大概會增長到什麼量級的資料。
如預估未來可能需要存放 50-100 億條記錄,然後我們大概按照單表 1000w 資料來設計,那麼就需要 500-1000 張表,那麼我們可以定 512 張表,512 張表我們可以考慮放 2 個或 4 個庫。
我們使用 key 來做分表鍵,同時在 key 上加唯一索引,對於單表 1000w 這種級別,查詢效能一般都差不了。
我沒有在生產環境做過壓測,測試環境中使用單庫 2 張表,在不使用快取的情況下,寫操作可以比較輕鬆地達到 3000 TPS,基本上也就滿足我們的需求了。本來測試環境各種硬體資源就和生產環境沒法比,更何況我們生產環境會設定多庫多表來分散壓力。