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

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