雪花算法(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