• 雪花算法(Snowflake)


    雪花算法(Snowflake)

        雪花算法的背景

        新浪科技讯 北京时间2012年1月30日下午消息,据《时代周刊》报道,在龙年新春零点微博抢发活动中,新浪微博发博量峰值再创新高,龙年正月初一0点0分0秒,共有 32312 条微博同时发布,超过Twitter此前创下的每秒25088条的最高纪录。

        每秒钟3.2万条消息是什么概念?1秒钟有1千毫秒,相当于每毫秒有32条消息(3.2万/1000毫秒=32条/毫秒)。如果我们需要对每条消息产生一个ID呢?

        要求做到:(1)自增有序:只要求有序,并不要求连续;(2)全局唯一:要跨机器,跨时间

        雪花算法产生的背景当然是twitter高并发环境下对唯一ID生成的需求,雪花算法流传至今并被广泛使用。它至少有如下几个特点:

       1)能满足高并发分布式系统环境下ID不重复

       2)基于时间戳,可以保证基本有序递增(有些业务场景对这个有要求)

       3)算法本身不依赖第三方的库或者中间件

       4)生成效率极高

        UUID

        分布式系统中,有一些需要使用全局唯一ID的场景,这种时候为了防止ID冲突可以使用36位的UUID,但是UUID有一些缺点,首先他相对比较长,另外UUID一般是无序的。有些时候我们希望能使用一种简单一些的ID,并且希望ID能够按照时间有序生成。

       雪花算法的原理

       

        格式(64bit):1bit保留 + 41bit时间戳 + 10bit机器 + 12bit序列号

        1)1bit-不用,因为二进制中最高位是符号位,1表示负数,0表示正数,生成的id一般都是用整数,所以最高位固定为0.

        2)41bit-用来记录时间戳(毫秒)

        41位可以表示2^41−1个数字,如果只用来表示正整数(计算机中正数包含0),可以表示的数值范围是:0 至 2^41-1,减1是因为可表示的数值范围是从0开始算的,而不是1。也就是说41位可以表示2^41-1个毫秒的值,转化成单位年则是:

        (2^41−1)/(1000∗60∗60∗24∗365)=69年 ,也就是说这个时间戳可以使用69年不重复

        疑问

        41位能表示的最大的时间戳为2199023255552(1L<<41),则可使用的时间为2199023255552/(1000606024365)≈69年。但是这里有个问题是,时间戳2199023255552对应的时间应该是2039-09-07 23:47:35,距离现在只有不到20年的时间,为什么算出来的是69年呢?

        其实时间戳的算法是1970年1月1日到指点时间所经过的毫秒或秒数,那咱们把开始时间从2021年开始,就可以延长41位时间戳能表达的最大时间,所以这里实际指的是相对自定义开始时间的时间戳

        3)10bit-用来记录工作机器id

        a.   可以部署在2^10=1024个节点,包括5位datacenterId和5位workerId

        b.   5位(bit)可以表示的最大正整数是2^5−1=31,即可以用0、1、2、3、....31这32个数字,来表示不同的datecenterId或workerId

        4)12bit-序列号,用来记录同毫秒内产生的不同id。

        a.  12位(bit)可以表示的最大正整数是2^12−1=4095,即可以用0、1、2、3、....4094这4095个数字,来表示同一机器同一时间截(毫秒)内产生的4095个ID序号

        b.  snowFlake算法在同一毫秒内最多可以生成多少个全局唯一ID呢?

        同一毫秒的ID数量 = 1024 X 4096 = 4194304,所以最大可以支持单应用差不多四百万的并发量,这个妥妥的够用了

        说明:上面总体是64位,具体位数可自行配置,如想运行更久,需要增加时间戳位数;如想支持更多节点,可增加工作机器id位数;如想支持更高并发,增加序列号位数

        雪花算法的作用

        SnowFlake可以保证: 所有声称的id按时间趋势递增,整个分布式系统内不会产生重复id(因为有datacenterId和workerId来区分)

        数据中心ID、机器ID

        数据中心(机房)ID、机器ID一共10位,用于标识工作的计算机,在这里数据中心ID、机器ID各占5位。实际上,数据中心ID的位数、机器ID位数可根据实际情况进行调整,没有必要一定按1:1的比例分配来这10位

        雪花算法的实现

        雪花算法的实现主要依赖于数据中心ID数据节点ID这两个参数,具体使用PHP实现如下:

     1 <?php
     2 class SnowFlake
     3 {
     4     const TWEPOCH = 1625664871000; // 时间起始标记点,作为基准,一般取系统的最近时间
     5  
     6     const WORKER_ID_BITS     = 5; // 机器标识位数
     7     const DATACENTER_ID_BITS = 5; // 数据中心标识位数
     8     const SEQUENCE_BITS      = 12; // 毫秒内自增位
     9  
    10     private $workerId; // 工作机器ID
    11     private $datacenterId; // 数据中心ID
    12     private $sequence; // 毫秒内序列
    13  
    14     private $maxWorkerId     = -1 ^ (-1 << self::WORKER_ID_BITS); // 机器ID最大值
    15     private $maxDatacenterId = -1 ^ (-1 << self::DATACENTER_ID_BITS); // 数据中心ID最大值
    16  
    17     private $workerIdShift      = self::SEQUENCE_BITS; // 机器ID偏左移位数
    18     private $datacenterIdShift  = self::SEQUENCE_BITS + self::WORKER_ID_BITS; // 数据中心ID左移位数
    19     private $timestampLeftShift = self::SEQUENCE_BITS + self::WORKER_ID_BITS + self::DATACENTER_ID_BITS; // 时间毫秒左移位数
    20     private $sequenceMask       = -1 ^ (-1 << self::SEQUENCE_BITS); // 生成序列的掩码
    21  
    22     private $lastTimestamp = -1; // 上次生产id时间戳
    23  
    24     public function __construct($workerId, $datacenterId, $sequence = 0)
    25     {
    26         if ($workerId > $this->maxWorkerId || $workerId < 0) {
    27             throw new Exception("worker Id can't be greater than {$this->maxWorkerId} or less than 0");
    28         }
    29  
    30         if ($datacenterId > $this->maxDatacenterId || $datacenterId < 0) {
    31             throw new Exception("datacenter Id can't be greater than {$this->maxDatacenterId} or less than 0");
    32         }
    33  
    34         $this->workerId     = $workerId;
    35         $this->datacenterId = $datacenterId;
    36         $this->sequence     = $sequence;
    37     }
    38  
    39     public function nextId()
    40     {
    41         $timestamp = $this->timeGen();
    42  
    43         if ($timestamp < $this->lastTimestamp) {
    44             $diffTimestamp = bcsub($this->lastTimestamp, $timestamp);
    45             throw new Exception("Clock moved backwards.  Refusing to generate id for {$diffTimestamp} milliseconds");
    46         }
    47  
    48         if ($this->lastTimestamp == $timestamp) {
    49             $this->sequence = ($this->sequence + 1) & $this->sequenceMask;
    50  
    51             if (0 == $this->sequence) {
    52                 $timestamp = $this->tilNextMillis($this->lastTimestamp);
    53             }
    54         } else {
    55             $this->sequence = 0;
    56         }
    57  
    58         $this->lastTimestamp = $timestamp;
    59  
    60         /*$gmpTimestamp    = gmp_init($this->leftShift(bcsub($timestamp, self::TWEPOCH), $this->timestampLeftShift));
    61         $gmpDatacenterId = gmp_init($this->leftShift($this->datacenterId, $this->datacenterIdShift));
    62         $gmpWorkerId     = gmp_init($this->leftShift($this->workerId, $this->workerIdShift));
    63         $gmpSequence     = gmp_init($this->sequence);
    64         return gmp_strval(gmp_or(gmp_or(gmp_or($gmpTimestamp, $gmpDatacenterId), $gmpWorkerId), $gmpSequence));*/
    65  
    66         return (($timestamp - self::TWEPOCH) << $this->timestampLeftShift) |
    67         ($this->datacenterId << $this->datacenterIdShift) |
    68         ($this->workerId << $this->workerIdShift) |
    69         $this->sequence;
    70     }
    71  
    72     protected function tilNextMillis($lastTimestamp)
    73     {
    74         $timestamp = $this->timeGen();
    75         while ($timestamp <= $lastTimestamp) {
    76             $timestamp = $this->timeGen();
    77         }
    78  
    79         return $timestamp;
    80     }
    81  
    82     protected function timeGen()
    83     {
    84         return floor(microtime(true) * 1000);
    85     }
    86  
    87     // 左移 <<
    88     protected function leftShift($a, $b)
    89     {
    90         return bcmul($a, bcpow(2, $b));
    91     }
    92 }

        我们再看下easyswoole里面EasySwooleUtilitySnowFlake的实现:

     1 <?php
     2 
     3 namespace EasySwooleUtility;
     4 
     5 /**
     6  * 雪花算法生成器
     7  * Class SnowFlake
     8  * @author  : evalor <master@evalor.cn>
     9  * @package EasySwooleUtility
    10  */
    11 class SnowFlake
    12 {
    13     private static $lastTimestamp = 0;
    14     private static $lastSequence  = 0;
    15     private static $sequenceMask  = 4095;
    16     private static $twepoch       = 1508945092000;
    17 
    18     /**
    19      * 生成基于雪花算法的随机编号
    20      * @author : evalor <master@evalor.cn>
    21      * @param int $dataCenterID 数据中心ID 0-31
    22      * @param int $workerID     任务进程ID 0-31
    23      * @return int 分布式ID
    24      */
    25     static function make($dataCenterID = 0, $workerID = 0)
    26     {
    27         // 41bit timestamp + 5bit dataCenter + 5bit worker + 12bit
    28         $timestamp = self::timeGen();
    29         if (self::$lastTimestamp == $timestamp) {
    30             self::$lastSequence = (self::$lastSequence + 1) & self::$sequenceMask;
    31             if (self::$lastSequence == 0) $timestamp = self::tilNextMillis(self::$lastTimestamp);
    32         } else {
    33             self::$lastSequence = 0;
    34         }
    35         self::$lastTimestamp = $timestamp;
    36         $snowFlakeId = (($timestamp - self::$twepoch) << 22) | ($dataCenterID << 17) | ($workerID << 12) | self::$lastSequence;
    37         return $snowFlakeId;
    38     }
    39 
    40     /**
    41      * 反向解析雪花算法生成的编号
    42      * @author : evalor <master@evalor.cn>
    43      * @param int|float $snowFlakeId
    44      * @return stdClass
    45      */
    46     static function unmake($snowFlakeId)
    47     {
    48         $Binary = str_pad(decbin($snowFlakeId), 64, '0', STR_PAD_LEFT);
    49         $Object = new stdClass;
    50         $Object->timestamp = bindec(substr($Binary, 0, 42)) + self::$twepoch;
    51         $Object->dataCenterID = bindec(substr($Binary, 42, 5));
    52         $Object->workerID = bindec(substr($Binary, 47, 5));
    53         $Object->sequence = bindec(substr($Binary, -12));
    54         return $Object;
    55     }
    56 
    57     /**
    58      * 等待下一毫秒的时间戳
    59      * @author : evalor <master@evalor.cn>
    60      * @param $lastTimestamp
    61      * @return float
    62      */
    63     private static function tilNextMillis($lastTimestamp)
    64     {
    65         $timestamp = self::timeGen();
    66         while ($timestamp <= $lastTimestamp) {
    67             $timestamp = self::timeGen();
    68         }
    69         return $timestamp;
    70     }
    71 
    72     /**
    73      * 获取毫秒级时间戳
    74      * @author : evalor <master@evalor.cn>
    75      * @return float
    76      */
    77     private static function timeGen()
    78     {
    79         return (float)sprintf('%.0f', microtime(true) * 1000);
    80     }
    81 }

        时钟倒拨问题

         雪花算法的另一个难题就是时间倒拨,也就是跑了一段时间之后,系统时间回到过去。显然,时间戳上有很大几率产生相同毫秒数,在机器码workerId相同的情况下,有较大几率出现重复雪花Id。

         Snowflake根据SmartOS操作系统调度算法,初始化时锁定基准时间,并记录处理器时钟嘀嗒数。在需要生成雪花Id时,取基准时间与当时处理器时钟嘀嗒数,计算得到时间戳。也就是说,在初始化之后,Snowflake根本不会读取系统时间,即使时间倒拨,也不影响雪花Id的生成!

         还存在的几个问题

         1)工作机器ID可能会重复的问题

          机器 ID(5 位)和数据中心 ID(5 位)配置没有解决(不一定各是5位,可自行配置),分布式部署的时候会使用相同的配置,仍然有 ID 重复的风险。

     1     /**
     2      * 生成基于雪花算法的随机编号
     3      * @author : evalor <master@evalor.cn>
     4      * @param int $dataCenterID 数据中心ID 0-31
     5      * @param int $workerID     任务进程ID 0-31
     6      * @return int 分布式ID
     7      */
     8     static function make($dataCenterID = 0, $workerID = 0)
     9     {
    10         // 41bit timestamp + 5bit dataCenter + 5bit worker + 12bit
    11         $timestamp = self::timeGen();
    12         if (self::$lastTimestamp == $timestamp) {
    13             self::$lastSequence = (self::$lastSequence + 1) & self::$sequenceMask;
    14             if (self::$lastSequence == 0) $timestamp = self::tilNextMillis(self::$lastTimestamp);
    15         } else {
    16             self::$lastSequence = 0;
    17         }
    18         self::$lastTimestamp = $timestamp;
    19         $snowFlakeId = (($timestamp - self::$twepoch) << 22) | ($dataCenterID << 17) | ($workerID << 12) | self::$lastSequence;
    20         return $snowFlakeId;
    21     }

         问题具体描述:

         比如这里,生成ID使用:$randId = SnowFlake::make(1, 1)

         如果是在单节点中,这种固定的配置没有问题的,但是在分布式部署中,需要由dataCenterID和workerID组成唯一的机器码,否则在同毫秒内,在机器码workerId相同的情况下,有较大几率出现重复雪花Id。那么这个时候,dataCenterID和workerID的配置就不能写死。而且必须保证唯一。

         这里,提供两种解决思路

         第一种: workId 使用服务器 hostName 生成,dataCenterId 使用 IP 生成,这样可以最大限度防止 10 位机器码重复,但是由于两个 ID 都不能超过 32,只能取余数,还是难免产生重复,但是实际使用中,hostName 和 IP 的配置一般连续或相近,只要不是刚好相隔 32 位,就不会有问题,况且,hostName 和 IP 同时相隔 32 的情况更加是几乎不可能的事,平时做的分布式部署,一般也不会超过 10 台容器。

       

        注意:使用ip地址时要考虑到使用docker容器部署时ip可能会相同的情况。

        第二种:所有的节点共用一个数据库配置,每次节点重启,往mysql某个自建的表中新增一条数据,主键id自增并且自增id 要和 2^10-1 做按位与操作,防止总计重启次数超过 2^10 后溢出。使用这个自增的id作为机器码,这样能保证机器码绝对不重复。如果是TiDB这种分布式数据库(id自增分片不连续),按位与操作后,还要注意不能拿到相同的workId。

        2)分布式ID的浪费

     1     /**
     2      * 等待下一毫秒的时间戳
     3      * @author : evalor <master@evalor.cn>
     4      * @param $lastTimestamp
     5      * @return float
     6      */
     7     private static function tilNextMillis($lastTimestamp)
     8     {
     9         $timestamp = self::timeGen();
    10         while ($timestamp <= $lastTimestamp) {
    11             $timestamp = self::timeGen();
    12         }
    13         return $timestamp;
    14     }

           因为序列号是每毫秒最多可以生成4096个id,所以在序列号到达最大值的时候,程序会循环直到获取下一个合适的时间戳,但是这个跨度不一定是1毫秒,取决于程序执行的时间,如果时间跨度超过1毫秒,那么在分布式ID服务运行期间,因为没有应用调用接口来获取,因而就被浪费掉了

    参考链接:

    http://www.php20.cn/article/261

    https://www.cnblogs.com/wt645631686/p/13173602.html

  • 相关阅读:
    解决nginx启动后进程存在但访问不到的问题
    验证nginx配置文件遇到的路径问题
    前端npm run build打包和tar压缩示例讲解
    安装vuecodemirror支持SQL可视化
    redis学习之安装
    nginx版本升级详解
    前端二次非对称RSA加密密文太长的问题
    听《Node服务线上故障》分享的思考
    fastdfs部署及官网
    vue踩坑
  • 原文地址:https://www.cnblogs.com/hld123/p/14976414.html
Copyright © 2020-2023  润新知