snow flake

snow flake

分佈式系統需要有一個方法去分配一個唯一的ID。如mysql 分表之後,如果各個表使用的都是自增ID 那麼不同表之間的ID 就會重複,對於其他業務可能會認爲是同一條數據,或者造成別的問題。所以需要一個分配ID的方法。

通常有兩種方式

mysql

使用mysql 最爲關節節點,每次分配數百個ID到內存,然後應用再在內存中取值。
這種方式較爲複雜,並且依賴於分配器所在的DB,如果出了問題,所有的依賴的表都會掛掉。

使用 snowflake 算法分配

snowflake是Twitter開源的分佈式ID生成算法,結果是一個long型的ID。其核心思想是:使用41bit作爲毫秒數,10bit作爲機器的ID(5個bit是數據中心,5個bit的機器ID),12bit作爲毫秒內的流水號(意味着每個節點在每毫秒可以產生 4096 個 ID),最後還有一個符號位,永遠是0。

定義是如上定義。但是可以學習他的原理以及是如何做到唯一性。並且在分佈式DB。如TIDB中,ID 是不允許設置自增的ID的。因爲連續的ID會被插入到同一個TIKV 內,會造成熱數據,需要數據的分散,而snowflake就很好的滿足了這一點。

  • mysql 分配ID 的方式還是比較集中的,任然會被分配到同一臺TIKV 節點上面,不適合作爲TIDB的ID。
  • snowflake 的時間在中間41bit 上,能夠很好的分散數據。所以作爲TIDB的ID 非常合適。

設計

關鍵在如何標誌唯一性的幾個bit。

在沒有別的方式獲取機器ID,數據ID 的情況下。決定使用 ip + pid 的方式來標誌。

原有的10bit分配其實還是有隱藏條件的。如果在單機中多個進程同時產生ID 還是會重複。多進程就不合適了

IP 使用數字表示需要 4Byte -> 32 bit
PID 到幾k 上萬都有可能。所以還是要做處理。
這裏取IP 後16bit.(重複可能非常小)
PID mod 128 7bit

最後代碼如下

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

import time
import os
from time_utils import get_timestamp
import socket
import threading

start_time = int(time.mktime(time.strptime('2019-01-01 00:00:00', "%Y-%m-%d %H:%M:%S")))


"""
ip -> 16
pid -> 7


--------------------------------------------————
|sign | timestamp left| work node id| sequence |
---------------------------------------------
|1 bit|     30bit     |     23bit   |   10bit  |
---------------------------------------------——
 snowFlake 三部分組成
  sign (1 bit) 爲正數
  timestamp left (41 bit) s 相對於基礎時間 的剩餘時間
  :work node 16 bit ip 後16bit + pid mod % 128 7bit -> 23bit
  :sequence 一秒生成的ID 數量

使用:
爲每一個表實例化一個全局對象
snow = SnowFlake()

"""


def get_host_ip():
    """
    查詢本機ip地址
    :return: ip
    """
    try:
        s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
        s.connect(('8.8.8.8', 80))
        ip = s.getsockname()[0]
    finally:
        s.close()

    return ip


def get_work_id_ip_part():
    ip = get_host_ip()
    ip_list = ip.split('.')
    return (int(ip_list[2]) << 8) + int(ip[3])


def get_work_id_pid_part():
    return os.getpid() % 128


work_ip_id = get_work_id_ip_part()
work_pid_id = get_work_id_pid_part()


class SnowFlake(object):
    # 各個部分所佔位數 類變量
    sequence_bit = 10
    work_node_id_bit = 23
    time_stamp_left_bit = 30
    sign_bit = 1

    sequence_max_value = 1 << sequence_bit
    work_node_id_max_value = 1 << work_node_id_bit
    time_stamp_left_max_value = 1 << time_stamp_left_bit

    work_node_id_shift = sequence_bit
    time_stamp_left_shift = sequence_bit + work_node_id_bit

    def __init__(self):
        self.start = start_time
        self.last_timestamp = int(time.time())
        self.sequence = 0
        # self.sequence = self.sequence_max_value - 1
        self.work_id = (work_ip_id << 7) + work_pid_id
        # print 'work_id:%s' % self.work_id
        self.lock = threading.RLock()

    def get_id(self):
        with self.lock:
            now = get_timestamp()
            # 時間回退 異常
            if now < self.last_timestamp:
                raise Exception('SnowFlake now:%s small than last_timestamp:%s', now, self.last_timestamp)

            time_stamp_count = now - self.start

            # 同一秒內 sequence 累加, 否則清0
            if now == self.last_timestamp:
                self.sequence += 1
            else:
                self.sequence = 0
                self.last_timestamp = now
            # 已經累計到最大值 需要休眠1s 再次調用
            if self.sequence == self.sequence_max_value:
                time.sleep(1)
                return self.get_id()

            current_id = time_stamp_count << self.time_stamp_left_shift | \
                         self.work_id << self.work_node_id_shift | self.sequence
            return current_id


snow = SnowFlake()

改進

後續發現使用jenkins 部署的時候可以獲取項目名稱以及實例的index。我們內部一個項目應該不超過8所以給3bit APP_NAME.

work_node_id app 3 bit worker id 7 bit 13 pid 8102 -> 23 bit

優化了中間的work_id。但是對於實例內多進程的話似乎沒有找到特別好的辦法,現在採用mod 8192 的方式。雖然概率低,但是還是存在分享。之前考慮在單機做一個分配器,但是這實在太過於麻煩了。。

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