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 的方式。雖然概率低,但是還是存在分享。之前考慮在單機做一個分配器,但是這實在太過於麻煩了。。