高性能短鏈設計

前言

今天,咱們來談談如何設計一個高性能短鏈系統,短鏈系統設計看起來很簡單,但每一個點都能展開不少知識點,也是在面試中很是適合考察侯選人的一道設計題,本文將會結合咱們生產上穩定運行兩年之久的高性能短鏈系統給你們簡單介紹下設計這套系統所涉及的一些思路,但願對你們能有一些幫助。html

本文將會從如下幾個方面來說解,每一個點包含的信息量都很多,相信你們看完確定有收穫mysql

  • 短鏈有啥好處,爲啥要設計它,用長鏈不香嗎
  • 短鏈跳轉的基本原理
  • 短鏈生成的幾種方法
  • 短鏈的架構支撐

注:裏面涉及到很多布隆過濾器,snowflake 等技術,因爲不是本文重點,因此建議你們看完後再本身去深刻了解,否則展開講篇幅會很長面試

短鏈有啥好處,用長鏈不香嗎

來看下如下極客時間發個人營銷短信,點擊下方藍色的連接(短鏈)redis

瀏覽器的地址欄上最終會顯示一條以下的長鏈。算法

那麼爲啥要用短鏈表示,直接用長鏈不行嗎,用短鏈的話有以下好外sql

一、連接變短,在對內容長度有限制的平臺發文,可編輯的文字就變多了shell

最典型的就是微博,限定了只能發 140 個字,若是一串長鏈直接懟上去,其餘可編輯的內容就所剩無幾了,用短鏈的話,連接長度大大減小,天然可編輯的文字多了很多。數據庫

再好比通常短信發文有長度限度,若是用長鏈,一條短信極可能要拆分紅兩三條發,原本一條一毛的短信費變成了兩三毛,何苦呢。另外用短鏈在內容排版上也更美觀。瀏覽器

二、咱們常常須要將連接轉成二維碼的形式分享給他人,若是是長鏈的話二維碼密集難識別,短鏈就不存在這個問題了,如圖示緩存

三、連接太長在有些平臺上沒法自動識別爲超連接

如圖示,在釘釘上,就沒法識別以下長連接,只能識別部分,用短地址無此問題

短鏈跳轉的基本原理

從上文可知,短鏈好處多多,那麼它是如何工做的呢。咱們在瀏覽器抓下包看看

能夠看到請求後,返回了狀態碼 302(重定向)與 location 值爲長鏈的響應,而後瀏覽器會再請求這個長鏈以獲得最終的響應,整個交互流程圖以下

主要步驟就是訪問短網址後重定向訪問 B,那麼問題來了,301 和 302 都是重定向,到底該用哪一個,這裏須要注意一下 301 和 302 的區別

  • 301,表明 永久重定向,也就是說第一次請求拿到長連接後,下次瀏覽器再去請求短鏈的話,不會向短網址服務器請求了,而是直接從瀏覽器的緩存裏拿,這樣在 server 層面就沒法獲取到短網址的點擊數了,若是這個連接恰好是某個活動的連接,也就沒法分析此活動的效果。因此咱們通常不採用 301。
  • 302,表明 臨時重定向,也就是說每次去請求短鏈都會去請求短網址服務器(除非響應中用 Cache-Control 或 Expired 暗示瀏覽器緩存),這樣就便於 server 統計點擊數,因此雖然用 302 會給 server 增長一點壓力,但在數據異常重要的今天,這點代碼是值得的,因此推薦使用 302!

短鏈生成的幾種方法

一、哈希算法

怎樣才能生成短鏈,仔細觀察上例中的短鏈,顯然它是由固定短鏈域名 + 長鏈映射成的一串字母組成,那麼長鏈怎麼才能映射成一串字母呢,哈希函數不就用來幹這事的嗎,因而咱們有了如下設計思路

那麼這個哈希函數該怎麼取呢,相信確定有不少人說用 MD5,SHA 等算法,其實這樣作有點殺雞用牛刀了,並且既然是加密就意味着性能上會有損失,咱們其實不關心反向解密的難度,反而更關心的是哈希的運算速度和衝突機率。

可以知足這樣的哈希算法有不少,這裏推薦 Google 出品的 MurmurHash 算法,MurmurHash 是一種非加密型哈希函數,適用於通常的哈希檢索操做。與其它流行的哈希函數相比,對於規律性較強的 key,MurmurHash 的隨機分佈特徵表現更良好。非加密意味着着相比 MD5,SHA 這些函數它的性能確定更高(實際上性能是 MD5 等加密算法的十倍以上),也正是因爲它的這些優勢,因此雖然它出現於 2008,但目前已經普遍應用到 Redis、MemCache、Cassandra、HBase、Lucene 等衆多著名的軟件中。

畫外音:這裏有個小插曲,MurmurHash 成名後,做者拿到了 Google 的 offer,因此多作些開源的項目,說不定成名後你也能不經意間收到 Google 的 offer ^_^。

MurmurHash 提供了兩種長度的哈希值,32 bit,128 bit,爲了讓網址儘可通地短,咱們選擇 32 bit 的哈希值,32 bit 能表示的最大值近 43 億,對於中小型公司的業務而言綽綽有餘。對上文提到的極客長鏈作 MurmurHash 計算,獲得的哈希值爲 3002604296,因而咱們如今獲得的短鏈爲 固定短鏈域名+哈希值 = gk.link/a/300260429…

如何縮短域名?

有人說人這個域名仍是有點長,還有一招,3002604296 獲得的這個哈希值是十進制的,那咱們把它轉爲 62 進制可縮短它的長度,10 進制轉 62 進制以下:

因而咱們有 (3002604296)10 = (3hcCxy)10,一下從 10 位縮短到了 6 位!因而如今獲得了咱們的短鏈爲 gk.link/a/3hcCxy

畫外音:6 位 62 進制數可表示 568 億的數,應付長鏈轉換綽綽有餘

如何解決哈希衝突的問題?

既然是哈希函數,不可避免地會產生哈希衝突(儘管機率很低),該怎麼解決呢。

咱們知道既然訪問訪問短鏈能跳轉到長鏈,那麼二者以前這種映射關係必定是要保存起來的,能夠用 Redis 或 Mysql 等,這裏咱們選擇用 Mysql 來存儲。表結構應該以下所示

CREATE TABLE `short_url_map` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
  `lurl` varchar(160) DEFAULT NULL COMMENT '長地址',
  `surl` varchar(10) DEFAULT NULL COMMENT '短地址',
  `gmt_create` int(11) DEFAULT NULL COMMENT '建立時間',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
複製代碼

因而咱們有了如下設計思路。

  1. 將長鏈(lurl)通過 MurmurHash 後獲得短鏈。
  2. 再根據短鏈去 short_url_map 表中查找看是否存在相關記錄,若是不存在,將長鏈與短鏈對應關係插入數據庫中,存儲。
  3. 若是存在,說明已經有相關記錄了,此時在長串上拼接一個自定義好的字段,好比「DUPLICATE」,而後再對接接的字段串「lurl + DUPLICATE」作第一步操做,若是最後仍是重複呢,再拼一個字段串啊,只要到時根據短鏈取出長鏈的時候把這些自定義好的字符串移除便是原來的長鏈。

以上步驟顯然是要優化的,插入一條記錄竟然要通過兩次 sql 查詢(根據短鏈查記錄,將長短鏈對應關係插入數據庫中),若是在高併發下,顯然會成爲瓶頸。

畫外音:通常數據庫和應用服務(只作計算不作存儲)會部署在兩臺不一樣的 server 上,執行兩條 sql 就須要兩次網絡通訊,這兩次網絡通訊與兩次 sql 執行是整個短鏈系統的性能瓶頸所在!

因此該怎麼優化呢

  1. 首先咱們須要給短鏈字段 surl 加上惟一索引
  2. 當長鏈通過 MurmurHash 獲得短鏈後,直接將長短鏈對應關係插入 db 中,若是 db 裏不含有此短鏈的記錄,則插入,若是包含了,說明違反了惟一性索引,此時只要給長鏈再加上咱們上文說的自定義字段「DUPLICATE」,從新 hash 再插入便可,看起來在違反惟一性索引的狀況下是多執行了步驟,但咱們要知道 MurmurHash 發生衝突的機率是很是低的,基本上不太可能發生,因此這種方案是能夠接受的。

固然若是在數據量很大的狀況下,衝突的機率會增大,此時咱們能夠加布隆過濾器來進行優化。

用全部生成的短網址構建布隆過濾器,當一個新的長鏈生成短鏈後,先將此短鏈在布隆過濾器中進行查找,若是不存在,說明 db 裏不存在此短網址,能夠插入!

畫外音:布隆過濾器是一種很是省內存的數據結構,長度爲 10 億的布隆過濾器,只須要 125 M 的內存空間。

綜上,若是用哈希函數來設計,整體的設計思路以下

用哈希算法生成的短鏈其實已經能知足咱們的業務需求,接下來咱們再來看看如何用自增序列的方式來生成短鏈

二、自增序列算法

咱們能夠維護一個 ID 自增生成器,好比 1,2,3 這樣的整數遞增 ID,當收到一個長鏈轉短鏈的請求時,ID 生成器爲其分配一個 ID,再將其轉化爲 62 進制,拼接到短鏈域名後面就獲得了最終的短網址,那麼這樣的 ID 自增生成器該如何設計呢。若是在低峯期發號還好,高併發下,ID 自增生成器的的 ID 生成可能會系統瓶頸,因此它的設計就顯得尤其重要。

主要有如下四種獲取 id 的方法

一、類 uuid

簡單地說就是用 UUID uuid = UUID.randomUUID(); 這種方式生成的 UUID,UUID(Universally Unique Identifier)全局惟一標識符,是指在一臺機器上生成的數字,它保證對在同一時空中的全部機器都是惟一的,但這種方式生成的 id 比較長,且無序,在插入 db 時可能會頻繁致使頁分裂,影響插入性能。

二、Redis

用 Redis 是個不錯的選擇,性能好,單機可支撐 10 w+ 請求,足以應付大部分的業務場景,但有人說若是一臺機器扛不住呢,能夠設置多臺嘛,好比我佈置 10 臺機器,每臺機器分別只生成尾號0,1,2,... 9 的 ID, 每次加 10便可,只要設置一個 ID 生成器代理隨機分配給發號器生成 ID 就好了。

不過用 Redis 這種方案,須要考慮持久化(短鏈 ID 總不能同樣吧),災備,成本有點高。

三、Snowflake

用 Snowflake 也是個不錯的選擇,不過 Snowflake 依賴於系統時鐘的一致性。若是某臺機器的系統時鐘回撥,有可能形成 ID 衝突,或者 ID 亂序。

四、Mysql 自增主鍵

這種方式使用簡單,擴展方便,因此咱們使用 Mysql 的自增主鍵來做爲短鏈的 id。簡單總結以下:

那麼問題來了,若是用 Mysql 自增 id 做爲短鏈 ID,在高併發下,db 的寫壓力會很大,這種狀況該怎麼辦呢。

考慮一下,必定要在用到的時候去生成 id 嗎,是否能夠提早生成這些自增 id ?

方案以下:

設計一個專門的發號表,每插入一條記錄,爲短鏈 id 預留 (主鍵 id * 1000 - 999) 到 (主鍵 id * 1000) 的號段,以下

發號表:url_sender_num

如圖示:tmp_start_num 表明短鏈的起始 id,tmp_end_num 表明短鏈的終止 id。

當長鏈轉短鏈的請求打到某臺機器時,先看這臺機器是否分配了短鏈號段,未分配就往發號表插入一條記錄,則這臺機器將爲短鏈分配範圍在 tmp_start_num 到 tmp_end_num 之間的 id。從 tmp_start_num 開始分配,一直分配到 tmp_end_num,若是發號 id 達到了 tmp_end_num,說明這個區間段的 id 已經分配完了,則再往發號表插入一條記錄就又獲取了一個發號 id 區間。

畫外音:思考一下這個自增短鏈 id 在機器上該怎麼實現呢, 能夠用 redis, 不過更簡單的方案是用 AtomicLong,單機上性能不錯,也保證了併發的安全性,固然若是併發量很大,AtomicLong 的表現就不太行了,能夠考慮用 LongAdder,在高併發下表現更加優秀。

總體設計圖以下

解決了發號器問題,接下來就簡單了,從發號器拿過來的 id ,即爲短鏈 id,接下來咱們再建立一個長短鏈的映射表便可, 短鏈 id 即爲主鍵,不過這裏有個須要注意的地方,咱們可能須要防止屢次相同的長鏈生成不一樣的短鏈 id 這種狀況,這就須要每次先根據長鏈來查找 db 看是否存在相關記錄,通常的作法是根據長鏈作索引,但這樣的話索引的空間會很大,因此咱們能夠對長鏈適當的壓縮,好比 MD5,再對長鏈的 MD5 字段作索引,這樣索引就會小不少。這樣只要根據長鏈的 md5 去表裏查是否存在相同的記錄便可。因此咱們設計的表以下

CREATE TABLE `short_url_map` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '短鏈 id',
  `lurl` varchar(10) DEFAULT NULL COMMENT '長鏈',
  `md5` char(32) DEFAULT NULL COMMENT '長鏈md5',
  `gmt_create` int(11) DEFAULT NULL COMMENT '建立時間',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
複製代碼

固然了,數據量若是很大的話,後期就須要分區或分庫分表了。

請求短鏈的高併發架構設計

在電商公司,常常有不少活動,秒殺,搶紅包等等,在某個時間點的 QPS 會很高,考慮到這種狀況,咱們引入了 openResty,它是一個基於 Nginx 與 Lua 的高性能 Web 平臺,因爲 Nginx 的非阻塞IO模型,使用 openResty 能夠輕鬆支持 100 w + 的併發數,通常狀況下你只要部署一臺便可,同時 openResty 也自帶了緩存機制,集成了 redis 這些緩存模塊,也能夠直接連 mysql。不須要再經過業務層連這些中間件,性能天然會高很多

如圖示,使用 openResty 省去了業務層這一步,直達緩存層與數據庫層,也提高了很多性能。

總結

本文對短鏈設計方案做了詳細地剖析,旨在給你們提供幾種不一樣的短鏈設計思路,文中涉及到挺多像布隆過濾器,openRestry 等技術,文中沒有展開講,建議你們回頭能夠去再詳細瞭解一下。再好比文中提到的 Mysql 頁分裂也須要對底層使用的 B+ tree 數據結構,操做系統按頁獲取等知識有比較詳細地瞭解,相信你們各個知識點都吃透後會收穫不小。

巨人的肩膀

www.cnblogs.com/rjzheng/p/1…

time.geekbang.org/column/arti…

歡迎你們掃碼關注公衆號,一塊兒探討

相關文章
相關標籤/搜索