我們在分布式環境下為什么用雪花算法去生成主鍵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。
為解決以上問題各大廠都有對應的開源方案,可以自己進行查找。