多种方式生成短网址(分布式发号器)解读+实现(亲测可用)

目录

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)的互转

答:

猜你喜欢

转载自blog.csdn.net/wuhuagu_wuhuaguo/article/details/106696903
今日推荐