短鏈接系統的設計與實現

 

目錄

1:什麼是短鏈接

2:為什麼需要短鏈接

3:系統的設計與目標

4:系統API的設計

5:資料庫設計

6:系統的實現

7:結語

 

1:什麼是短鏈接

短鏈接顧名思義,就是一個比較短的鏈接,我們平時看到的鏈接可能由於一些其他的問題導致我們的鏈接比較長,可能長這樣:

//www.baidu.com/s?wd=%E4%B8%8A%E6%B5%B7%E8%87%B4%E5%85%A8%E5%B8%82%E4%BA%BA%E6%B0%91%E7%9A%84%E6%84%9F%E8%B0%A2%E4%BF%A1&tn=baidutop10&rsv_idx=2&ie=utf-8&rsv_pq=abb53d500004deb0&oq=%E7%88%86%E6%BB%A1!%E4%B8%8A%E6%B5%B7%E5%B0%8F%E5%90%83%E5%BA%97%E9%81%87%E8%BF%9E%E5%A4%9C%E6%8A%A5%E5%A4%8D%E6%80%A7%E6%B6%88%E8%B4%B9&rsv_t=46b1xbgK2MjS8qj9hu%2B06QusLsL5QfltAGjtfKUgayO%2BvSC0MajShrNGroi3h1s5eQ&rqid=abb53d500004deb0&rsf=33e47e97990129c1c0bb42a6c85e13b9_1_15_1&rsv_dl=0_right_fyb_pchot_20811&sa=0_right_fyb_pchot_20811

如果我們需要將某個鏈接發在某個文章或者推廣給別人的時候,這麼長的鏈接不僅會給人一種喧賓奪主的感覺,而且影響我們整篇文章的排版和美感,而短鏈接的出現就是用一個很短的URL來替代這個很長的鏈接,當用戶訪問短鏈接的時候,會重定向到原來的鏈接。比如縮短之後的鏈接下面這樣:shorturl.at/fnDQS,我們也可能會注意到一些商業推廣的簡訊都會轉成一些很短的鏈接的形式。

2:為什麼需要短鏈接

URL短鏈接用於為長URL創建較短的別名,我們稱這些縮短的別名為「短鏈接」;當用戶點擊這些短鏈接時,會被重定向到原始URL;短鏈接在顯示、列印、發送消息時可節省大量空間。我們也可以基於這些短鏈接來統計鏈接訪問量等資訊。

URL縮寫經常用於優化設備之間的鏈接,跟蹤單個鏈接以分析受眾,衡量廣告活動的表現,或隱藏關聯的原始URL。

如果你以前沒有使用過短鏈接相關的系統,可以嘗試使用//www.shorturl.at創建一個短鏈接並訪問,並花一些時間瀏覽一下他們的服務提供的各種選項。可以讓你更好的理解短鏈接的應用場景。

3:系統的設計與目標

我們的短鏈接系統總共需要滿足的目標有兩方面功能性需求和非功能性需求

1:功能性需求

  • 給定一個URL,我們的服務應該為其生成一個較短且唯一的別名,這叫做短鏈接,此鏈接應足夠短,以便於複製和粘貼到應用程式中;

  • 當用戶訪問短鏈接時,我們的服務應該將他們重定向到原始鏈接;

  • 用戶即使輸入的是相同的長鏈接,生成的短鏈接也應該是不相同的;

  • 鏈接可以在指定時間跨度之後過期,用戶應該能夠指定過期時間;

  • 可以統計短鏈接的訪問次數;

2:非功能性需求

  • 系統必須高可用。

  • URL重定向的延遲情況應該足夠小;

  • 短鏈接應該是不可猜測的。

4:系統API的設計

本著系統介面易用性和擴展性的要求,我們設計介面是應考慮如下幾個問題,介面職責單一性,擴展性,安全性,冪等性,可閱讀性。結合以上幾個問題考慮,設計如下兩個API,創建短鏈接的API和訪問短鏈接的API,根據職責單一性和可閱讀性,我們明確的定義的介面的名稱,參數列表,返回值等資訊,介面調用失敗返回錯誤資訊和錯誤碼,而且每一個介面都會有明確的使用文檔。由於系統要求即使是相同的長鏈接,創建短鏈接也會得到不同的短鏈接,所以我們創建短鏈接的介面不做冪等處理,而訪問短鏈接的介面只是做重定向和訪問統計功能,天生冪等。在考慮介面的安全性,我們可以在創建短鏈接的API上加上介面防刷策略等,在訪問短鏈接的API上根據短鏈接做布隆過濾器,短鏈接不存在的請求快速失敗等。

1:創建短鏈接

1.1:API

@PostMapping
public String createUrlMapping(@RequestBody CreateUrlMappingCmd createUrlMappingCmd)

1.2:介面參數

 class CreateUrlMappingCmd {
   /**
    * 原始的長鏈接
    */
   private String originUrl;
}

1.3:介面返回值

成功生成短鏈接將返回短鏈接URL;否則,將返回錯誤程式碼。

2:訪問短鏈接

2.1:API

@GetMapping("{shortUrl}")
public String redirect(@PathVariable String shortUrl)

2.2:介面參數

創建短鏈接介面返回的短鏈接

2.3:介面返回值

介面重定向到長鏈接,無返回值

5:資料庫設計

由於業務邏輯相對簡單,所以數據持久化如果使用關係資料庫的話,這裡僅使用一張表就可以解決存儲問題。所以這裡的問題重點是我們要考慮數據量的問題。

5.1:數據表設計

create table url_mapping
(
  id             bigserial                                                           not null primary key,
  short_url       char(6)      default ''                                             not null,
  origin_url      varchar(200) default ''                                             not null,
  start_timestamp timestamp    default current_timestamp::timestamp without time zone not null,
  end_timestamp   timestamp    default current_timestamp::timestamp without time zone not null
);
create unique index url_mapping_short_url_index on url_mapping (short_url);
comment on table url_mapping is 'short url and origin url mapping';
comment on column url_mapping.id is '主鍵';
comment on column url_mapping.short_url is '短鏈接';
comment on column url_mapping.origin_url is '原始的鏈接';
comment on column url_mapping.start_timestamp is '映射有效開始時間';
comment on column url_mapping.end_timestamp is '映射有效結束時間';

5.2:硬碟估計

假設每秒鐘200個創建短鏈接的請求,一條數據存儲500位元組,那麼每年的硬碟使用量為

200 * 60 * 60 * 24 * 30 *12 * 500 / 1024 /1024 / 1024 ≈ 2896GB ≈ 3TB

所以如果使用關係資料庫的話我們要考慮分庫分表的設計方案比如採用一致性hash演算法根據key去路由到不同的資料庫去存儲,這樣有利於我們橫向擴展;當然也可以採用mongoDB這樣的文檔資料庫來存儲擴展的時候相對容易一些。

5.3:快取設計

考慮到系統的API響應時間問題,所以會在介面的訪問的同時加上一層快取,降低介面的響應時間,同時也避免請求直接到達資料庫,導致資料庫壓力過大,造成服務不可用。所以為了保證快取的高可用,我們也會在快取這一層做集群,保證快取的高可用。

6:系統的實現

6.1:創建短鏈接映射的實現

創建短鏈接的實現主要包括以下幾個關鍵點

1:短鏈接的映射

2:數據的存儲(包括數據存儲資料庫的路由,快取的更新等)

3:創建的短鏈接要放入布隆過濾器,防止訪問短鏈接的時候快速失敗

 

創建短鏈接的映射有以下兩種方式創建短鏈接,1:通過哈希映射的方式 2:通過Id自增映射的方式,通過Id映射的方式可以有以下幾種實現,資料庫自增Id、Redis自增Id,UUID。下面會一一介紹這幾種實現方式。

6.1.1:哈希映射

一般的哈希映射有MD5、SHA、GoogleMurmur等,由於MD5和SHA的實現考慮到加密性的問題,所以效率不如Google的Murmur演算法高效,而且GoogleMurmur的離散性會更好一點。但是還會有以下兩個問題,1:哈希衝突、2:哈希值作為短鏈接依然過長。

哈希衝突,既然是哈希演算法,就不能避免哈希衝突,所以為了解決HASH衝突的問題我們可以在原來的URL上加上時間戳和隨機數的拼接作為鹽值來減少HASH衝突的問題,如果在衝突的話我們可以使用重試的方式再次HASH。

短鏈接過長,一般我們得到的哈希值是一個32為的十六進位數,如果使用這個值作為短鏈接的話可能還是太長,我們考慮url一般是字母的大小寫和數據組合而成的,所以我們可以考慮使用62進位數來解決URL過長的問題,如果短鏈接的長度為6位的話,那麼我們的最大數為62的6次冪。那麼系統可以使用的時間為:pow(62,6)/(200 * 60 * 60 * 24 * 30 *12)≈ 9年,如果時間不夠的話我們可以擴展到7位,就可以使用566年了,哈哈哈,足夠用了。由於篇幅問題這裡就不放具體的程式碼實現了,文章末尾會附上具體的程式碼實現的完整鏈接,具體的實現可以參考com.philosophy.web.domain.generate.GenerateShortUrlByHash

6.1.2:Id映射

如果使用Id映射的話我們可以使用一個數字和一個長鏈接,由於數字特別大的時候可能也會特別長,所以我們依然可以使用上述的62進位樹來解決這個問題。但是我們依然要考慮以下幾個問題,資料庫和Redis的自增Id是連續的,很容易可以猜想到下一個短鏈接是什麼,由於是高可用,所以我們可以在集群的每一個節點中每一次獲取一定量的序號(假設每次獲取10000個序號,要保證獲取和更新資料庫的原子性),然後在將這一萬個需要亂序,這樣就不可以猜想到下一個短鏈接是什麼了,當然如果要使用UUID比如雪花id的實現的話就可以不用考慮這些問題了。自增Id具體實現com.philosophy.web.domain.generate.GenerateShortUrlByIdentity,雪花Id的實現com.philosophy.web.domain.generate.GenerateShortUrlBySnow

6.2:訪問短鏈接的實現

訪問短鏈接的實現就比較簡單了,防止惡意訪問,這裡加上布隆過濾器,具體的功能我們只需要根據短鏈接獲取長鏈接,然後發布訪問事件(可以進行後續的擴展,比如統計URL的訪問次數,也可以起到非同步解耦和提高介面性能),最後進行請求重定向就可以了,重定向的時候我們要注意一個小點(返回碼301和302的區別),由於我們這裡要統計介面的訪問次數,所以返回302。具體實現com.philosophy.web.controller.VisitController#redirect

6.3:應用啟動

載入數據到布隆過濾器

7:結語

系統雖小,五臟俱全,通過對本系統的實現,也學習到了很多新的知識以及一些系統的設計思想;路漫漫其修遠兮,吾將上下而求索。

 

源程式碼地址://gitee.com/philosoxhy/short_url