前幾天整理面試題的時候,有一道試題是《如何將一個很長的URL轉換爲一個短的URL,並實現他們之間的相互轉換?》,如今想起來這是一個絕對不簡單的問題,須要考慮不少方面,今天和你們一塊兒學習研究一下!html
短網址:顧名思義,就是將長網址縮短到一個很短的網址,用戶訪問這個短網址能夠重定向到本來的長網址(也就是還原的過程)。這樣能夠達到易於記憶、轉換的目的,經常使用於有字數限制的微博、二維碼等等場景。git
關於短URL的使用場景,舉個簡單的例子來講明一下,看一下業務中使用短URL的重要性!github
一、新浪微博面試
咱們在新浪微博上發佈網址的時候,微博會自動判別網址,並將其轉換,例如:t.cn/RuPKzRW。爲何…算法
這是由於微博限制字數爲140字一條,那麼若是咱們須要發一些連接上去,可是這個連接很是的長,以致於將近要佔用咱們內容的一半篇幅,這確定是不能被容許的或者說用戶體驗不好的,因此短網址應運而生了,短網址這種服務能夠說是在微博出現以後才流行開來的!往下看:數據庫
(1)首先,我先發一條微博帶有一個URL地址: 後端
二、短網址二維碼瀏覽器
網址在轉換成短網址時,也能夠生成相應的短網址二維碼,短網址二維碼的應用,二維碼核心解決的是跨平臺、跨現實的數據傳輸問題;並且二維碼跟應用場景結合以後,所能解決的問題會愈來愈多。緩存
(1)短網址二維碼相比短連接更方便,能少輸入,儘可能少輸入,哪怕只是少點一下鍵盤,都是有意義的。bash
(2)二維碼只是掃描一個簡單的連接,打開的倒是一個世界。想象一下,用手機購買售貨機裏商品,二維碼掃描是略快於從用手機找到該售貨機並找到該商品的,並且這種操做相對於搜索/查找而言不是更優雅嗎?
(3)全部商超裏面的商品,都是使用條碼來肯定商品的惟一性的,去買單的時候都是掃描條碼。試想,若是裏面加入了更多產品的生產日期、廠家、流轉途徑、原材料等等信息,是否是厲害了呢?特別是針對食品信息的可追溯上,二維碼應用場景更普遍。
除了上述場景中,咱們將長地址轉換爲短地址的使用場景的優勢(壓縮URL長度)以外,短地址還具備不少實際場景中的優勢,例如:
(1)節省網址長度,便於社交化傳播,一個是讓URL更短小,傳播更方便,尤爲是URL中有中文和特殊字符,短網址解決很長的URL難以記憶不利於傳播的問題;
(2)短網址在咱們項目裏能夠很好的對開放以及對URL進行管理。有一部分網址能夠會涵蓋性、暴力、廣告等信息,這樣咱們能夠經過用戶的舉報,徹底管理這個鏈接將不出如今咱們的應用中,對一樣的URL經過加密算法以後,獲得的地址是同樣的;
(3)方便後臺跟蹤點擊量、地域分佈等用戶統計。咱們能夠對一系列的網址進行流量,點擊等統計,挖掘出大多數用戶的關注點,這樣有利於咱們對項目的後續工做更好的做出決策;
(4)規避關鍵詞、域名屏蔽手段、隱藏真實地址,適合作付費推廣連接;
(5)當你看到一個淘寶的寶貝鏈接後面是200個「e7x8bv7c8bisdj」這樣的字符的時候,你還會以爲舒服嗎。更況且微博字數只有140字,微博或短信裏,字數不夠,你用條短網址就能幫你騰出不少空間來;
目前,國內網又不少提供短地址服務的平臺,例如:
等等還有不少,這個能夠搜索一下就會有不少!可是一個注意的是,若是使用某一個平臺的短地址服務,必定要保證長期可靠的服務,否則一段時間失效了,咱們之前已經轉換的URL就完了!
這裏以百度例,將咱們上述博客的地址轉換爲短地址以下所示:
固然,對於咱們的業務來講,若是本身能夠提供本身的短URL服務那纔是更好的,不須要受制於人!(中國芯片須要崛起!!!)
關於短地址URL如何生成方式的,網上有不少方式,有基於映射的,有基於Hash的,有基於簽名的,可是總的來講並不能知足絕大部分場景的使用,或者說是一種錯誤的設計方式。這裏再也不重複造輪子!如下是知乎用戶iammutex關於該問題的探討,截圖過來和你們一塊兒學習一下:
做者:iammutex
連接:https://www.zhihu.com/question/29270034/answer/46446911
來源:知乎
著做權歸做者全部。商業轉載請聯繫做者得到受權,非商業轉載請註明出處。
複製代碼
看到上述知乎用戶iammutex關於如何正確生成短地址URL的探討,咱們知道了,能夠經過發號器的方式正確的生成短地址,生成算法設計要點以下:
(1)利用放號器,初始值爲0,對於每個短連接生成請求,都遞增放號器的值,再將此值轉換爲62進制(a-zA-Z0-9),好比第一次請求時放號器的值爲0,對應62進製爲a,第二次請求時放號器的值爲1,對應62進製爲b,第10001次請求時放號器的值爲10000,對應62進製爲sBc。
(2)將短連接服務器域名與放號器的62進制值進行字符串鏈接,即爲短連接的URL,好比:t.cn/sBc。
(3)重定向過程:生成短連接以後,須要存儲短連接到長連接的映射關係,即sBc -> URL,瀏覽器訪問短連接服務器時,根據URL Path取到原始的連接,而後進行302重定向。映射關係可以使用K-V存儲,好比Redis或Memcache。
對於該部分的討論,咱們能夠認爲他是整個交互的流程,具體的流程細節以下:
(1)用戶訪問短連接:t.cn/RuPKzRW;
(2)短連接服務器t.cn收到請求,根據URL路徑RuPKzRW獲取到原始的長連接(KV緩存數據庫中去查找):blog.csdn.net/xlgen157387…
(3)服務器返回302狀態碼,將響應頭中的Location設置爲:blog.csdn.net/xlgen157387…
(4)瀏覽器從新向https://blog.csdn.net/xlgen157387/article/details/79863301發送請求;
(5)返回響應;
一、算法優化
採用以上算法,若是不加判斷,那麼即便對於同一個原始URL,每次生成的短連接也是不一樣的,這樣就會浪費存儲空間(由於須要存儲多個短連接到同一個URL的映射),若是能將相同的URL映射成同一個短連接,這樣就能夠節省存儲空間了。主要的思路有以下兩個:
方案1:查表
每次生成短連接時,先在映射表中查找是否已有原始URL的映射關係,若是有,則直接返回結果。很明顯,這種方式效率很低。
方案2:使用LRU本地緩存,空間換時間
使用固定大小的LRU緩存,存儲最近N次的映射結果,這樣,若是某一個連接生成的很是頻繁,則能夠在LRU緩存中找到結果直接返回,這是存儲空間和性能方面的折中。
二、可伸縮和高可用
若是將短連接生成服務單機部署,缺點一是性能不足,不足以承受海量的併發訪問,二是成爲系統單點,若是這臺機器宕機則整套服務不可 用,爲了解決這個問題,能夠將系統集羣化,進行「分片」。
在以上描述的系統架構中,若是發號器用Redis實現,則Redis是系統的瓶頸與單點,所以,利用數據庫分片的設計思想,可部署多個發號器實例,每一個實例負責特定號段的發號,好比部署10臺Redis,每臺分別負責號段尾號爲0-9的發號,注意此時發號器的步長則應該設置爲10(實例個數)。
另外,也可將長連接與短連接映射關係的存儲進行分片,因爲沒有一箇中心化的存儲位置,所以須要開發額外的服務,用於查找短連接對應的原始連接的存儲節點,這樣才能去正確的節點上找到映射關係。
一、使用隨機序列生成短地址
說到這裏終於說到重點了,不少小夥伴已經按捺不住了,很差意思讓你們失望了,這只是一片簡單的文章,並不能把這麼繁雜的一個系統演示清楚!秉着不要重複造輪子的原則,這裏給出一個爲數很少還算能夠的實現短地址的開源項目:urlshorter
注意:urlshorter自己仍是基於隨機的方式生成短地址的,並不算是一個短地址發號器,所以會有性能問題和衝突的出現,和知乎用戶iammutex 描述的實現方式仍是有區別的!而關於短地址發號器的方式目前尚未找到更好的開源項目可供參考!
二、使用SnowFlake發號器生成短地址
實現參考: github.com/beyondfengy… www.wolfbe.com/detail/2016…
Twitter的雪花算法SnowFlake,使用Java語言實現。
SnowFlake算法用來生成64位的ID,恰好能夠用long整型存儲,可以用於分佈式系統中生產惟一的ID, 而且生成的ID有大體的順序。 在此次實現中,生成的64位ID能夠分紅5個部分:
0 - 41位時間戳 - 5位數據中心標識 - 5位機器標識 - 12位序列號
複製代碼
5位數據中心標識、5位機器標識這樣的分配僅僅是當前實現中分配的,若是業務有其實的須要,能夠按其它的分配比例分配,如10位機器標識,不須要數據中心標識。
Java代碼實現以下:
/**
* 進制轉換工具,最大支持十進制和62進制的轉換
* 一、將十進制的數字轉換爲指定進制的字符串;
* 二、將其它進制的數字(字符串形式)轉換爲十進制的數字
* @author xuliugen
* @date 2018/04/23
*/
public class NumericConvertUtils {
/**
* 在進製表示中的字符集合,0-Z分別用於表示最大爲62進制的符號表示
*/
private static final char[] digits = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm',
'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z',
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M',
'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'};
/**
* 將十進制的數字轉換爲指定進制的字符串
* @param number 十進制的數字
* @param seed 指定的進制
* @return 指定進制的字符串
*/
public static String toOtherNumberSystem(long number, int seed) {
if (number < 0) {
number = ((long) 2 * 0x7fffffff) + number + 2;
}
char[] buf = new char[32];
int charPos = 32;
while ((number / seed) > 0) {
buf[--charPos] = digits[(int) (number % seed)];
number /= seed;
}
buf[--charPos] = digits[(int) (number % seed)];
return new String(buf, charPos, (32 - charPos));
}
/**
* 將其它進制的數字(字符串形式)轉換爲十進制的數字
* @param number 其它進制的數字(字符串形式)
* @param seed 指定的進制,也就是參數str的原始進制
* @return 十進制的數字
*/
public static long toDecimalNumber(String number, int seed) {
char[] charBuf = number.toCharArray();
if (seed == 10) {
return Long.parseLong(number);
}
long result = 0, base = 1;
for (int i = charBuf.length - 1; i >= 0; i--) {
int index = 0;
for (int j = 0, length = digits.length; j < length; j++) {
//找到對應字符的下標,對應的下標纔是具體的數值
if (digits[j] == charBuf[i]) {
index = j;
}
}
result += index * base;
base *= seed;
}
return result;
}
}
複製代碼
/**
* Twitter的SnowFlake算法,使用SnowFlake算法生成一個整數,而後轉化爲62進制變成一個短地址URL
* @author beyond
* @author xuliugen
* @date 2018/04/23
*/
public class SnowFlakeShortUrl {
/**
* 起始的時間戳
*/
private final static long START_TIMESTAMP = 1480166465631L;
/**
* 每一部分佔用的位數
*/
private final static long SEQUENCE_BIT = 12; //序列號佔用的位數
private final static long MACHINE_BIT = 5; //機器標識佔用的位數
private final static long DATA_CENTER_BIT = 5; //數據中心佔用的位數
/**
* 每一部分的最大值
*/
private final static long MAX_SEQUENCE = -1L ^ (-1L << SEQUENCE_BIT);
private final static long MAX_MACHINE_NUM = -1L ^ (-1L << MACHINE_BIT);
private final static long MAX_DATA_CENTER_NUM = -1L ^ (-1L << DATA_CENTER_BIT);
/**
* 每一部分向左的位移
*/
private final static long MACHINE_LEFT = SEQUENCE_BIT;
private final static long DATA_CENTER_LEFT = SEQUENCE_BIT + MACHINE_BIT;
private final static long TIMESTAMP_LEFT = DATA_CENTER_LEFT + DATA_CENTER_BIT;
private long dataCenterId; //數據中心
private long machineId; //機器標識
private long sequence = 0L; //序列號
private long lastTimeStamp = -1L; //上一次時間戳
/**
* 根據指定的數據中心ID和機器標誌ID生成指定的序列號
* @param dataCenterId 數據中心ID
* @param machineId 機器標誌ID
*/
public SnowFlake(long dataCenterId, long machineId) {
if (dataCenterId > MAX_DATA_CENTER_NUM || dataCenterId < 0) {
throw new IllegalArgumentException("DtaCenterId can't be greater than MAX_DATA_CENTER_NUM or less than 0!");
}
if (machineId > MAX_MACHINE_NUM || machineId < 0) {
throw new IllegalArgumentException("MachineId can't be greater than MAX_MACHINE_NUM or less than 0!");
}
this.dataCenterId = dataCenterId;
this.machineId = machineId;
}
/**
* 產生下一個ID
* @return
*/
public synchronized long nextId() {
long currTimeStamp = getNewTimeStamp();
if (currTimeStamp < lastTimeStamp) {
throw new RuntimeException("Clock moved backwards. Refusing to generate id");
}
if (currTimeStamp == lastTimeStamp) {
//相同毫秒內,序列號自增
sequence = (sequence + 1) & MAX_SEQUENCE;
//同一毫秒的序列數已經達到最大
if (sequence == 0L) {
currTimeStamp = getNextMill();
}
} else {
//不一樣毫秒內,序列號置爲0
sequence = 0L;
}
lastTimeStamp = currTimeStamp;
return (currTimeStamp - START_TIMESTAMP) << TIMESTAMP_LEFT //時間戳部分
| dataCenterId << DATA_CENTER_LEFT //數據中心部分
| machineId << MACHINE_LEFT //機器標識部分
| sequence; //序列號部分
}
private long getNextMill() {
long mill = getNewTimeStamp();
while (mill <= lastTimeStamp) {
mill = getNewTimeStamp();
}
return mill;
}
private long getNewTimeStamp() {
return System.currentTimeMillis();
}
public static void main(String[] args) {
SnowFlake snowFlake = new SnowFlake(2, 3);
for (int i = 0; i < (1 << 4); i++) {
//10進制
Long id = snowFlake.nextId();
//62進制
String convertedNumStr = NumericConvertUtils.toOtherNumberSystem(id, 62);
//10進制轉化爲62進制
System.out.println("10進制:" + id + " 62進制:" + convertedNumStr);
//TODO 執行具體的存儲操做,能夠存放在Redis等中
//62進制轉化爲10進制
System.out.println("62進制:" + convertedNumStr + " 10進制:" + NumericConvertUtils.toDecimalNumber(convertedNumStr, 62));
System.out.println();
}
}
}
//生成結果:
10進制:185784275776581632 62進制:dITqmhW2He
62進制:dITqmhW2He 10進制:185784275776581632
10進制:185784284689477632 62進制:dITqw17E6k
62進制:dITqw17E6k 10進制:185784284689477632
10進制:185784284689477633 62進制:dITqw17E6l
62進制:dITqw17E6l 10進制:185784284689477633
10進制:185784284689477634 62進制:dITqw17E6m
62進制:dITqw17E6m 10進制:185784284689477634
10進制:185784284689477635 62進制:dITqw17E6n
62進制:dITqw17E6n 10進制:185784284689477635
10進制:185784284689477636 62進制:dITqw17E6o
62進制:dITqw17E6o 10進制:185784284689477636
10進制:185784284689477637 62進制:dITqw17E6p
62進制:dITqw17E6p 10進制:185784284689477637
10進制:185784284693671936 62進制:dITqw1pfeo
62進制:dITqw1pfeo 10進制:185784284693671936
10進制:185784284693671937 62進制:dITqw1pfep
62進制:dITqw1pfep 10進制:185784284693671937
10進制:185784284693671938 62進制:dITqw1pfeq
62進制:dITqw1pfeq 10進制:185784284693671938
10進制:185784284693671939 62進制:dITqw1pfer
62進制:dITqw1pfer 10進制:185784284693671939
10進制:185784284693671940 62進制:dITqw1pfes
62進制:dITqw1pfes 10進制:185784284693671940
10進制:185784284693671941 62進制:dITqw1pfet
62進制:dITqw1pfet 10進制:185784284693671941
10進制:185784284693671942 62進制:dITqw1pfeu
62進制:dITqw1pfeu 10進制:185784284693671942
10進制:185784284693671943 62進制:dITqw1pfev
62進制:dITqw1pfev 10進制:185784284693671943
10進制:185784284693671944 62進制:dITqw1pfew
62進制:dITqw1pfew 10進制:185784284693671944
複製代碼
最後的代碼地址:gitee.com/xuliugen/co…
三、推薦一個通用ID發號器
這裏直接給你們地址,不在介紹,有想了解的能夠移步查看文檔。
到此爲止,咱們一塊兒學習了什麼是短地址,短地址的優勢,如何選擇一種正確的方式來實現咱們的短地址,以及在碼雲上找到的一個還算能夠的短地址生成項目,相信此時的你可以有一個更好的瞭解!
參考文章:
三、blog.sina.com.cn/s/blog_16aa…