從主鍵id生成方案到雪花算法的python詳解


我們在分布式環境下為什么用雪花算法去生成主鍵id, 為什么單機情況下推薦mysql自增id而不推薦使用uuid,雪花算法的具體實現是怎么樣的?接下來詳細講述一下。

1、概述


分布式id方案那么多種,我們該以什么樣的角度去思考並選擇,下面我給出我的出發點。

  • 1.1、常用的索引方案

    • mysql自增id: 這是mysql官方推薦的方案(適合單機版)
    • uuid:數據量小的時候可以使用(不推薦)
    • redis自增id:分布式id的一種方案
    • 雪花算法:分布式id的解決方案(推薦)
  • 1.2、什么樣的方案適合做索引

    • 唯一:生成出來的序列必須是唯一的
    • 趨勢遞增:生成出來的序列可以不是連續遞增,但必須是趨勢遞增的
    • 占用字段小:索引占用的空間盡量小
    • 分布式:分布式環境下生成的id要唯一
    • 高並發:能滿足高並發環境生成id的要求
    • 高可用:要高可用,盡量不依賴外界

綜上所述,如果我們要選擇一個分布式id生成方案雪花算法是最好的選擇,其中mysql自增id在分庫分表時的id不是唯一(pass),uuid方案它生成的id不是遞增的在索引查找時效率慢,reids自增id方案,因為redis的計算層面是單線程的所以可以生成唯一且遞增的id又符合分布式要求,目前有一些公司有在使用這種方案,但是這種方案我們必須依賴redis增加我們的不可控的因素,所以不是很推薦。

2、雪花算法的實現

雪花算法是Twitter提出來的算法,看完真的很精妙,它最終會生成一個64bit的整數,最終存到數據庫就只占用8字節。

2.0 組成

  • 1bit: 一般是符號位,代表正負數的所以這一位不做處理
  • 41bit:這個部分用來記錄時間戳,如果從1970-01-01 00:00:00來計算開始時間的話,它可以記錄到2039年,足夠我們用了,並且后續我們可以設置起始時間,這樣就不用擔心不夠的問題, 這一個部分是保證我們生辰的id趨勢遞增的關鍵。
  • 10bit:這是用來記錄機器id的, 默認情況下這10bit會分成兩部分前5bit代表數據中心,后5bit代表某個數據中心的機器id,默認情況下計算大概可以支持32*32 - 1= 1023台機器。
  • 12bit:循環位,來對應1毫秒內產生的不同的id, 大概可以滿足1毫秒並發生成2^12-1=4095次id的要求。

2.1 實現代碼

import time
import logging



# 64位ID的划分
WORKER_ID_BITS = 5
DATACENTER_ID_BITS = 5
SEQUENCE_BITS = 12

# 最大取值計算
MAX_WORKER_ID = -1 ^ (-1 << WORKER_ID_BITS)  # 2**5-1 0b11111
MAX_DATACENTER_ID = -1 ^ (-1 << DATACENTER_ID_BITS)

# 移位偏移計算
WOKER_ID_SHIFT = SEQUENCE_BITS
DATACENTER_ID_SHIFT = SEQUENCE_BITS + WORKER_ID_BITS
TIMESTAMP_LEFT_SHIFT = SEQUENCE_BITS + WORKER_ID_BITS + DATACENTER_ID_BITS

# 序號循環掩碼
SEQUENCE_MASK = -1 ^ (-1 << SEQUENCE_BITS)

# Twitter元年時間戳
TWEPOCH = 1288834974657


logger = logging.getLogger('flask.app')


class IdWorker(object):
    """
    用於生成IDs
    """

    def __init__(self, datacenter_id, worker_id, sequence=0):
        """
        初始化
        :param datacenter_id: 數據中心(機器區域)ID
        :param worker_id: 機器ID
        :param sequence: 其實序號
        """
        # sanity check
        if worker_id > MAX_WORKER_ID or worker_id < 0:
            raise ValueError('worker_id值越界')

        if datacenter_id > MAX_DATACENTER_ID or datacenter_id < 0:
            raise ValueError('datacenter_id值越界')

        self.worker_id = worker_id
        self.datacenter_id = datacenter_id
        self.sequence = sequence

        self.last_timestamp = -1  # 上次計算的時間戳

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

    def get_id(self):
        """
        獲取新ID
        :return:
        """
        timestamp = self._gen_timestamp()

        # 時鍾回撥
        if timestamp < self.last_timestamp:
            logging.error('clock is moving backwards. Rejecting requests until{}'.format(self.last_timestamp))
            raise Exception

        if timestamp == self.last_timestamp:
            self.sequence = (self.sequence + 1) & SEQUENCE_MASK
            if self.sequence == 0:
                timestamp = self._til_next_millis(self.last_timestamp)
        else:
            self.sequence = 0

        self.last_timestamp = timestamp

        new_id = ((timestamp - TWEPOCH) << TIMESTAMP_LEFT_SHIFT) | (self.datacenter_id << DATACENTER_ID_SHIFT) | \
                 (self.worker_id << WOKER_ID_SHIFT) | self.sequence
        return new_id

    def _til_next_millis(self, last_timestamp):
        """
        等到下一毫秒
        """
        timestamp = self._gen_timestamp()
        while timestamp <= last_timestamp:
            timestamp = self._gen_timestamp()
        return timestamp


def test():
    for i in range(10):
        id = worker.get_id()
        print(id)


if __name__ == '__main__':
    from threading import Thread
    worker = IdWorker(1, 1, 0)
    l = list()
    for i in range(2):
        t = Thread(target=test)
        t.start()
        l.append(t)
    for t in l:
        t.join()

2.2 位運算簡示

# 經過計算各個部分可以得出(以下全部為二進制)

# 1、時間戳部分,

time = b'10101010101010101010101010101010101010101000000000000000000000'

# 2、機器id部分,假設是1機器中心的1機器生成id

dev = b'000000000000000000000000000000000000000000000100001000000000000'

# 3、在本毫秒生成的第一個id為

seq = b'000000000000000000000000000000000000000000000000000000000000001'

# 從上我們得出每個部分二進制的值,那我們進行或位運算,兩個位同為0時才為0,只要有1則為1

id = time | dev | seq

# 經過上面計算可以得出
id = b'10101010101010101010101010101010101010101000010000100000000001'

# 然后二進制id轉成整形以后就變成一下

id = 1368043503778664451

# 這樣就得到了我們的id

3、缺點及方案

看完上述方案,可能察覺到雪花算法方案非常依賴機器上的時間戳,你想如果機器上的時鍾發生回撥的話,極有可能生成重復的id。

為解決以上問題各大廠都有對應的開源方案,可以自己進行查找。


免責聲明!

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



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