多種方式生成短網址(分佈式發號器)解讀+實現(親測可用)

目錄

1、固定長度分段(4組6位字符,任選一組)

2. 可選長度分段(短鏈長度需能被30整除)

3.分佈式發號器snowflake(新短url與源url無關,與發號器有關)

二、重點解答

1.短網址的長度?---長度<=7的62進制數(大小寫字母加數字),最多可生成568億個短鏈

2.如何計算短網址?---分佈式發號器

3.長網址與短網址的關係是一對一還是一對多映射?---一對多

4.301還是302重定向?---302重定向

5.如何預防攻擊?---redis緩存長網址->ID,而不是ID->長網址

6.十進制整數與N進制(N<=62)的互轉


一、算法設計方案

1、固定長度分段(4組6位字符,任選一組)

32位md5串=4段*8位,其中的8位看成16進制a,(bin2hex(a) & 0x3fffffff)=30位=6段*5位,其中的5位看作16進制b,(b & 0x0000003D)=0~61
① 將長網址用md5算法生成32位簽名串,分爲4段,,每段8個字符;
② 對這4段循環處理,取每段的8個字符, 將他看成16進制字符串與0x3fffffff(30位1)的位與操作,超過30位的忽略處理;
③ 將每段得到的這30位又分成6段,每5位的數字與0x0000003D(61)的位與操作,結果作爲字母表的索引取得特定字符,依次進行獲得6位字符串;
④ 這樣一個md5字符串可以獲得4個6位串,取裏面的任意一個就可作爲這個長url的短url地址。

2. 可選長度分段(短鏈長度需能被30整除)

上文把短網址長度固定=6位,進一步可改進成動態調整的,如5、6、10、15位等,使其是30個質數之一,能被30整除就行。

1、固定長度分段(4組6位字符,任選一組)

function shortUrl($url_arr, $url_pre = 'https://t.cn/') {
    if (empty($url_arr) || !is_array($url_arr)) return false;
    $res = [];
    foreach($url_arr as $key => $url ) {
        $base62_arr = ['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','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'];
        //已經是短網址了就不要處理了。https://t.cn/abc經過basename後=abc
        if( strlen(basename($url))<=7 ) {
            $res[] = [$url, $url, $url, $url];
            continue;
        }
        $hex = md5($url);
        $subLen = strlen($hex) / 8;
        $output = [];
        for ($i = 0; $i < $subLen; $i++) {
            $subHex = substr ($hex, $i * 8, 8);
            $int = 0x3FFFFFFF & bin2hex($subHex);//0x3FFFFFFF & ('0x'.$subHex);不奏效
            $out = '';
            for ($j = 0; $j < 6; $j++) {
                $val = 0x0000003D & $int;
                $out .= $base62_arr[$val];
                $int = $int >> 5;
            }
            $output[] = $url_pre.$out;
        }
        $res[] = $output;   
    }
    return $res;
}
$res = shortUrl(['https://www.baidu.com/25420.html', 'https://t.cn/NVzmQn']);
var_dump(array_column($res, 3));die;//4組中任取1組,這裏取第4組
//結果=["https://t.cn/niu2Uz", "https://t.cn/NVzmQn"]

2. 可選長度分段(短鏈長度需能被30整除)

function shortUrl($url_arr, $url_len, $url_pre = 'https://t.cn/') {
    if (empty($url_arr) || !is_array($url_arr)) return false;
    $res = [];
    foreach($url_arr as $key => $url ) {
        $base62_arr = ['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','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'];
        //已經是短網址了就不要處理了。https://t.cn/abc經過basename後=abc
        if( strlen(basename($url))<=7 ) {
            $res[] = [$url, $url, $url, $url];
            continue;
        }
        $hex = md5($url);
        $subLen = strlen($hex) / 8;
        $output = [];
        for ($i = 0; $i < $subLen; $i++) {
            $subHex = substr ($hex, $i * 8, 8);
            $int = 0x3FFFFFFF & bin2hex($subHex);//0x3FFFFFFF & ('0x'.$subHex);不奏效
            $out = '';
            for ($j = 0; $j < $url_len; $j++) {
                $val = 0x0000003D & $int;
                $out .= $base62_arr[$val];
                $int = $int >> (30/$url_len);
            }
            $output[] = $url_pre.$out;
        }
        $res[] = $output;   
    }
    return $res;
}
$res = shortUrl(['https://www.baidu.com/25420.html'], 10);//短鏈長度10位
var_dump(array_column($res, 3));die;//4組中任取1組,這裏取第4組 
//結果=["https://t.cn/n7AIB27YRe"]

3.分佈式發號器snowflake(新短url與源url無關,與發號器有關)

snowflake生產的ID是一個18位的long型數字,二進制結構表示如下(每部分用-分開):
0 - 00000000 00000000 00000000 00000000 00000000 0 - 00000 - 00000 - 00000000 0000
1 + 41 + 10(10位workId或者5位datacenterId+5位workerId)+ 12 = 64位(恰好是一個Long型,轉換爲字符串長度爲18)
第1位未使用,
接下來41位爲毫秒級時間(41位的長度可以使用69年,從1970-01-0108:00:00開始),
然後是5位datacenterId(最大支持25=32個,二進制表示從00000-11111,也即是十進制0-31),和5位workerId(最大支持25=32個,原理同datacenterId),所以datacenterId*workerId最多支持部署1024個節點,
最後12位是毫秒內的計數(12位的計數順序號支持每個節點每毫秒產生2^12=4096個ID序號).
單臺機器實例,通過時間戳保證前41位是唯一的,分佈式系統多臺機器實例下,通過對每個機器實例分配不同的datacenterId和workerId避免中間的10位碰撞。最後12位每毫秒從0遞增生產ID,再提一次:每毫秒最多生成4096個ID,每秒可達4096000個(每秒400萬+個ID)。理論上,只要CPU計算能力足夠,單機每秒實測10w+

//親測可用

class SnowFlake {  
    const startMiltime =  1482394743339;//開始時間,固定一個小於當前時間的毫秒數即可.2016/12/22 16:19:03
    const datacenterIdBits = 5;//數據中心標識佔的位數
    const workerIdBits = 5;//機器標識佔的位數 
    const sequenceBits = 12;//毫秒內自增數點的位數
    protected $workId = 0;  //當前workid
    protected $datacenterId = 0;  //數據中心id
    protected $maxWorkerId = 0; //最大workid
    static $lastMiltime = -1;   //最新時間戳
    static $sequence = 0;  //發號頻率
  
    public function __construct($workId, $datacenterId){  
        //機器ID範圍判斷  
        $maxWorkerId = -1 ^ (-1 << self::workerIdBits);  //31
        $this->maxWorkerId = $maxWorkerId;
        if($workId > $maxWorkerId || $workId< 0){  
            throw new Exception("workerId can't be greater than ".$this->maxWorkerId." or less than 0");  
        }  
        //數據中心ID範圍判斷  
        $maxDatacenterId = -1 ^ (-1 << self::datacenterIdBits);  //31
        if ($datacenterId > $maxDatacenterId || $datacenterId < 0) {  
            throw new Exception("datacenter Id can't be greater than ".$this->maxDatacenterId." or less than 0");  
        }  
        //賦值  
        $this->workId = $workId;  
        $this->datacenterId = $datacenterId;  
    } 

    public function initProperty($totalProcess = 1, $pid = 1){//初始化成員屬性
        $this->workId = $pid + 1;
        $this->datacenterId = 1;
        $this->totalProcess = $totalProcess >= 1 ? $totalProcess : 1;
        //實例化雪花算法
        $this->snowflake = new SnowFlake($this->workId, $this->datacenterId); 
        //參與短地址計算的數組
        $this->base62 = [  
           "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" , "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"];
        return;
    }

    protected static function getCurMiltime(){//取當前時間毫秒 
        $timestramp = (float)sprintf("%.0f", microtime(true) * 1000);
        return  $timestramp;
    }  
   
    protected static function getNextMiltime($lastMiltime) {//取下一毫秒
        $timestamp = self::getCurMiltime();  
        while ($timestamp <= $lastMiltime) {  
            $timestamp = self::getCurMiltime();  
        }  
        return $timestamp;  
    }
   
    public function getUniqueId(){//發號器:雪花算法生成64位二進制ID(等價於18位的十進制ID) 
        $timestamp = self::getCurMiltime();
        $lastMiltime = self::$lastMiltime;  
        //判斷時鐘是否正常  
        if ($timestamp < $lastMiltime) {  
            throw new Exception("Clock moved backwards.  Refusing to generate id for %d milliseconds", ($lastMiltime - $timestamp));  
        }  
        //生成唯一序列  
        if ($lastMiltime == $timestamp) {  
            $sequenceMask = -1 ^ (-1 << self::sequenceBits);  
            self::$sequence = (self::$sequence + 1) & $sequenceMask; 
            if (self::$sequence == 0) {  
                $timestamp = self::getNextMiltime($lastMiltime);  
            }  
        } else {
            self::$sequence = 0;  
        }  
        self::$lastMiltime = $timestamp;  
        //  
        //時間毫秒/數據中心ID/機器ID,要左移的位數  
        $timestampLeftShift = self::sequenceBits + self::workerIdBits + self::datacenterIdBits;  //22
        $datacenterIdShift = self::sequenceBits + self::workerIdBits;  //17
        $workerIdShift = self::sequenceBits;  //12
        //組合4段數據返回: 時間戳.數據標識.工作機器.序列  
        $nextId = (($timestamp - (self::startMiltime)) << $timestampLeftShift) |  
            ($this->datacenterId << $datacenterIdShift) |  
            ($this->workId << $workerIdShift) | self::$sequence;  
        return $nextId;  
    }  

    public function createShortUrl($url_pre = 'https://t.cn/', $url_len = 6) {//從發號器的id生成短鏈
        if (30 % $url_len != 0) {//分段能被30整除
            return false;
        }
        $this->initProperty();//初始化成員屬性
        $id = $this->snowflake->getUniqueId();//先通過雪花算法計算出一個64位二進制ID(等價於18位的十進制ID)
        $secretKey = "&-$@①?h";//隨便寫1個
        $hex = md5($id.$secretKey);  
        $hexLen = strlen($hex); 
        $subHexLen = $hexLen / 8;   //將長網址md5生成32位簽名串,分爲4段,每段8個字節;
        $output = array();   
        for ($i = 0; $i < $subHexLen; $i++) {   
          $subHex = substr ($hex, $i * 8, 8);   //對這四段循環處理,取8個字節
          $int = 0x3FFFFFFF & bin2hex($subHex);//將他看成16進制串與0x3fffffff(30位1)與操作,即超過30位的忽略處理
          $out = '';   
          //將這30位分成(30/$url_len)段
          for ($j = 0; $j < $url_len; $j++) {   
            $val = 0x0000003D & $int;
            $out .= $this->base62[$val];   
            $int = $int >> (30/$url_len);  
          }   
          $output[] = $out;
        } 
        //總的md5串可以獲得4個$url_len位串;取裏面的任意一個就可作爲這個長url的短url地址;
        $newid = $url_pre.$output[mt_rand(0, 3)];
        unset($output,$out);
        return $newid;
    }
}  

調用:

$snowflake = new SnowFlake(0, 1);
$short_url = $snowflake->createShortUrl();
echo $short_url;die; // https://t.cn/BnEVjm

4.分佈式發號器的優化

多進程下 執行防止重複 將進程id 傳入賦值給 workid 就可以保證單機下多進程不重複

詳見:https://www.jianshu.com/p/5b3570373c62 

 

二、重點解答

1.短網址的長度?---長度<=7的62進制數(大小寫字母加數字),最多可生成568億個短鏈

答:長度不超過7的字符串,由大小寫字母加數字共62個字母組成。a-zA-Z0-9這62位取6位組合,可產生62^6=568億個組合數量

2.如何計算短網址?---分佈式發號器

答:分佈式發號器。

3.長網址與短網址的關係是一對一還是一對多映射?---一對多

答:一對多。

4.301還是302重定向?---302重定向

答:

5.如何預防攻擊?---redis緩存長網址->ID,而不是ID->長網址

答:


6.十進制整數與N進制(N<=62)的互轉

答:

 

 

 

 

 

 

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