全局唯一的短地址服务实现

一、业务背景:

将长链接转化成“指定的短域名+字母数字组合”不重复的URL短地址,用于业务线的常规业务及日常推广使用,基于此需求 需要实现一套简单高效的全局唯一的5位字母数字组合的短地址

在实现出上述需求的短地址之前 我们先讨论下 有几种办法可以生成一个全局唯一ID供后续生成5位字母组合使用

先说有几种唯一id的实现方法:

1、数据库自增长ID

优势:无需编码

缺陷:直接自增,不够随机,对外暴露信息过多。高并发下插入数据需要加入事务机制

2、时间戳+随机数

优势:编码简单

缺陷:随机数存在重复问题,即使在相同的时间戳下。每次插入数据库前需要校验下是否已经存在相同的数值

3、UUID

优势:简单

劣势:用户不友好,索引关联效率较低、影响性能。

4、今天想聊的是twitter开源的一款分布式自增ID算法snowflake

snowflake算法是一款本地生成的(ID生成过程不依赖任何中间件,无网络通信),保证ID全局唯一,并且ID总体有序递增,性能每秒生成10w+、完全可以满足大部分的业务要求。

二、snowflake算法原理

snowflake生产的ID是一个18位的long型数字,二进制结构表示如下(每部分用-分开):

0 - 00000000 00000000 00000000 00000000 00000000 0 - 00000 - 00000 - 00000000 0000

第一位未使用,接下来的41位为毫秒级时间(41位的长度可以使用69年,从1970-01-01

08:00:00),然后是5位datacenterId(最大支持25=32个,二进制表示从00000-11111,也即是十进制0-31),和5位workerId(最大支持25=32个,原理同datacenterId),所以datacenterId*workerId最多支持部署1024个节点,最后12位是毫秒内的计数(12位的计数顺序号支持每个节点每毫秒产生2^12=4096个ID序号).

所有位数加起来共64位,恰好是一个Long型(转换为字符串长度为18).

单台机器实例,通过时间戳保证前41位是唯一的,分布式系统多台机器实例下,通过对每个机器实例分配不同的datacenterId和workerId避免中间的10位碰撞。最后12位每毫秒从0递增生产ID,再提一次:每毫秒最多生成4096个ID,每秒可达4096000个。理论上,只要CPU计算能力足够,单机每秒实测10w+,效率之高由此可见。

三、snowflake算法源码(PHP版)

    /**
 * Description of SnowFlake
 * 经典的分布式发号器 雪花算法
 * @date 2018-4-19 16:23:21
 */  
class SnowFlake
{  
    //开始时间,固定一个小于当前时间的毫秒数即可  
    const twepoch =  1482394743339;//2016/12/22 16:19:03
    //机器标识占的位数  
    const workerIdBits = 5;  
    //数据中心标识占的位数  
    const datacenterIdBits = 5;  
    //毫秒内自增数点的位数  
    const sequenceBits = 12;  
    protected $workId = 0;  //当前workid
    protected $datacenterId = 0;  //数据中心id
    protected $maxWorkerId = 0; //最大workid

    static $lastTimestamp = -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;  
    }  
  
    //生成一个ID  
    public function nextId(){  
        $timestamp = self::timeGen();  
        $lastTimestamp = self::$lastTimestamp;  
        //判断时钟是否正常  
        if ($timestamp < $lastTimestamp) {  
            throw new Exception("Clock moved backwards.  Refusing to generate id for %d milliseconds", ($lastTimestamp - $timestamp));  
        }  
        //生成唯一序列  
        if ($lastTimestamp == $timestamp) {  
            $sequenceMask = -1 ^ (-1 << self::sequenceBits);  
            self::$sequence = (self::$sequence + 1) & $sequenceMask;  
            if (self::$sequence == 0) {  
                $timestamp = self::tilNextMillis($lastTimestamp);  
            }  
        } else {  
            self::$sequence = 0;  
        }  
        self::$lastTimestamp = $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::twepoch)) << $timestampLeftShift) |  
            ($this->datacenterId << $datacenterIdShift) |  
            ($this->workId << $workerIdShift) | self::$sequence;  
        return $nextId;  
    }  
  
    //取当前时间毫秒  
    protected static function timeGen(){  
        $timestramp = (float)sprintf("%.0f", microtime(true) * 1000);  
        return  $timestramp;  
    }  
  
    //取下一毫秒  
    protected static function tilNextMillis($lastTimestamp) {  
        $timestamp = self::timeGen();  
        while ($timestamp <= $lastTimestamp) {  
            $timestamp = self::timeGen();  
        }  
        return $timestamp;  
    }  
}  

四、snowflake算法推导和演算过程

说明:

演算使用的对象实例:$snowflake = new SnowFlake(1, 1);

运行时数据workerId=1,datacenterId=1,分别表示机器实例的生产者编号,数据中心编号;

sequence=0表示每毫秒生产ID从0开始计数递增;

以下演算基于时间戳=1482394743339时刻进行推导。

一句话描述:以下演算模拟了1482394743339这一毫秒时刻,workerId=1,datacenterId=1的id生成器,生产第一个id的过程。

图片.png

(图片转载自:https://blog.csdn.net/li396864285/article/details/54668031

五、派号器根据snowflake去生成唯一的5位字母数字组合的短地址

/*
     * 多进程 短地址发号器
     * @date 2018-04-20 14:09
     */
    public function ticketAction(){
        //初始化成员属性
        $this->initializeAttribute();
        //检测队列剩余量 大于10万的话不生成
        $queueNum = $this->redisService->llen($this->config->redisCommonShortUrlQueue);
        if($queueNum > self::lastQueueNums){
            echo "Date: " . date("Y-m-d H:i:s", time()) . "the data ".$queueNum." is full".PHP_EOL;
            exit();
        }
        for ($i = 0; $i < 10; $i ++) {
            $pid = pcntl_fork();
            if ($pid == -1) {
                echo 'could not fork';
                continue;
            } else if ($pid) {
                pcntl_wait($status);
            } else {
                //通过pipeline批量生成
                $pipe = $this->redisService->multi(Redis::PIPELINE);  
                for($j = 0; $j < self::perForkNums;$j ++) {  
                    $newid = $this->patchTicketAction();
                    //压入队列
                    $pipe->lpush($this->config->redisCommonShortUrlQueue,$newid);
                } 
                $result = $pipe->exec();
                print_r($result);
                unset($result);
                echo "Date: " . date("Y-m-d H:i:s", time()) . "the ".$i." child process is end".PHP_EOL;
                exit();
            }
        }
    }
    

/*
     * 发号器生成派发
     * 压入队列 进入去重库
     * @date 2018-04-20 14:09
     * @return string
     */
    public function patchTicketAction(){
        //初始化成员属性
        $this->initializeAttribute();
        //先通过雪花算法 计算出一个key
        $id = $this->snowflake->nextId(); 
        //echo $id.PHP_EOL;
        $secretKey = "a①d shor€t?";
        $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 & (1 * ('0x'.$subHex));    //将他看成16进制串与0x3fffffff(30位1)与操作,即超过30位的忽略处理
          $out = '';   
          //将这30位分成5段
          for ($j = 0; $j < 5; $j++) {   
            $val = 0x0000001F & $int;   //每5位的数字作为字母表的索引取得特定字符,依次进行获得5位字符串;
            $out .= $this->base32[$val];   
            $int = $int >> 5;  
          }   
          $output[] = $out;   
        } 
        //总的md5串可以获得4个5位串;取里面的任意一个就可作为这个长url的短url地址;
        $newid = $output[mt_rand(0, 3)];
        unset($output,$out);
        return $newid;
    }

   /**
     * 初始化成员属性
     * @return null
     */
    public function initializeAttribute($totalProcess = 1, $pid = 1)
    {
        $this->workId = $pid + 1;
        $this->datacenterId = 1;
        $this->totalProcess = $totalProcess >= 1 ? $totalProcess : 1;
        $this->pid = $pid;
        //实例化redis
        $this->redisService = new RedisService();
        //实例化雪花算法
        $this->snowflake = new SnowFlake($this->workId, $this->datacenterId); 
        //参与短地址计算的数组
        $this->base32 = [  
           "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" , "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;
    }

经多进程测试 上亿条 没出现重复

注意

多进程下 执行防止重复 将进程id 传入赋值给 workid 就可以保证单机下多进程不重复
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 219,366评论 6 508
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,521评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 165,689评论 0 356
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,925评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,942评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,727评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,447评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,349评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,820评论 1 317
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,990评论 3 337
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,127评论 1 351
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,812评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,471评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,017评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,142评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,388评论 3 373
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,066评论 2 355

推荐阅读更多精彩内容