原理介紹(摘自極客時間):
Snowflake的核心思想是將64bit的二進制數字分成若干部分,每一部分都存儲有特定含義的數據,比如說時間戳、機器ID、序列號等等,最終生成全局唯一的有序ID。它的標准算法是這樣的:
從上面這張圖中我們可以看到,41位的時間戳大概可以支撐pow(2,41)/1000/60/60/24/365年,約等於69年,對於一個系統是足夠了。
如果你的系統部署在多個機房,那么10位的機器ID可以繼續划分為2~3位的IDC標示(可以支撐4個或者8個IDC機房)和7~8位的機器ID(支持128-256台機器);12位的序列號代表着每個節點每毫秒最多可以生成4096的ID。
不同公司也會依據自身業務的特點對Snowflake算法做一些改造,比如說減少序列號的位數增加機器ID的位數以支持單IDC更多的機器,也可以在其中加入業務ID字段來區分不同的業務。比方說我現在使用的發號器的組成規則就是:1位兼容位恆為0 + 41位時間信息 + 6位IDC信息(支持64個IDC)+ 6位業務信息(支持64個業務)+ 10位自增信息(每毫秒支持1024個號)
我選擇這個組成規則,主要是因為我在單機房只部署一個發號器的節點,並且使用KeepAlive保證可用性。業務信息指的是項目中哪個業務模塊使用,比如用戶模塊生成的ID,內容模塊生成的ID,把它加入進來,一是希望不同業務發出來的ID可以不同,二是因為在出現問題時可以反解ID,知道是哪一個業務發出來的ID。
那么了解了Snowflake算法的原理之后,我們如何把它工程化,來為業務生成全局唯一的ID呢?一般來說我們會有兩種算法的實現方式:
一種是嵌入到業務代碼里,也就是分布在業務服務器中。這種方案的好處是業務代碼在使用的時候不需要跨網絡調用,性能上會好一些,但是就需要更多的機器ID位數來支持更多的業務服務器。另外,由於業務服務器的數量很多,我們很難保證機器ID的唯一性,所以就需要引入ZooKeeper等分布式一致性組件來保證每次機器重啟時都能獲得唯一的機器ID。
另外一個部署方式是作為獨立的服務部署,這也就是我們常說的發號器服務。業務在使用發號器的時候就需要多一次的網絡調用,但是內網的調用對於性能的損耗有限,卻可以減少機器ID的位數,如果發號器以主備方式部署,同時運行的只有一個發號器,那么機器ID可以省略,這樣可以留更多的位數給最后的自增信息位。即使需要機器ID,因為發號器部署實例數有限,那么就可以把機器ID寫在發號器的配置文件里,這樣即可以保證機器ID唯一性,也無需引入第三方組件了。微博和美圖都是使用獨立服務的方式來部署發號器的,性能上單實例單CPU可以達到兩萬每秒。
Snowflake算法設計的非常簡單且巧妙,性能上也足夠高效,同時也能夠生成具有全局唯一性、單調遞增性和有業務含義的ID,但是它也有一些缺點,其中最大的缺點就是它依賴於系統的時間戳,一旦系統時間不准,就有可能生成重復的ID。所以如果我們發現系統時鍾不准,就可以讓發號器暫時拒絕發號,直到時鍾准確為止。
另外,如果請求發號器的QPS不高,比如說發號器每毫秒只發一個ID,就會造成生成ID的末位永遠是1,那么在分庫分表時如果使用ID作為分區鍵就會造成庫表分配的不均勻。這一點,也是我在實際項目中踩過的坑,而解決辦法主要有兩個:
1.時間戳不記錄毫秒而是記錄秒,這樣在一個時間區間里可以多發出幾個號,避免出現分庫分表時數據分配不均。
2.生成的序列號的起始號可以做一下隨機,這一秒是21,下一秒是30,這樣就會盡量的均衡了。
我在開頭提到,自己的實際項目中采用的是變種的Snowflake算法,也就是說對Snowflake算法進行了一定的改造,從上面的內容中你可以看出,這些改造:一是要讓算法中的ID生成規則符合自己業務的特點;二是為了解決諸如時間回撥等問題。
其實,大廠除了采取Snowflake算法之外,還會選用一些其他的方案,比如滴滴和美團都有提出基於數據庫生成ID的方案。這些方法根植於公司的業務,同樣能解決分布式環境下ID全局唯一性的問題。對你而言,可以多角度了解不同的方法,這樣能夠尋找到更適合自己業務目前場景的解決方案,不過我想說的是,方案不在多,而在精,方案沒有最好,只有最適合,真正弄懂方法背后的原理,並將它落地,才是你最佳的選擇。
第一種:
簡介:
最高位是符號位,始終為0,不可用。
41位的時間序列,精確到毫秒級,41位的長度可以使用69年。時間位還有一個很重要的作用是可以根據時間進行排序。
10位的機器標識,10位的長度最多支持部署1024個節點。
12位的計數序列號,序列號即一系列的自增id,可以支持同一節點同一毫秒生成多個ID序號,12位的計數序列號支持每個節點每毫秒產生4096個ID序號。
看的出來,這個算法很簡潔也很簡單,但依舊是一個很好的ID生成策略。其中,10位器標識符一般是5位IDC+5位machine編號,唯一確定一台機器。
<?php class lib_snowflake { const TWEPOCH = 1488834974657; // 時間起始標記點,作為基准,一般取系統的最近時間(一旦確定不能變動) const WORKER_ID_BITS = 5; // 機器標識位數 const DATACENTER_ID_BITS = 5; // 數據中心標識位數 const SEQUENCE_BITS = 11; // 毫秒內自增位 private $workerId; // 工作機器ID(0~31) private $datacenterId; // 數據中心ID(0~31) private $sequence; // 毫秒內序列(0~4095) private $maxWorkerId = -1 ^ (-1 << self::WORKER_ID_BITS); // 機器ID最大值31 private $maxDatacenterId = -1 ^ (-1 << self::DATACENTER_ID_BITS); // 數據中心ID最大值31 private $workerIdShift = self::SEQUENCE_BITS; // 機器ID偏左移11位 private $datacenterIdShift = self::SEQUENCE_BITS + self::WORKER_ID_BITS; // 數據中心ID左移16位 private $timestampLeftShift = self::SEQUENCE_BITS + self::WORKER_ID_BITS + self::DATACENTER_ID_BITS; // 時間毫秒左移21位 private $sequenceMask = -1 ^ (-1 << self::SEQUENCE_BITS); // 生成序列的掩碼4095 private $lastTimestamp = -1; // 上次生產id時間戳 public function __construct($workerId, $datacenterId, $sequence = 0) { if ($workerId > $this->maxWorkerId || $workerId < 0) { throw new Exception("worker Id can't be greater than {$this->maxWorkerId} or less than 0"); } if ($datacenterId > $this->maxDatacenterId || $datacenterId < 0) { throw new Exception("datacenter Id can't be greater than {$this->maxDatacenterId} or less than 0"); } $this->workerId = $workerId; $this->datacenterId = $datacenterId; $this->sequence = $sequence; } public function nextId() { $timestamp = $this->timeGen(); if ($timestamp < $this->lastTimestamp) { $diffTimestamp = bcsub($this->lastTimestamp, $timestamp); throw new Exception("Clock moved backwards. Refusing to generate id for {$diffTimestamp} milliseconds"); } if ($this->lastTimestamp == $timestamp) { $this->sequence = ($this->sequence + 1) & $this->sequenceMask; if (0 == $this->sequence) { $timestamp = $this->tilNextMillis($this->lastTimestamp); } } else { $this->sequence = 0; } $this->lastTimestamp = $timestamp; $gmpTimestamp = gmp_init($this->leftShift(bcsub($timestamp, self::TWEPOCH), $this->timestampLeftShift)); $gmpDatacenterId = gmp_init($this->leftShift($this->datacenterId, $this->datacenterIdShift)); $gmpWorkerId = gmp_init($this->leftShift($this->workerId, $this->workerIdShift)); $gmpSequence = gmp_init($this->sequence); return gmp_strval(gmp_or(gmp_or(gmp_or($gmpTimestamp, $gmpDatacenterId), $gmpWorkerId), $gmpSequence)); } protected function tilNextMillis($lastTimestamp) { $timestamp = $this->timeGen(); while ($timestamp <= $lastTimestamp) { $timestamp = $this->timeGen(); } return $timestamp; } protected function timeGen() { return floor(microtime(true) * 1000); } // 左移 << protected function leftShift($a, $b) { return bcmul($a, bcpow(2, $b)); } }
第二種:
<?php class SnowFlake { const TWEPOCH = 1288834974657; // 時間起始標記點,作為基准,一般取系統的最近時間(一旦確定不能變動) const WORKER_ID_BITS = 5; // 機器標識位數 const DATACENTER_ID_BITS = 5; // 數據中心標識位數 const SEQUENCE_BITS = 12; // 毫秒內自增位 private $workerId; // 工作機器ID private $datacenterId; // 數據中心ID private $sequence; // 毫秒內序列 private $maxWorkerId = -1 ^ (-1 << self::WORKER_ID_BITS); // 機器ID最大值 private $maxDatacenterId = -1 ^ (-1 << self::DATACENTER_ID_BITS); // 數據中心ID最大值 private $workerIdShift = self::SEQUENCE_BITS; // 機器ID偏左移位數 private $datacenterIdShift = self::SEQUENCE_BITS + self::WORKER_ID_BITS; // 數據中心ID左移位數 private $timestampLeftShift = self::SEQUENCE_BITS + self::WORKER_ID_BITS + self::DATACENTER_ID_BITS; // 時間毫秒左移位數 private $sequenceMask = -1 ^ (-1 << self::SEQUENCE_BITS); // 生成序列的掩碼 private $lastTimestamp = -1; // 上次生產id時間戳 public function __construct($workerId, $datacenterId, $sequence = 0) { if ($workerId > $this->maxWorkerId || $workerId < 0) { throw new Exception("worker Id can't be greater than {$this->maxWorkerId} or less than 0"); } if ($datacenterId > $this->maxDatacenterId || $datacenterId < 0) { throw new Exception("datacenter Id can't be greater than {$this->maxDatacenterId} or less than 0"); } $this->workerId = $workerId; $this->datacenterId = $datacenterId; $this->sequence = $sequence; } public function nextId() { $timestamp = $this->timeGen(); if ($timestamp < $this->lastTimestamp) { $diffTimestamp = bcsub($this->lastTimestamp, $timestamp); throw new Exception("Clock moved backwards. Refusing to generate id for {$diffTimestamp} milliseconds"); } if ($this->lastTimestamp == $timestamp) { $this->sequence = ($this->sequence + 1) & $this->sequenceMask; if (0 == $this->sequence) { $timestamp = $this->tilNextMillis($this->lastTimestamp); } } else { $this->sequence = 0; } $this->lastTimestamp = $timestamp; /*$gmpTimestamp = gmp_init($this->leftShift(bcsub($timestamp, self::TWEPOCH), $this->timestampLeftShift)); $gmpDatacenterId = gmp_init($this->leftShift($this->datacenterId, $this->datacenterIdShift)); $gmpWorkerId = gmp_init($this->leftShift($this->workerId, $this->workerIdShift)); $gmpSequence = gmp_init($this->sequence); return gmp_strval(gmp_or(gmp_or(gmp_or($gmpTimestamp, $gmpDatacenterId), $gmpWorkerId), $gmpSequence));*/ return (($timestamp - self::TWEPOCH) << $this->timestampLeftShift) | ($this->datacenterId << $this->datacenterIdShift) | ($this->workerId << $this->workerIdShift) | $this->sequence; } protected function tilNextMillis($lastTimestamp) { $timestamp = $this->timeGen(); while ($timestamp <= $lastTimestamp) { $timestamp = $this->timeGen(); } return $timestamp; } protected function timeGen() { return floor(microtime(true) * 1000); } // 左移 << protected function leftShift($a, $b) { return bcmul($a, bcpow(2, $b)); } }