答面試官問:如何設計短url服務

Martist發表於2020-09-09

什麼是短url

短url, 顧名思義,就是將長網址縮短到一個很短的網址,使用者訪問這個短網址可以重定向到原本的長網址(還原)。這樣可以達到易於記憶、轉換的目的,還有隱藏連結引數,利於簡訊推廣的作用,常用於有字數限制的簡訊、微博、二維碼等場景。

錯誤答案

這個實現的思路真的是天花亂墜,此處總結幾種錯誤的實現方式來避坑。

1. 實現一個演算法,可以直接把一百個字元左右的長網址縮短到10位以內,並可以原樣還原,即可逆

不可能的。為所有可能存在的長連結實現一一對應,本身是不可能的,會出現碰撞,多個長連結對應一個短連結,所以不會可逆。

2. 實現演算法將長地址轉短地址,不實現逆運算,將短長對應關係存資料庫中

不可能的。沒有改變本質(長連結數量遠多於長連結),只要長連結夠多,必然會碰撞

3. 用一個hash演算法,生成的短連結碰撞後,在短連結後面加1,2,3

物理邏輯上可行,生產應用不可行。效率會不可控的降低,通過演算法算出來短url之後,hash碰撞後生成多個xx1,xx2,xx3….xx20…的短url,長短對應數量不可控,查詢效率會降低。

4. 隨機生成一個短地址,去資料庫查詢是否用過,用過就再隨機,直到隨機到一個沒用過的短地址,存資料庫

物理邏輯上可行,生產應用不可行。每次生成都有必要的全量查詢操作,肯定是不OK的。

5. 維護一個超級大的全量資料,預先生成超越實際生產使用的短url,介面呼叫直接頒發,同步修改短url使用狀態。

物理邏輯上可行,生產應用低可用。具體是在redis還是db裡批量生成其實是截然不同的兩種實現。

若是redis, 那麼裡面要放入全量的短url麼?否則怎麼知道這個短url到底是不是唯一的?如果全量,那對redis的可用性就要嚴格保證,為了高可用,就需要多節點同步維持全量資料,這個過程要做好不是那麼的容易; 若是db, 那麼就要有大量的併發鎖定,意味著大量讀寫,這個對資料庫也是個考驗。

正確答案

建立一個發號器,每次有一個新的長URL進來,我們就增加一,並且將新的數值返回.第一個來的url返回”www.x.cn/0",第二個返回"www.x.cn/1".

細節問題

短url的還原跳轉用301還是302?

301是永久重定向,302是臨時重定向。

短地址一經生成就不會變化,所以用301是符合http語義的。同時瀏覽器會對301請求儲存一個比較長期的快取,這樣就減輕對伺服器的壓力;而且301對於網址的SEO有一定的提升。但是很多情況下我們需要對介面點選或者使用者行為進行一些業務監控處理的時候,301明顯就不合適了(瀏覽器直接按照快取資料跳轉了), 所以很多業務場景下還是採用302比較合適。

字元超長問題

即使到了10億(Billion)轉換而成的62進位制也無非是6位字元,所以長度基本不在考慮範圍內,這個範圍足夠使用了。

對應關係如何儲存?

這個對應資料肯定是要落盤的,不能每次系統重啟就重新排號,所以可以採用mysql等資料庫來儲存.而且如果資料量小且qps低,直接使用資料庫的自增主鍵就可以實現.

如何保證長短連結一一對應?

按照上面的發號器策略,是不能保證長短連結的一一對應的,你連續用同一個URL請求兩次,結果值都是不一樣的.

為了實現長短連結一一對應,我們需要付出很大的空間代價,尤其是為了快速響應,我們可以需要在記憶體中做一層快取,這樣子太浪費了.

但是可以實現一些變種的,來實現部分的一一對應, 比如將最近/最熱門的對應關係儲存在K-V資料庫中,這樣子可以節省空間的同時,加快響應速度.

短URL的儲存

我們返回的短URL一般是將數字轉換成62進位制,這樣子可以更加有效的縮短URL長度,那麼62進位制的數字對計算機來說只是字串,怎麼儲存呢?直接儲存字串對等值查詢好找,對範圍查詢等太不友好了.

其實可以直接儲存10進位制的數字,這樣不僅佔用空間少,對查詢的支援較好,同時還可以更加方便的轉換到更多/更少的進位制來進一步縮短URL.

短碼安全問題

按照演算法從0-61都是1位字元,然後2位、3位…這樣的話很容易被人發現規律並進行攻擊,當然防禦手段很多,請求籤名之類的安全驗證手段不在本文討論範圍內。

首先計數器可以從一個比較大的隨機中間值開始,比如從10000開始計數,他的62進位制是 2Bi 3位的字串;

然後採用一些校驗位演算法(比如Luhn改進一下),計算出1位校驗位拼接起來,4位短碼,這樣可以排除一定的安全風險;

再加點安全料的話,可以在62進位制的轉換過程中把排序好的62個字母數字隨機打亂,比如ABCD1234打亂成1BC43A2D, 轉換的62進位制也就更難hack了;

最後如果仍不放心,還可以在某些位置(比如1,3,5)插入隨機數,讓人無法看出規律來也可以達到良好的效果。

同一長網址短url是否應該相同問題

發號策略中,是不判斷長地址是否已轉過,所以造成結果就是一長對多短,有人說浪費空間,建立一個長對短的map儲存即可,但是用map儲存本身就是浪費大量空間,甚至是用大空間換小空間,這就要考慮是否真有必要做一一對應,不能一對多;

最簡單方案:建一個長對短的map,空間換空間,

更好的方案:用map儲存”最近”生成的長對短關係,一小時過期機制實現LRU淘汰

長對短流程:

  1. 這個“最近”表中檢視一下,看長地址有沒有對應的短地址

  2. 有就直接返回,並且將這個key-value對的過期時間重置為一小時

  3. 如果沒有,就通過發號器生成一個短地址,並且將這個“最近”表中,過期時間為1小時

一個地址被頻繁使用,那麼它會一直在這個key-value表中,總能返回當初生成那個短地址,不會出現重複的問題。如果它使用並不頻繁,那麼長對短的key會過期,LRU機制自動就會淘汰掉它。

這樣在空間和發號數量之間取得了一個平衡,此處也應該看具體的業務需求來,是否會存在一對多的情況。比如下單未支付,給使用者發簡訊召回,簡訊內的短url裡面存在使用者暱稱,訂單號等個性化資訊,即不需要增加這一邏輯環節了。

高併發

如果直接儲存在MySQL中,當併發請求增大,對資料庫的壓力太大,可能會造成瓶頸,這時候是可以有一些優化的.

快取

上面保證長短連結一一對應中也提到過快取,這裡我們是為了加快程式處理速度.

可以將熱門的長連結(需要對長連結進來的次數進行計數),最近的長連結(可以使用redis儲存最近一個小時的)等等進行一個快取,儲存在記憶體中或者類似redis的記憶體資料庫中,如果請求的長URL命中了快取,那麼直接獲取對應的短URL進行返回,不需要再進行生成操作.

批量發號

每一次發號都需要訪問一次MySQL來獲取當前的最大號碼,並且在獲取之後更新最大號碼,這個壓力是比較大的.

我們可以每次從資料庫獲取10000個號碼,然後在記憶體中進行發放,當剩餘的號碼不足1000時,重新向MySQL請求下10000個號碼.在上一批號碼發放完了之後,批量進行寫入.

這樣可以將對資料庫持續的操作移到程式碼中進行,並且非同步進行獲取和寫入操作,保證服務的持續高併發.

分散式

上面設計的系統是有單點的,那就是發號器是個單點,容易掛掉.

可以採用分散式服務,分散式的話,如果每一個發號器進行發號之後都需要同步給其他發號器,那未必也太麻煩了.

換一種思路,可以有兩個發號器,一個發單號,一個發雙號,發號之後不再是遞增1,而是遞增2.

類比可得,我們可以用1000個服務,分別發放0-999尾號的數字,每次發號之後遞增1000.這樣做很簡單,服務互相之間基本都不用通訊,做好自己的事情就好了.

轉自

www.kancloud.cn/martist/be_new_fri...
歡迎關注哦

本作品採用《CC 協議》,轉載必須註明作者和本文連結
是非之外有一座花園,我們會在那裡相遇

相關文章