短 URL 服務的設計以及實現

-     背景    -

想必大家也經常收到垃圾短信吧…… 短信中的鏈接一般都是短鏈接,類似於下圖這樣:


爲什麼這裏面的 URL 都是短的呢? 有什麼好處呢?怎麼做到的呢?

-     端 URL 的好處    -

  1. 短信和許多平臺 (微博) 有字數限制 ,太長的鏈接加進去都沒有辦法寫正文了。

  2. 好看。 比起一大堆不知所以的參數, 短鏈接更加簡潔友好。

  3. 方便做一些統計。 你點了鏈接會有人記錄然後分析的。

  4. 安全。 不暴露訪問參數。

這就是爲什麼我們現在收到的垃圾短信大多數都是短 URL 的原因了。

那麼短 URL 是怎麼做到的呢?

-     短 URL 基礎原理    -

短 URL 從生成到使用分爲以下幾步:

  1. 有一個服務, 將要發送給你的長 URL 對應到一個短 URL 上. 例如 www.baidu.com -> www.t.cn/1。

  2. 把短 URL 拼接到短信等的內容上發送。.

  3. 用戶點擊短 URL, 瀏覽器用 301/302 進行重定向, 訪問到對應的長 URL。

  4. 展示對應的內容。

本文主要集中於第一步, 即如何將一個長 URL 對應到短 URL 上。

-     服務設計    -

如果你在往長短 URL 真實的對應關係上想,那麼就走遠了。

最理想的情況是:我們用一種算法,對每一個長 URL,唯一的轉換成短 URL。還能保持反向轉換的能力。

但是這是不可能的,如果有這樣的算法,世界上的所有壓縮算法都可以原地去世了。

正確的思路是建立一個發號器,每次有一個新的長 URL 進來,我們就增加一,並且將新的數值返回。第一個來的 URL 返回 "www.x.cn/0", 第二個返回 "www.x.cn/1"。

接下來以 QA 形式寫幾個小問題:

對應關係如何存儲?

這個對應數據肯定是要落盤的,不能每次系統重啓就重新排號,所以可以採用 mysql 等數據庫來存儲。而且如果數據量小且 qps 低,直接使用數據庫的自增主鍵就可以實現。

如何保證長短鏈接一一對應?

按照上面的發號器策略,是不能保證長短鏈接的一一對應的,你連續用同一個 URL 請求兩次, 結果值都是不一樣的。

爲了實現長短鏈接一一對應,我們需要付出很大的空間代價,尤其是爲了快速響應,我們可以需要在內存中做一層緩存,這樣子太浪費了。

但是可以實現一些變種的,來實現部分的一一對應,比如將最近 / 最熱門的對應關係存儲在 K-V 數據庫中,這樣子可以節省空間的同時,加快響應速度。

短 URL 的存儲

我們返回的短 URL 一般是將數字轉換成 32 進制,這樣子可以更加有效的縮短 URL 長度,那麼 32 進制的數字對計算機來說只是字符串,怎麼存儲呢?直接存儲字符串對等值查找好找,對範圍查找等太不友好了。

其實可以直接存儲 10 進制的數字,這樣不僅佔用空間少,對查找的支持較好,同時還可以更加方便的轉換到更多 / 更少的進制來進一步縮短 URL。

高併發

如果直接存儲在 MySQL 中,當併發請求增大,對數據庫的壓力太大,可能會造成瓶頸,這時候是可以有一些優化的。

緩存

上面保證長短鏈接一一對應中也提到過緩存,這裏我們是爲了加快程序處理速度。可以將熱門的長鏈接 (需要對長鏈接進來的次數進行計數),最近的長鏈接(可以使用 redis 保存最近一個小時的) 等等進行一個緩存,保存在內存中或者類似 redis 的內存數據庫中,如果請求的長 URL 命中了緩存,那麼直接獲取對應的短 URL 進行返回,不需要再進行生成操作。

批量發號

每一次發號都需要訪問一次 MySQL 來獲取當前的最大號碼,並且在獲取之後更新最大號碼,這個壓力是比較大的。

我們可以每次從數據庫獲取 10000 個號碼,然後在內存中進行發放,當剩餘的號碼不足 1000 時,重新向 MySQL 請求下 10000 個號碼,在上一批號碼發放完了之後,批量進行寫入。

這樣可以將對數據庫持續的操作移到代碼中進行,並且異步進行獲取和寫入操作,保證服務的持續高併發。

分佈式

上面設計的系統是有單點的,那就是發號器是個單點,容易掛掉。

可以採用分佈式服務,分佈式的話,如果每一個發號器進行發號之後都需要同步給其他發號器, 那未必也太麻煩了。

換一種思路,可以有兩個發號器,一個發單號,一個發雙號,發號之後不再是遞增 1,而是遞增 2。

類比可得,我們可以用 1000 個服務,分別發放 0-999 尾號的數字,每次發號之後遞增 1000。這樣做很簡單,服務互相之間基本都不用通信, 做好自己的事情就好了。

實現


由於我懶得寫 JDBC 代碼,更懶得弄 Mybatis,所以代碼中使用到 MySQL 的地方都使用了 Redis。

package util;

import redis.clients.jedis.Jedis;

/**
 * Created by pfliu on 2019/06/23.
 */
public class ShortURLUtil {


    private static final String SHORT_URL_KEY = "SHORT_URL_KEY";
    private static final String LOCALHOST = "http://localhost:4444/";
    private static final String SHORT_LONG_PREFIX = "short_long_prefix_";
    private static final String CACHE_KEY_PREFIX = "cache_key_prefix_";
    private static final int CACHE_SECONDS = 1 * 60 * 60;

    private final String redisConfig;
    private final Jedis jedis;

    public ShortURLUtil(String redisConfig) {
        this.redisConfig = redisConfig;
        this.jedis = new Jedis(this.redisConfig);
    }

    public String getShortURL(String longURL, Decimal decimal) {
        // 查詢緩存
        String cache = jedis.get(CACHE_KEY_PREFIX + longURL);
        if (cache != null) {
            return LOCALHOST + toOtherBaseString(Long.valueOf(cache), decimal.x);
        }

        // 自增
        long num = jedis.incr(SHORT_URL_KEY);
        // 在數據庫中保存短-長URL的映射關係,可以保存在MySQL中
        jedis.set(SHORT_LONG_PREFIX + num, longURL);
        // 寫入緩存
        jedis.setex(CACHE_KEY_PREFIX + longURL, CACHE_SECONDS, String.valueOf(num));
        return LOCALHOST + toOtherBaseString(num, decimal.x);
    }

    /**
     * 在進製表示中的字符集合
     */
    final static 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'};

    /**
     * 由10進制的數字轉換到其他進制
     */
    private String toOtherBaseString(long n, int base) {
        long num = 0;
        if (n < 0) {
            num = ((long) 2 * 0x7fffffff) + n + 2;
        } else {
            num = n;
        }
        char[] buf = new char[32];
        int charPos = 32;
        while ((num / base) > 0) {
            buf[--charPos] = digits[(int) (num % base)];
            num /= base;
        }
        buf[--charPos] = digits[(int) (num % base)];
        return new String(buf, charPos, (32 - charPos));
    }

    enum Decimal {
        D32(32),
        D64(64);

        int x;

        Decimal(int x) {
            this.x = x;
        }
    }


    public static void main(String[] args) {

        for (int i = 0; i < 100; i++) {
            System.out.println(new ShortURLUtil("localhost").getShortURL("www.baidudu.com", Decimal.D32));
            System.out.println(new ShortURLUtil("localhost").getShortURL("www.baidu.com", Decimal.D64));
        }
    }
}

作者:翁智華

來源:

https://www.cnblogs.com/wzh2010/p/14454954.html

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/Lo-j4kAgahOxV7_w4Km1wQ