如何將一個長URL轉換爲一個短URL?

原文鏈接:https://blog.csdn.net/xlgen157387/article/details/80026452

轉自:https://blog.csdn.net/xlgen157387/article/details/80026452

https://www.zhihu.com/question/29270034/answer/46446911

 

一、前言
前幾天整理面試題的時候,有一道試題是《如何將一個很長的URL轉換爲一個短的URL,並實現他們之間的相互轉換?》,現在想起來這是一個絕對不簡單的問題,需要考慮很多方面,今天和大家一起學習研究一下!

短網址:顧名思義,就是將長網址縮短到一個很短的網址,用戶訪問這個短網址可以重定向到原本的長網址(也就是還原的過程)。這樣可以達到易於記憶、轉換的目的,常用於有字數限制的微博、二維碼等等場景。

關於短URL的使用場景,舉個簡單的例子來說明一下,看一下業務中使用短URL的重要性!

二、短地址使用場景
1、新浪微博

我們在新浪微博上發佈網址的時候,微博會自動判別網址,並將其轉換,例如:https://t.cn/RuPKzRW。爲什麼要這樣做的?

這是因爲微博限制字數爲140字一條,那麼如果我們需要發一些鏈接上去,但是這個鏈接非常的長,以至於將近要佔用我們內容的一半篇幅,這肯定是不能被允許的或者說用戶體驗很差的,所以短網址應運而生了,短網址這種服務可以說是在微博出現之後才流行開來的!往下看:

(1)首先,我先發一條微博帶有一個URL地址:

(2)然後,看他轉換之後顯示的效果是什麼樣子的哪?

(3)查看對應頁面元素的HTML源碼如下:

(4)可以看出:https://blog.csdn.net/xlgen157387/article/details/79863301 被轉換爲:http://t.cn/RuPKzRW,此時你訪問http://t.cn/RuPKzRW是可以定位到https://blog.csdn.net/xlgen157387/article/details/79863301,也就是實現了轉換。

2、短網址二維碼

網址在轉換成短網址時,也可以生成相應的短網址二維碼,短網址二維碼的應用,二維碼核心解決的是跨平臺、跨現實的數據傳輸問題;而且二維碼跟應用場景結合之後,所能解決的問題會越來越多。

(1)短網址二維碼相比短鏈接更方便,能少輸入,儘量少輸入,哪怕只是少點一下鍵盤,都是有意義的。

(2)二維碼只是掃描一個簡單的鏈接,打開的卻是一個世界。想象一下,用手機購買售貨機裏商品,二維碼掃描是略快於從用手機找到該售貨機並找到該商品的,而且這種操作相對於搜索/查找而言不是更優雅嗎?

(3)所有商超裏面的商品,都是使用條碼來確定商品的唯一性的,去買單的時候都是掃描條碼。試想,如果裏面加入了更多產品的生產日期、廠家、流轉途徑、原材料等等信息,是不是厲害了呢?特別是針對食品信息的可追溯上,二維碼應用場景更廣泛。

三、短地址的好處
除了上述場景中,我們將長地址轉換爲短地址的使用場景的優點(壓縮URL長度)之外,短地址還具有很多實際場景中的優點,例如:

(1)節省網址長度,便於社交化傳播,一個是讓URL更短小,傳播更方便,尤其是URL中有中文和特殊字符,短網址解決很長的URL難以記憶不利於傳播的問題;

(2)短網址在我們項目裏可以很好的對開放以及對URL進行管理。有一部分網址可以會涵蓋性、暴力、廣告等信息,這樣我們可以通過用戶的舉報,完全管理這個連接將不出現在我們的應用中,對同樣的URL通過加密算法之後,得到的地址是一樣的;

(3)方便後臺跟蹤點擊量、地域分佈等用戶統計。我們可以對一系列的網址進行流量,點擊等統計,挖掘出大多數用戶的關注點,這樣有利於我們對項目的後續工作更好的作出決策;

(4)規避關鍵詞、域名屏蔽手段、隱藏真實地址,適合做付費推廣鏈接;

(5)當你看到一個淘寶的寶貝連接後面是200個“e7x8bv7c8bisdj”這樣的字符的時候,你還會覺得舒服嗎。更何況微博字數只有140字,微博或短信裏,字數不夠,你用條短網址就能幫你騰出很多空間來;

四、短網址服務提供平臺
目前,國內網又很多提供短地址服務的平臺,例如:

新浪:http://sina.lt/
百度:http://dwz.cn/
0x3:http://0x3.me/
MRW:http://mrw.so/
等等還有很多,這個可以搜索一下就會有很多!但是一個注意的是,如果使用某一個平臺的短地址服務,一定要保證長期可靠的服務,不然一段時間失效了,我們以前已經轉換的URL就完了!

這裏以百度例,將我們上述博客的地址轉換爲短地址如下所示:

當然,對於我們的業務來說,如果自己可以提供自己的短URL服務那纔是更好的,不需要受制於人!(中國芯片需要崛起!!!)

五、關於如何生成短地址URL的討論
關於短地址URL如何生成方式的,網上有很多方式,有基於映射的,有基於Hash的,有基於簽名的,但是總的來說並不能滿足絕大部分場景的使用,或者說是一種錯誤的設計方式。這裏不再重複造輪子!以下是知乎用戶iammutex關於該問題的探討,截圖過來和大家一起學習一下:

六、生成短地址URL需要注意的
看到上述知乎用戶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)用戶訪問短鏈接:http://t.cn/RuPKzRW;

(2)短鏈接服務器t.cn收到請求,根據URL路徑RuPKzRW獲取到原始的長鏈接(KV緩存數據庫中去查找):https://blog.csdn.net/xlgen157387/article/details/79863301;

(3)服務器返回302狀態碼,將響應頭中的Location設置爲:https://blog.csdn.net/xlgen157387/article/details/79863301;

(4)瀏覽器重新向https://blog.csdn.net/xlgen157387/article/details/79863301發送請求;

(5)返回響應;

八、短地址發號器優化方案
1、算法優化

採用以上算法,如果不加判斷,那麼即使對於同一個原始URL,每次生成的短鏈接也是不同的,這樣就會浪費存儲空間(因爲需要存儲多個短鏈接到同一個URL的映射),如果能將相同的URL映射成同一個短鏈接,這樣就可以節省存儲空間了。主要的思路有如下兩個:

方案1:查表

每次生成短鏈接時,先在映射表中查找是否已有原始URL的映射關係,如果有,則直接返回結果。很明顯,這種方式效率很低。

方案2:使用LRU本地緩存,空間換時間

使用固定大小的LRU緩存,存儲最近N次的映射結果,這樣,如果某一個鏈接生成的非常頻繁,則可以在LRU緩存中找到結果直接返回,這是存儲空間和性能方面的折中。

2、可伸縮和高可用

如果將短鏈接生成服務單機部署,缺點一是性能不足,不足以承受海量的併發訪問,二是成爲系統單點,如果這臺機器宕機則整套服務不可 用,爲了解決這個問題,可以將系統集羣化,進行“分片”。

在以上描述的系統架構中,如果發號器用Redis實現,則Redis是系統的瓶頸與單點,因此,利用數據庫分片的設計思想,可部署多個發號器實例,每個實例負責特定號段的發號,比如部署10臺Redis,每臺分別負責號段尾號爲0-9的發號,注意此時發號器的步長則應該設置爲10(實例個數)。

另外,也可將長鏈接與短鏈接映射關係的存儲進行分片,由於沒有一箇中心化的存儲位置,因此需要開發額外的服務,用於查找短鏈接對應的原始鏈接的存儲節點,這樣才能去正確的節點上找到映射關係。

九、如何用代碼實現短地址
1、使用隨機序列生成短地址

說到這裏終於說到重點了,很多小夥伴已經按捺不住了,不好意思讓大家失望了,這只是一片簡單的文章,並不能把這麼繁雜的一個系統演示清楚!秉着不要重複造輪子的原則,這裏給出一個爲數不多還算可以的實現短地址的開源項目:urlshorter

注意:urlshorter本身還是基於隨機的方式生成短地址的,並不算是一個短地址發號器,因此會有性能問題和衝突的出現,和知乎用戶iammutex 描述的實現方式還是有區別的!而關於短地址發號器的方式目前還沒有找到更好的開源項目可供參考!

項目地址:https://gitee.com/tinyframework/urlshorter

2、使用SnowFlake發號器生成短地址

實現參考:
https://github.com/beyondfengyu/SnowFlake
http://www.wolfbe.com/detail/201611/381.html

Twitter的雪花算法SnowFlake,使用Java語言實現。

SnowFlake算法用來生成64位的ID,剛好可以用long整型存儲,能夠用於分佈式系統中生產唯一的ID, 並且生成的ID有大致的順序。 在這次實現中,生成的64位ID可以分成5個部分:

0 - 41位時間戳 - 5位數據中心標識 - 5位機器標識 - 12位序列號
1
5位數據中心標識、5位機器標識這樣的分配僅僅是當前實現中分配的,如果業務有其實的需要,可以按其它的分配比例分配,如10位機器標識,不需要數據中心標識。

Java代碼實現如下:
 

/**
 * 進制轉換工具,最大支持十進制和62進制的轉換
 * 1、將十進制的數字轉換爲指定進制的字符串;
 * 2、將其它進制的數字(字符串形式)轉換爲十進制的數字
 * @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

 

最後的代碼地址:https://gitee.com/xuliugen/codes/9upvmzyk6c2i78eb3lgnj63

3、推薦一個通用ID發號器

碼雲地址:https://gitee.com/robertleepeak/vesta-id-generator

這裏直接給大家地址,不在介紹,有想了解的可以移步查看文檔。

十、總結
到此爲止,我們一起學習了什麼是短地址,短地址的優點,如何選擇一種正確的方式來實現我們的短地址,以及在碼雲上找到的一個還算可以的短地址生成項目,相信此時的你能夠有一個更好的瞭解!

參考文章:

1、https://www.2cto.com/kf/201601/486883.html
2、https://blog.csdn.net/lz0426001/article/details/52370177
3、http://blog.sina.com.cn/s/blog_16aace20e0102x4tt.html
4、https://www.zhihu.com/question/29270034/answer/46446911
5、https://github.com/beyondfengyu/SnowFlake
 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章