雪花算法原理介紹及基於php的雪花算法(snowflake)


原理介紹(摘自極客時間):

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));
    }
}
 

 


免責聲明!

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



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