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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM