雪花算法(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、....4095这4096个数字,来表示同一机器同一时间截(毫秒)内产生的4096个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里面EasySwoole\Utility\SnowFlake的实现:

 1 <?php
 2 
 3 namespace EasySwoole\Utility;
 4 
 5 /**
 6  * 雪花算法生成器
 7  * Class SnowFlake
 8  * @author  : evalor <master@evalor.cn>
 9  * @package EasySwoole\Utility
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

posted @ 2021-07-06 13:01  欢乐豆123  阅读(4906)  评论(0编辑  收藏  举报