什麼是 SnowFlake 算法❓

目錄

問在最前:

方法一:UUID

方法二:數據庫自增主鍵

拋磚引玉:你聽說過 SnowFlake 算法嗎?

1、初始 SnowFlake

2、SnowFlake 的代碼實現

3、SnowFlake 的優勢和劣勢


問在最前:

如何在分佈式集羣中,生成全局唯一的 ID❓

方法一:UUID

UUID 是通用唯一識別碼 (Universally Unique Identifier),在其他語言中也叫 GUID,可以生成一個長度32位的全局唯一識別碼。

Python3 代碼:

import uuid

# uuid1() 基於時間戳,Python 中 UUID 主要有五個算法,當前我們僅用第一種

#由 MAC 地址,當前時間戳,隨機數字生成。可以保證全球範圍內的唯一性

# 但是由於 MAC 地址的使用同時帶來了安全問題,局域網中可以使用 IP 來代替 MAC

uuid1 = uuid.uuid1()

print(uuid1)

結果展示:
1698126c-3a61-11ea-9b12-b083fec202a4

缺點也很明顯:UUID 雖然可以保證全局唯一,但是佔用 32 位有些太長,而且是無序的,入庫時性能比較差。

疑問:爲什麼無序的 UUID 會導致入庫性能變差呢?

這就涉及到 B+樹(關於什麼是 B+樹可查看:什麼是 B+樹❓) 索引的分裂

 

衆所周知,關係型數據庫的索引大都是 B+樹結構,像 MySQL,拿 ID 字段來舉例,索引樹的每一個節點都存儲着若干個 ID。

如果我們的 ID 按遞增的順序來插入,比如陸續插入 8、9、10,新的 ID 都只會插入到最後一個節點當中。當最後一個節點滿了,會裂變出新的節點。這樣的插入是性能比較高的插入,因爲這樣節點的分裂次數最少,而且充分利用了每一個節點的空間。

 

但是,如果我們的插入玩去無序,不但會導致一些中間節點產生分裂,也會白白創造出很多不飽和的節點,這樣大大降低了數據庫插入的性能。

方法二:數據庫自增主鍵

假設名爲 table 的表有如下結構:

id name
37 wufei

每一次生成 ID 的時候,訪問數據庫,執行下面的語句:

begin;
REPLACE INTO table(name) VALUES('wufei');
SELECT LAST_INSERT_ID();
commit;

REPLACE INTO 的含義是插入一條記錄,如果表中唯一索引的值遇到衝突,則替換老數據。

這樣一來,每次都可以得到一個遞增的 ID。

爲了提高性能,在分佈式系統中可以用 DB proxy(如 mycat)請求不同的分庫,每個分庫設置不同的初始值,步長和分庫數量相等:

 

在 MySQL 中配置步長:auto_increment_increment =2

在 MySQL 中配置起始:auto_increment_offset =1(如 DB01)

這樣一來,DB1生成的ID是1,4,7,10,13....,DB2生成的ID是2,5,8,11,14.....

起始確定也很明顯:ID 的生成對數據庫嚴重依賴,不但影響性能,而且一旦數據庫掛掉,服務將變得不可用。

拋磚引玉:你聽說過 SnowFlake 算法嗎?

SnowFlake 是 Twitter 公司所採用的一種算法,目的是在分佈式系統中生成全局唯一且趨勢遞增的 ID。

1、初始 SnowFlake

SnowFlake 算法所生成的 ID 結構是什麼樣子呢?我們來看看下圖:

 

SnowFlake 所生成的 ID 一共分成四部分:

1> 第一位

佔用 1bit,其值始終是 0,沒有實際作用。

2> 時間戳

佔用 41bit,精確到毫秒,總共可以容納 69 年的時間。

3> 工作機器 ID

佔用 10bit,其中高位 5bit 是數據中心 ID(datacenterID),地位 5bit 是工作節點 ID(workerID),最多可以容納 1024 個節點。

4> 序列號

佔用 12bit,這個值在同一毫秒內最多可以生成多少個全局唯一 ID 呢?只需要做一個簡單的乘法:

同一毫秒的 ID 數量 = 1024 X 4096 = 4194304(即 3> 和 4> 相乘)

這個數字絕大多數併發場景下都是夠用的。

2、SnowFlake 的代碼實現

是不是覺得 SnowFlake 算法還真是強大,可是如何用代碼來實現它呢?

其實並不難實現,來看一看 Python3 實現代碼(至於代碼中初始時間戳爲什麼爲 2014-08-22 00:00:00,不多說😀)。

有人可能會問:爲什麼要減去一個初始時間?

那是因爲 41 位字節作爲時間戳數值的話,大約 68 年就會用完,如果不減去 2014-08-22,那麼白白浪費 40 多年的時間戳!一般初始時間爲該項目開始成立的時間

話不多說,快快上碼:

#!/usr/bin/env python3
# -*- coding: UTF-8 -*-

'''=================================================================================
@Project -> File   :python_imggen -> snowflake_id.py
@IDE    :PyCharm
@Author :Mr. Wufei
@Date   :2020/1/19 11:23
@Desc   :通過雪花算法(SnowFlake)用 Python3 實現一個簡單的發號器
@CSDN   :什麼是 SnowFlake(https://showufei.blog.csdn.net/article/details/104041576)
=================================================================================='''

import sys
import time
import logging

class MySnow(object):

    def __init__(self, datacenter_id, worker_id):
        # 初始毫秒級時間戳(2014-08-22)
        self.initial_time_stamp = int(time.mktime(time.strptime('2014-08-22 00:00:00', "%Y-%m-%d %H:%M:%S")) * 1000)
        # 機器 ID 所佔的位數
        self.worker_id_bits = 5
        # 數據表示 ID 所佔的位數
        self.datacenter_id_bits = 5
        # 支持的最大機器 ID,結果是 31(這個位移算法可以很快的計算出幾位二進制數所能表示的最大十進制數)
        # 2**5-1 0b11111
        self.max_worker_id = -1 ^ (-1 << self.worker_id_bits)
        # 支持最大標識 ID,結果是 31
        self.max_datacenter_id = -1 ^ (-1 << self.datacenter_id_bits)
        # 序列號 ID所佔的位數
        self.sequence_bits = 12
        # 機器 ID 偏移量(12)
        self.workerid_offset = self.sequence_bits
        # 數據中心 ID 偏移量(12 + 5)
        self.datacenterid_offset = self.sequence_bits + self.datacenter_id_bits
        # 時間戳偏移量(12 + 5 + 5)
        self.timestamp_offset = self.sequence_bits + self.datacenter_id_bits + self.worker_id_bits
        # 生成序列的掩碼,這裏爲 4095(0b111111111111 = 0xfff = 4095)
        self.sequence_mask = -1 ^ (-1 << self.sequence_bits)

        # 初始化日誌
        self.logger = logging.getLogger('snowflake')

        # 數據中心 ID(0 ~ 31)
        if datacenter_id > self.max_datacenter_id or datacenter_id < 0:
            err_msg = 'datacenter_id 不能大於 %d 或小於 0' % self.max_worker_id
            self.logger.error(err_msg)
            sys.exit()
        self.datacenter_id = datacenter_id
        # 工作節點 ID(0 ~ 31)
        if worker_id > self.max_worker_id or worker_id < 0:
            err_msg = 'worker_id 不能大於 %d 或小於 0' % self.max_worker_id
            self.logger.error(err_msg)
            sys.exit()
        self.worker_id = worker_id
        # 毫秒內序列(0 ~ 4095)
        self.sequence = 0
        # 上次生成 ID 的時間戳
        self.last_timestamp = -1

    def _gen_timestamp(self):
        """
        生成整數毫秒級時間戳
        :return: 整數毫秒級時間戳
        """
        return int(time.time() * 1000)

    def next_id(self):
        """
        獲得下一個ID (用同步鎖保證線程安全)
        :return: snowflake_id
        """
        timestamp = self._gen_timestamp()
        # 如果當前時間小於上一次 ID 生成的時間戳,說明系統時鐘回退過這個時候應當拋出異常
        if timestamp < self.last_timestamp:
            self.logger.error('clock is moving backwards. Rejecting requests until {}'.format(self.last_timestamp))
        # 如果是同一時間生成的,則進行毫秒內序列
        if timestamp == self.last_timestamp:
            self.sequence = (self.sequence + 1) & self.sequence_mask
            # sequence 等於 0 說明毫秒內序列已經增長到最大值
            if self.sequence == 0:
                # 阻塞到下一個毫秒,獲得新的時間戳
                timestamp = self._til_next_millis(self.last_timestamp)
        else:
            # 時間戳改變,毫秒內序列重置
            self.sequence = 0

        # 上次生成 ID 的時間戳
        self.last_timestamp = timestamp

        # 移位並通過或運算拼到一起組成 64 位的 ID
        new_id = ((timestamp - self.initial_time_stamp) << self.timestamp_offset) | \
                 (self.datacenter_id << self.datacenterid_offset) | \
                 (self.worker_id << self.workerid_offset) | \
                 self.sequence
        return new_id

    def _til_next_millis(self, last_timestamp):
        """
        阻塞到下一個毫秒,直到獲得新的時間戳
        :param last_timestamp: 上次生成 ID 的毫秒級時間戳
        :return: 當前毫秒級時間戳
        """
        timestamp = self._gen_timestamp()
        while timestamp <= last_timestamp:
            timestamp = self._gen_timestamp()
        return timestamp

"""
if __name__ == '__main__':
    mysnow = MySnow(1, 2)
    id = mysnow.next_id()
    print(id)
"""

這段代碼改寫自網上 Java 實現的 SnowFlake 算法,Java 代碼如下:

//始時間截 (2017-01-01)
private static final long INITIAL_TIME_STAMP = 1483200000000L;

//機器id所佔的位數
private static final long WORKER_ID_BITS = 5L;

//數據標識id所佔的位數
private static final long DATACENTER_ID_BITS = 5L;

//支持的最大機器id,結果是31 (這個移位算法可以很快的計算出幾位二進制數所能表示的最大十進制數)
private static final long MAX_WORKER_ID = ~(-1L << WORKER_ID_BITS);

//支持的最大數據標識id,結果是31
private static final long MAX_DATACENTER_ID = ~(-1L << DATACENTER_ID_BITS);

//序列在id中佔的位數
private final long SEQUENCE_BITS = 12L;

//機器ID的偏移量(12)
private final long WORKERID_OFFSET = SEQUENCE_BITS;

//數據中心ID的偏移量(12+5)
private final long DATACENTERID_OFFSET = SEQUENCE_BITS + SEQUENCE_BITS;

//時間截的偏移量(5+5+12)
private final long TIMESTAMP_OFFSET = SEQUENCE_BITS + WORKER_ID_BITS + DATACENTER_ID_BITS;

//生成序列的掩碼,這裏爲4095 (0b111111111111=0xfff=4095)
private final long SEQUENCE_MASK = ~(-1L << SEQUENCE_BITS);

//工作節點ID(0~31)
private long workerId;

//數據中心ID(0~31)
private long datacenterId;

//毫秒內序列(0~4095)
private long sequence = 0L;

//上次生成ID的時間截
private long lastTimestamp = -1L;

/**
 * 構造函數
 * @param workerId     工作ID (0~31)
 * @param datacenterId 數據中心ID (0~31)
 */
public SnowFlakeIdGenerator(long workerId, long datacenterId) {
    if (workerId > MAX_WORKER_ID || workerId < 0) {
        throw new IllegalArgumentException(String.format("WorkerID 不能大於 %d 或小於 0", MAX_WORKER_ID));
    }
    if (datacenterId > MAX_DATACENTER_ID || datacenterId < 0) {
        throw new IllegalArgumentException(String.format("DataCenterID 不能大於 %d 或小於 0", MAX_DATACENTER_ID));
    }
    this.workerId = workerId;
    this.datacenterId = datacenterId;
}

/**
 * 獲得下一個ID (用同步鎖保證線程安全)
 * @return SnowflakeId
 */
public synchronized long nextId() {
    long timestamp =  System.currentTimeMillis();
    //如果當前時間小於上一次ID生成的時間戳,說明系統時鐘回退過這個時候應當拋出異常
    if (timestamp < lastTimestamp) {
        throw new RuntimeException("當前時間小於上一次記錄的時間戳!");
    }
    //如果是同一時間生成的,則進行毫秒內序列
    if (lastTimestamp == timestamp) {
        sequence = (sequence + 1) & SEQUENCE_MASK;
        //sequence等於0說明毫秒內序列已經增長到最大值
        if (sequence == 0) {
            //阻塞到下一個毫秒,獲得新的時間戳
            timestamp = tilNextMillis(lastTimestamp);
        }
    }
    //時間戳改變,毫秒內序列重置
    else {
        sequence = 0L;
    }

    //上次生成ID的時間截
    lastTimestamp = timestamp;
    //移位並通過或運算拼到一起組成64位的ID
    return ((timestamp - INITIAL_TIME_STAMP) << TIMESTAMP_OFFSET)
            | (datacenterId << DATACENTERID_OFFSET)
            | (workerId << WORKERID_OFFSET)
            | sequence;
}

/**
 * 阻塞到下一個毫秒,直到獲得新的時間戳
 * @param lastTimestamp 上次生成ID的時間截
 * @return 當前時間戳
 */
protected long tilNextMillis(long lastTimestamp) {
    long timestamp = System.currentTimeMillis();
    while (timestamp <= lastTimestamp) {
        timestamp = System.currentTimeMillis();
    }
    return timestamp;
}

public static void main(String[] args) {
    final SnowFlakeIdGenerator idGenerator = new SnowFlakeIdGenerator(1, 1);
    //線程池並行執行10000次ID生成
    ExecutorService executorService = Executors.newCachedThreadPool();;
    for (int i = 0; i < 10000; i++) {
        executorService.execute(new Runnable() {
            @Override
            public void run() {
                long id = idGenerator.nextId();
                System.out.println(id);
            }
        });
    }
    executorService.shutdown();

有幾點需要解釋一下:

  1. 獲得單一機器的下一序列號,Java 使用 Synchronized 控制併發,而非 CAS 的方式,是因爲 CAS 不適合併發量非常高的場景
  2. 如果當前毫秒在一臺機器的序列號已經增長到最大值 4095,則使用 while 循環等待直到下一毫秒
  3. 如果當前時間小於記錄的上一個毫秒值,則說明這臺機器的時間回撥了,拋出異常。但如果這臺機器的系統時間在啓動之前回撥過,那麼有可能出現 ID 重複的危險
  4. ((timestamp - self.initial_time_stamp) << self.timestamp_offset) | (self.datacenter_id << self.datacenterid_offset) | (self.worker_id << self.workerid_offset) | self.sequence 是什麼意思❓
((timestamp - self.initial_time_stamp) << self.timestamp_offset) | (self.datacenter_id << self.datacenterid_offset) | (self.worker_id << self.workerid_offset) | self.sequence
((1579488405321 - 1408636800000) << 22 | (1 << 17) | (2 << 12) | 0
# 時間戳:((1579488405321 - 1408636800000) << 22 
1579488405321 - 1408636800000 = 170851605321(10011111000111100011001001101101001001)
即:0 - 000 0000000 0000000000 0000010011 1110001111 0 - 00110 - 01001 - 101101001001
左移 22 位爲:0 - 0001001111 1000111100 0110010011 0110100100 1 - 00000 - 00000 - 000000000000

 # 數據中心 ID:(1 << 17)

即:0 - 0000000000 0000000000 0000000000 0000000000 0 - 00000 - 00000 - 000000000001

左移 17 位爲:0 - 0000000000 0000000000 0000000000 0000000000 0 - 00001 - 00000 - 000000000000

#工作節點 ID: (2 << 12)

即:0 - 0000000000 0000000000 0000000000 0000000000 0 - 00000 - 00000 - 000000000010

左移 12 位爲:0 - 0000000000 0000000000 0000000000 0000000000 0 - 00000 - 00010 - 000000000000

# 序列號:0

即:0 - 0000000000 0000000000 0000000000 0000000000 0 - 00000 - 00000 - 000000000000

左移 0 位爲:0 - 0000000000 0000000000 0000000000 0000000000 0 - 00000 - 00000 - 000000000000

或操作:

0 - 0001001111 1000111100 0110010011 0110100100 1 - 00000 - 00000 - 000000000000

0 - 0000000000 0000000000 0000000000 0000000000 0 - 00001 - 00000 - 000000000000

0 - 0000000000 0000000000 0000000000 0000000000 0 - 00000 - 00010 - 000000000000

0 - 0000000000 0000000000 0000000000 0000000000 0 - 00000 - 00000 - 000000000000

結果爲:

0 - 0001001111 1000111100 0110010011 0110100100 1 - 00001 - 00010 - 000000000000

即:100111110001111000110010011011010010010000100010000000000000

轉換爲 10 進製爲(ID):716603571604430848

附加說明:self.sequence = (self.sequence + 1) & self.sequence_mask

# 序列掩碼(同一時間戳校正)

# 按位與運算符(&):參與運算的兩個值,如果兩個相應位都爲 1,則該位的結果爲 1,否則爲 0

舉個例子:self.sequence = (37 + 1) & 4095

即:000000100110 & 111111111111

結果爲:000000100110(38)

3、SnowFlake 的優勢和劣勢

SnowFlake 算法的優點:

  1. 生成 ID 時不依賴於 DB,完全在內存生成,高性能高可用
  2. ID 呈趨勢遞增,後續插入索引樹的時候性能較好

SnowFlake 算法的缺點:

  1. 依賴於系統時鐘的一致性
  2. 如果某臺機器的系統時鐘回撥,有可能造成 ID 衝突,或者 ID 亂序

好了,關於 SnowFlake 算法,就介紹到這裏!

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章