短網址 顧名思義,就是將長網址縮短到一個很短的網址,用戶訪問這個短網址可以重定向到原本的長網址(還原)。這樣可以達到易于記憶、轉換的目的,常用于有字數限制的微博、二維碼等場景。
開篇先拋出幾個問題,如果大家自己去實現會怎么實現這個看似很簡單的服務呢?
是否有什么算法可以直接把一百個字符左右的長網址縮短到10位以內,并可以原樣還原,即可逆。
10倍的壓縮比在無損壓縮算法領域誰介紹下?當然這個比例是基于多樣數據而不是特定的文本,否則文本只有一個字符a,那壓縮比想多少有多少。
只實現字符壓縮/hash,不需要做到可逆,然后存儲到數據庫中,恢復時只需要查詢數據庫。
從壓縮的角度和第一條說明沒有區別,不可能無損壓縮,那就有可能出現hash碰撞。不同的長網址縮短成了同一個短網址,那也做不到還原了。
接著第二條,出現碰撞了可以后面再加隨機字符。
隨機字符可以緩解碰撞問題,但是無法根治。另外,增加幾位字符呢才可靠呢?這種概率事件無法通過測試來回答,通過運維監控不斷的調整也是一件頭疼和折磨人的事。
預先在redis/db里異步生成一批短碼,每次從列表里面取不就好了。
具體是在redis還是db里批量生成其實是截然不同的兩種實現。 若是
redis, 那么里面要放入全量的短碼么?否則怎么知道這個短碼到底是不是唯一的?如果全量,那對redis的可用性就要嚴格保證,否則它掛了,就必須全量的預熱,這個過程要做好不是那么的容易; 若是
db, 那么就要有大量的并發鎖定,意味著大量讀寫,這個對數據庫也是個考驗。
短網址的還原跳轉用301還是302呢?
301是永久重定向,302是臨時重定向。短地址一經生成就不會變化,所以用301是符合http語義的。同時瀏覽器會對301請求保存一個比較長期的緩存,這樣就減輕對服務器的壓力;而且301對于網址的SEO有一定的提升。但是很多情況下我們需要對接口點擊或者用戶行為進行一些業務監控處理的時候,301明顯就不合適了(瀏覽器直接按照緩存數據跳轉了), 所以很多業務場景下還是采用302比較合適。
請結合上述問題后的描述思考5分鐘,然后我們開始正文
首先聲明本文設計思路也只是取自己業務場景可以容忍的情況,而有所取舍.
沒有完美的系統只有適用的系統。
縮短網址的算法
# 初期選型算法
對于算法本人也算是踩了不少的坑,最開始采用的是網上流傳甚廣的微博短網址算法(原理類似16進制與62進制的轉換),加了些許改進:
- 將"原始鏈接(長鏈接)+ key(隨機字符串,防止算法泄漏)"MD5后得到32位的一個字符串A
- 將上面的A字符串分為4段處理, 每段的長度為8 , 比如四段分別為 M、N、O、P
- 將M字符串當作一個16進制格式的數字來處理, 將其轉換為一個Long類型。 比如轉換為L
- 此時L的二進制有效長度為32位, 需要將前面兩位去掉,留下30位 , 可以 & 0x3fffffff 進行位與運算得到想要的結果。(30位才能轉換62進制,否則超出)
- 此時L的二進制有效長度為30位, 分為6段處理, 每段的長度為5位
- 依次取出L的每一段(5位),進行位操作 & 0x0000003D 得到一個 <= 61的數字,來當做index
- 根據index 去預定義的62進制字符表里面去取一個字符, 最后能取出6個字符,然后拼裝這6個字符成為一個6位字符串,作為短鏈接碼
- 拿出第②步剩余的三段,重復3-7 步
- 這樣總共可以獲得 4 個6位字符串,取里面的任意一個就可作為這個長鏈接的短網址
- 串碼添加校驗位checksum,用于簡單校驗。所以總共7位碼
算法其實并不太復雜,大家自行解讀。這個算法在服務量不大的情況下hash碰撞的概率尚可以接受,一定量的壓測效果也還算理想。因為有4個hash可選項,即使碰撞到了還有其他3次機會去避免。但是如果作為基礎服務,在使用方調用量級無法估量無法保證短鏈接絕對可用的情況下,這個算法還是有很大的隱患!
# 改進算法
只要有hash就有碰撞的可能,要一勞永逸就得拋棄hash算法。這里我們就用全局自增長的10進制序號 -> 62進制
實現。這里繼續拋出問題來了:
1. 字符超長問題
即使到了10億(Billion)轉換而成的62進制也無非是6位字符,所以長度基本不在考慮范圍內,這個范圍足夠使用了。
2. 短碼安全問題
按照算法從0-61都是1位字符,然后2位、3位...這樣的話很容易被人發現規律并進行攻擊,當然防御手段很多,請求簽名之類的安全驗證手段不在本文討論范圍內。
首先計數器可以從一個比較大的隨機中間值開始,比如從10000
開始計數,他的62進制是 2Bi
3位的字符串;
然后采用一些校驗位算法(比如Luhn改進一下),計算出1位校驗位拼接起來,4位短碼,這樣可以排除一定的安全風險;
再加點安全料的話,可以在62進制的轉換過程中把排序好的62個字母數字隨機打亂,比如ABCD1234
打亂成1BC43A2D
, 轉換的62進制也就更難hack了;
最后如果仍不放心,還可以在某些位置(比如1,3,5)插入隨機數,讓人無法看出規律來也可以達到良好的效果。
3. 同一長網址短碼是否應該相同問題
這個問題按照碼農的完美主義原則,基本上回答是Yes。做到也不麻煩,比如對長網址進行sha1生成的hash值存入hashtable或者redis,在縮短之前進行hash值比對,如果相同就查詢出之前生成的短碼即可。
但是緩存內預熱多少sha1值讓其比對,多少會穿透到數據庫進行比對,就是你自己需要對熱點數據如何預熱和緩存命中的問題了,這里不展開。
4. 自增算法是否完美無缺
相比上面的hash分段算法,自增算法能已經基本可以保證碼唯一、同址同碼的目標了。但是它也有缺陷,那就是隨著序號的自增,碼越來越長,到了很大的數值后沒有辦法循環往復,讓碼重新變短!而hash分段就沒有這個問題。
所以這兩個算法其實各有優劣,如果業務所需的短網址有效期相對較短,通過批處理定期清洗掉,那第一種算法不失為一種可選方案;
而自增算法對于無差別的業務短網址,可以保證任何的請求量都不會出現沖突,權衡下來不失為最佳之選。碼無非分為永久碼或臨時碼,結合業務的話其實大部分的碼都是臨時碼,只是或長或短而已,所以甚至可以設計永久的碼序號區間是0-10000,0-5天的碼10001-20000,5-30天的碼20001-30000,30天+的碼30001-無窮。這樣就可以實現序號的重復使用了。當然如果你對自己設定的區間沒有自信的話(溢出),請千萬不要這么做。
Redis/DB 如何設計
# DB設計
只需要一張表,存放短碼與原網址的映射關系,其他一些屬性比如原網址的sha1碼,過期時間等保存好即可。當然短碼和sha1字段都要加上唯一索引,保證唯一性的同時提高查詢效率。
# Redis設計
若想短鏈接服務達到低延遲高并發的目標,Redis在很多環節都可以起到關鍵作用。
1. 自增長序列
通過Redis的 incr
方法可以很容易的實現全局自增長序列,但前提是Redis的高可用,如果Redis掛了序列從哪里開始呢?當然是從DB中拿咯,怎么拿?
方式一:DB表中新增一個字段,存入最新的短碼基于的序號值,然后Redis在此基礎上+1即可。這部分代碼務必做好同步; 方式二:直接從DB中獲取最新的短碼,然后逆向計算出序號值,+1后繼續;
2. 長網址的Hash表
在Redis中存入熱點網址的hash映射數據,注意,這里說的是熱點網址而且不是全量網址,實現者需要有所取舍。或者沒有命中的就產生新的短碼(會導致同址不同碼),或者沒有命中就到數據庫查詢,保證強一致的同址同碼。
3. 短碼與長網址的映射表
同樣,在Redis中存入熱點網址的映射,在短網址還原的請求處理中可以快速的查詢到原網址。所以這個點的緩存是必須的。
其他一些說明
- 原網址的Hash方法文中是sha1是選擇之一,類似Murmur64等更快更優的hash算法,可以自行采納
- 類似的服務已經有比較成熟的開源解決方案,比如YOURLS,不想重復造輪子的同學可以直接拿來用
- 如果要對短鏈接做一些監控,比如訪問量,可以通過Redis自行實現。目前公司應用集成了點評開源CAT,所以訪問量監控都不是問題
文終。
任何疏漏或者意見,歡迎討論。