目录
问在最前:
如何在分布式集群中,生成全局唯一的 ID❓
方法一:UUID
UUID 是通用唯一识别码 (Universally Unique Identifier),在其他语言中也叫 GUID,可以生成一个长度32位的全局唯一识别码。
Python3 代码: |
import uuid # uuid1() 基于时间戳,Python 中 UUID 主要有五个算法,当前我们仅用第一种 #由 MAC 地址,当前时间戳,随机数字生成。可以保证全球范围内的唯一性 # 但是由于 MAC 地址的使用同时带来了安全问题,局域网中可以使用 IP 来代替 MAC uuid1 = uuid.uuid1() print(uuid1) |
结果展示: |
1698126c-3a61-11ea-9b12-b083fec202a4 |
缺点也很明显:UUID 虽然可以保证全局唯一,但是占用 32 位有些太长,而且是无序的,入库时性能比较差。
疑问:为什么无序的 UUID 会导致入库性能变差呢?
这就涉及到 B+树(关于什么是 B+树可查看:什么是 B+树❓) 索引的分裂:
众所周知,关系型数据库的索引大都是 B+树结构,像 MySQL,拿 ID 字段来举例,索引树的每一个节点都存储着若干个 ID。
如果我们的 ID 按递增的顺序来插入,比如陆续插入 8、9、10,新的 ID 都只会插入到最后一个节点当中。当最后一个节点满了,会裂变出新的节点。这样的插入是性能比较高的插入,因为这样节点的分裂次数最少,而且充分利用了每一个节点的空间。
但是,如果我们的插入玩去无序,不但会导致一些中间节点产生分裂,也会白白创造出很多不饱和的节点,这样大大降低了数据库插入的性能。
方法二:数据库自增主键
假设名为 table 的表有如下结构:
id | name |
---|---|
37 | wufei |
每一次生成 ID 的时候,访问数据库,执行下面的语句:
|
REPLACE INTO 的含义是插入一条记录,如果表中唯一索引的值遇到冲突,则替换老数据。
这样一来,每次都可以得到一个递增的 ID。
为了提高性能,在分布式系统中可以用 DB proxy(如 mycat)请求不同的分库,每个分库设置不同的初始值,步长和分库数量相等:
在 MySQL 中配置步长:auto_increment_increment =2
在 MySQL 中配置起始:auto_increment_offset =1(如 DB01)
这样一来,DB1生成的ID是1,4,7,10,13....,DB2生成的ID是2,5,8,11,14.....
起始确定也很明显:ID 的生成对数据库严重依赖,不但影响性能,而且一旦数据库挂掉,服务将变得不可用。
抛砖引玉:你听说过 SnowFlake 算法吗?
SnowFlake 是 Twitter 公司所采用的一种算法,目的是在分布式系统中生成全局唯一且趋势递增的 ID。
1、初始 SnowFlake
SnowFlake 算法所生成的 ID 结构是什么样子呢?我们来看看下图:
SnowFlake 所生成的 ID 一共分成四部分:
1> 第一位
占用 1bit,其值始终是 0,没有实际作用。
2> 时间戳
占用 41bit,精确到毫秒,总共可以容纳 69 年的时间。
3> 工作机器 ID
占用 10bit,其中高位 5bit 是数据中心 ID(datacenterID),地位 5bit 是工作节点 ID(workerID),最多可以容纳 1024 个节点。
4> 序列号
占用 12bit,这个值在同一毫秒内最多可以生成多少个全局唯一 ID 呢?只需要做一个简单的乘法:
同一毫秒的 ID 数量 = 1024 X 4096 = 4194304(即 3> 和 4> 相乘)
这个数字绝大多数并发场景下都是够用的。
2、SnowFlake 的代码实现
是不是觉得 SnowFlake 算法还真是强大,可是如何用代码来实现它呢?
其实并不难实现,来看一看 Python3 实现代码(至于代码中初始时间戳为什么为 2014-08-22 00:00:00,不多说😀)。
有人可能会问:为什么要减去一个初始时间?
那是因为 41 位字节作为时间戳数值的话,大约 68 年就会用完,如果不减去 2014-08-22,那么白白浪费 40 多年的时间戳!一般初始时间为该项目开始成立的时间。
话不多说,快快上码:
#!/usr/bin/env python3
# -*- coding: UTF-8 -*-
'''=================================================================================
@Project -> File :python_imggen -> snowflake_id.py
@IDE :PyCharm
@Author :Mr. Wufei
@Date :2020/1/19 11:23
@Desc :通过雪花算法(SnowFlake)用 Python3 实现一个简单的发号器
@CSDN :什么是 SnowFlake(https://showufei.blog.csdn.net/article/details/104041576)
=================================================================================='''
import sys
import time
import logging
class MySnow(object):
def __init__(self, datacenter_id, worker_id):
# 初始毫秒级时间戳(2014-08-22)
self.initial_time_stamp = int(time.mktime(time.strptime('2014-08-22 00:00:00', "%Y-%m-%d %H:%M:%S")) * 1000)
# 机器 ID 所占的位数
self.worker_id_bits = 5
# 数据表示 ID 所占的位数
self.datacenter_id_bits = 5
# 支持的最大机器 ID,结果是 31(这个位移算法可以很快的计算出几位二进制数所能表示的最大十进制数)
# 2**5-1 0b11111
self.max_worker_id = -1 ^ (-1 << self.worker_id_bits)
# 支持最大标识 ID,结果是 31
self.max_datacenter_id = -1 ^ (-1 << self.datacenter_id_bits)
# 序列号 ID所占的位数
self.sequence_bits = 12
# 机器 ID 偏移量(12)
self.workerid_offset = self.sequence_bits
# 数据中心 ID 偏移量(12 + 5)
self.datacenterid_offset = self.sequence_bits + self.datacenter_id_bits
# 时间戳偏移量(12 + 5 + 5)
self.timestamp_offset = self.sequence_bits + self.datacenter_id_bits + self.worker_id_bits
# 生成序列的掩码,这里为 4095(0b111111111111 = 0xfff = 4095)
self.sequence_mask = -1 ^ (-1 << self.sequence_bits)
# 初始化日志
self.logger = logging.getLogger('snowflake')
# 数据中心 ID(0 ~ 31)
if datacenter_id > self.max_datacenter_id or datacenter_id < 0:
err_msg = 'datacenter_id 不能大于 %d 或小于 0' % self.max_worker_id
self.logger.error(err_msg)
sys.exit()
self.datacenter_id = datacenter_id
# 工作节点 ID(0 ~ 31)
if worker_id > self.max_worker_id or worker_id < 0:
err_msg = 'worker_id 不能大于 %d 或小于 0' % self.max_worker_id
self.logger.error(err_msg)
sys.exit()
self.worker_id = worker_id
# 毫秒内序列(0 ~ 4095)
self.sequence = 0
# 上次生成 ID 的时间戳
self.last_timestamp = -1
def _gen_timestamp(self):
"""
生成整数毫秒级时间戳
:return: 整数毫秒级时间戳
"""
return int(time.time() * 1000)
def next_id(self):
"""
获得下一个ID (用同步锁保证线程安全)
:return: snowflake_id
"""
timestamp = self._gen_timestamp()
# 如果当前时间小于上一次 ID 生成的时间戳,说明系统时钟回退过这个时候应当抛出异常
if timestamp < self.last_timestamp:
self.logger.error('clock is moving backwards. Rejecting requests until {}'.format(self.last_timestamp))
# 如果是同一时间生成的,则进行毫秒内序列
if timestamp == self.last_timestamp:
self.sequence = (self.sequence + 1) & self.sequence_mask
# sequence 等于 0 说明毫秒内序列已经增长到最大值
if self.sequence == 0:
# 阻塞到下一个毫秒,获得新的时间戳
timestamp = self._til_next_millis(self.last_timestamp)
else:
# 时间戳改变,毫秒内序列重置
self.sequence = 0
# 上次生成 ID 的时间戳
self.last_timestamp = timestamp
# 移位并通过或运算拼到一起组成 64 位的 ID
new_id = ((timestamp - self.initial_time_stamp) << self.timestamp_offset) | \
(self.datacenter_id << self.datacenterid_offset) | \
(self.worker_id << self.workerid_offset) | \
self.sequence
return new_id
def _til_next_millis(self, last_timestamp):
"""
阻塞到下一个毫秒,直到获得新的时间戳
:param last_timestamp: 上次生成 ID 的毫秒级时间戳
:return: 当前毫秒级时间戳
"""
timestamp = self._gen_timestamp()
while timestamp <= last_timestamp:
timestamp = self._gen_timestamp()
return timestamp
"""
if __name__ == '__main__':
mysnow = MySnow(1, 2)
id = mysnow.next_id()
print(id)
"""
这段代码改写自网上 Java 实现的 SnowFlake 算法,Java 代码如下:
//始时间截 (2017-01-01)
private static final long INITIAL_TIME_STAMP = 1483200000000L;
//机器id所占的位数
private static final long WORKER_ID_BITS = 5L;
//数据标识id所占的位数
private static final long DATACENTER_ID_BITS = 5L;
//支持的最大机器id,结果是31 (这个移位算法可以很快的计算出几位二进制数所能表示的最大十进制数)
private static final long MAX_WORKER_ID = ~(-1L << WORKER_ID_BITS);
//支持的最大数据标识id,结果是31
private static final long MAX_DATACENTER_ID = ~(-1L << DATACENTER_ID_BITS);
//序列在id中占的位数
private final long SEQUENCE_BITS = 12L;
//机器ID的偏移量(12)
private final long WORKERID_OFFSET = SEQUENCE_BITS;
//数据中心ID的偏移量(12+5)
private final long DATACENTERID_OFFSET = SEQUENCE_BITS + SEQUENCE_BITS;
//时间截的偏移量(5+5+12)
private final long TIMESTAMP_OFFSET = SEQUENCE_BITS + WORKER_ID_BITS + DATACENTER_ID_BITS;
//生成序列的掩码,这里为4095 (0b111111111111=0xfff=4095)
private final long SEQUENCE_MASK = ~(-1L << SEQUENCE_BITS);
//工作节点ID(0~31)
private long workerId;
//数据中心ID(0~31)
private long datacenterId;
//毫秒内序列(0~4095)
private long sequence = 0L;
//上次生成ID的时间截
private long lastTimestamp = -1L;
/**
* 构造函数
* @param workerId 工作ID (0~31)
* @param datacenterId 数据中心ID (0~31)
*/
public SnowFlakeIdGenerator(long workerId, long datacenterId) {
if (workerId > MAX_WORKER_ID || workerId < 0) {
throw new IllegalArgumentException(String.format("WorkerID 不能大于 %d 或小于 0", MAX_WORKER_ID));
}
if (datacenterId > MAX_DATACENTER_ID || datacenterId < 0) {
throw new IllegalArgumentException(String.format("DataCenterID 不能大于 %d 或小于 0", MAX_DATACENTER_ID));
}
this.workerId = workerId;
this.datacenterId = datacenterId;
}
/**
* 获得下一个ID (用同步锁保证线程安全)
* @return SnowflakeId
*/
public synchronized long nextId() {
long timestamp = System.currentTimeMillis();
//如果当前时间小于上一次ID生成的时间戳,说明系统时钟回退过这个时候应当抛出异常
if (timestamp < lastTimestamp) {
throw new RuntimeException("当前时间小于上一次记录的时间戳!");
}
//如果是同一时间生成的,则进行毫秒内序列
if (lastTimestamp == timestamp) {
sequence = (sequence + 1) & SEQUENCE_MASK;
//sequence等于0说明毫秒内序列已经增长到最大值
if (sequence == 0) {
//阻塞到下一个毫秒,获得新的时间戳
timestamp = tilNextMillis(lastTimestamp);
}
}
//时间戳改变,毫秒内序列重置
else {
sequence = 0L;
}
//上次生成ID的时间截
lastTimestamp = timestamp;
//移位并通过或运算拼到一起组成64位的ID
return ((timestamp - INITIAL_TIME_STAMP) << TIMESTAMP_OFFSET)
| (datacenterId << DATACENTERID_OFFSET)
| (workerId << WORKERID_OFFSET)
| sequence;
}
/**
* 阻塞到下一个毫秒,直到获得新的时间戳
* @param lastTimestamp 上次生成ID的时间截
* @return 当前时间戳
*/
protected long tilNextMillis(long lastTimestamp) {
long timestamp = System.currentTimeMillis();
while (timestamp <= lastTimestamp) {
timestamp = System.currentTimeMillis();
}
return timestamp;
}
public static void main(String[] args) {
final SnowFlakeIdGenerator idGenerator = new SnowFlakeIdGenerator(1, 1);
//线程池并行执行10000次ID生成
ExecutorService executorService = Executors.newCachedThreadPool();;
for (int i = 0; i < 10000; i++) {
executorService.execute(new Runnable() {
@Override
public void run() {
long id = idGenerator.nextId();
System.out.println(id);
}
});
}
executorService.shutdown();
有几点需要解释一下:
- 获得单一机器的下一序列号,Java 使用 Synchronized 控制并发,而非 CAS 的方式,是因为 CAS 不适合并发量非常高的场景
- 如果当前毫秒在一台机器的序列号已经增长到最大值 4095,则使用 while 循环等待直到下一毫秒
- 如果当前时间小于记录的上一个毫秒值,则说明这台机器的时间回拨了,抛出异常。但如果这台机器的系统时间在启动之前回拨过,那么有可能出现 ID 重复的危险
- ((timestamp - self.initial_time_stamp) << self.timestamp_offset) | (self.datacenter_id << self.datacenterid_offset) | (self.worker_id << self.workerid_offset) | self.sequence 是什么意思❓
((timestamp - self.initial_time_stamp) << self.timestamp_offset) | (self.datacenter_id << self.datacenterid_offset) | (self.worker_id << self.workerid_offset) | self.sequence |
---|
((1579488405321 - 1408636800000) << 22 | (1 << 17) | (2 << 12) | 0 |
# 时间戳:((1579488405321 - 1408636800000) << 22 1579488405321 - 1408636800000 = 170851605321(10011111000111100011001001101101001001) 即:0 - 000 0000000 0000000000 0000010011 1110001111 0 - 00110 - 01001 - 101101001001 左移 22 位为:0 - 0001001111 1000111100 0110010011 0110100100 1 - 00000 - 00000 - 000000000000 |
# 数据中心 ID:(1 << 17) 即:0 - 0000000000 0000000000 0000000000 0000000000 0 - 00000 - 00000 - 000000000001 左移 17 位为:0 - 0000000000 0000000000 0000000000 0000000000 0 - 00001 - 00000 - 000000000000 |
#工作节点 ID: (2 << 12) 即:0 - 0000000000 0000000000 0000000000 0000000000 0 - 00000 - 00000 - 000000000010 左移 12 位为:0 - 0000000000 0000000000 0000000000 0000000000 0 - 00000 - 00010 - 000000000000 |
# 序列号:0 即:0 - 0000000000 0000000000 0000000000 0000000000 0 - 00000 - 00000 - 000000000000 左移 0 位为:0 - 0000000000 0000000000 0000000000 0000000000 0 - 00000 - 00000 - 000000000000 |
或操作: 0 - 0001001111 1000111100 0110010011 0110100100 1 - 00000 - 00000 - 000000000000 0 - 0000000000 0000000000 0000000000 0000000000 0 - 00001 - 00000 - 000000000000 0 - 0000000000 0000000000 0000000000 0000000000 0 - 00000 - 00010 - 000000000000 0 - 0000000000 0000000000 0000000000 0000000000 0 - 00000 - 00000 - 000000000000 结果为: 0 - 0001001111 1000111100 0110010011 0110100100 1 - 00001 - 00010 - 000000000000 即:100111110001111000110010011011010010010000100010000000000000 转换为 10 进制为(ID):716603571604430848 |
附加说明:self.sequence = (self.sequence + 1) & self.sequence_mask # 序列掩码(同一时间戳校正) # 按位与运算符(&):参与运算的两个值,如果两个相应位都为 1,则该位的结果为 1,否则为 0 举个例子:self.sequence = (37 + 1) & 4095 即:000000100110 & 111111111111 结果为:000000100110(38) |
3、SnowFlake 的优势和劣势
SnowFlake 算法的优点:
- 生成 ID 时不依赖于 DB,完全在内存生成,高性能高可用
- ID 呈趋势递增,后续插入索引树的时候性能较好
SnowFlake 算法的缺点:
- 依赖于系统时钟的一致性
- 如果某台机器的系统时钟回拨,有可能造成 ID 冲突,或者 ID 乱序
好了,关于 SnowFlake 算法,就介绍到这里!