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