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 的方式。虽然概率低,但是还是存在分享。之前考虑在单机做一个分配器,但是这实在太过于麻烦了。。