场景:
在n个分布式数据库中,用数据库自带的自增ID作为主键是不合理的,过滤出用户是很有重复
需求:在分布式系统中生成一个全局唯一的ID,满足唯一性、有序性、可扩展性等要求。
- 唯一性
- 有序性
- 可扩展性
UUID算法
UUID(Universally Unique Identifier)是一个128位的全局唯一标识符,标准形式包含32个十六进制数字,以连字号分为五段(8-4-4-4-12)
UUID 是一个 128 位的数字,通常以 36 个字符的字符串形式表示,如:
bb0fc150-5d3a-11f0-84ac-ec2e9871c874
不同版本的UUID实现原理不大相同
版本 | 名称 | 特点 |
---|---|---|
v1 | 基于时间戳和 MAC 地址 | 可追溯生成时间和设备 |
v3 | 基于命名空间的 MD5 哈希 | 输入相同则输出相同 |
v4 | 基于随机数 | 最常用,完全随机 |
v5 | 基于命名空间的 SHA-1 哈希 | 类似 v3,但更安全 |
v6+ | 新提案,优化排序性能 | 尚未广泛支持 |
案例:
时间戳 + MAC地址
简单的模拟:
import time
import random
import uuid as py_uuid
import struct
def get_mac_address():
"""获取本机 MAC 地址(48 位)"""
mac = py_uuid.getnode()
return mac & 0xFFFFFFFFFFFF
def get_timestamp():
"""获取 UUID v1 所需的时间戳(100ns 单位,自 1582-10-15 起)"""
uuid_epoch = 12219292800 # 与 Unix 时间戳的差值(秒)
timestamp = int((time.time() + uuid_epoch) * 1e7)
return timestamp
def generate_uuid_v1():
timestamp = get_timestamp()
clock_seq = random.getrandbits(14)
node = get_mac_address()
time_low = timestamp & 0xFFFFFFFF
time_mid = (timestamp >> 32) & 0xFFFF
time_hi_and_version = ((timestamp >> 48) & 0x0FFF) | (1 << 12)
clock_seq_low = clock_seq & 0xFF
clock_seq_hi_and_reserved = ((clock_seq >> 8) & 0x3F) | 0x80
fields = struct.pack(">IHHBB6s",
time_low,
time_mid,
time_hi_and_version,
clock_seq_hi_and_reserved,
clock_seq_low,
node.to_bytes(6, 'big'))
return py_uuid.UUID(bytes=fields)
if __name__ == "__main__":
print("手动生成的 UUID v1:", generate_uuid_v1())
优势:
实现简单,所有主流语言都有原生支持 , 完全分布式生成,无需中心节点协调 - 碰撞概率极低(v4版本理论碰撞概率为10^-37) - 跨系统、跨组织保证唯一性.
局限性:
最大的问题就是无序性,在分布式数据库中,最理想的情况下 新ID > 老ID
早期版本安全性有些问题,无内置时间戳
Snowflake算法
雪花算法是Twitter开源的一种分布式ID生成算法,生成的ID是64位的长整型,其精妙的位分配结构如下:
+------+----------------------+----------------+------------+
| 符号位 | 时间戳(41位) | 工作节点(10位) | 序列号(12位) |
+------+----------------------+----------------+------------+
1bit 41bits 10bits 12bits
- 时间戳部分:毫秒级精度,从自定义纪元开始(通常为算法实现时间),可用约69年
- 工作节点ID:通常分为5位数据中心ID和5位机器ID,支持32个数据中心×32台机器
- 序列号:每毫秒重新计数,支持单机每毫秒4096个ID生成
案例:
class SimpleSnowflake:
"""简单版 Snowflake 算法实现(64位整数)"""
def __init__(self, datacenter_id=0, worker_id=0):
self.datacenter_id = datacenter_id & 0x1F # 5位
self.worker_id = worker_id & 0x1F # 5位
self.sequence = 0
self.last_timestamp = -1
self.epoch = 1609459200000 # 2021-01-01 00:00:00 UTC (自定义起始时间)
def _timestamp(self):
return int(time.time() * 1000)
def next_id(self):
timestamp = self._timestamp()
if timestamp == self.last_timestamp:
self.sequence = (self.sequence + 1) & 0xFFF # 12位
if self.sequence == 0:
while self._timestamp() <= self.last_timestamp:
pass
timestamp = self._timestamp()
else:
self.sequence = 0
self.last_timestamp = timestamp
id = ((timestamp - self.epoch) << 22) | (self.datacenter_id << 17) | (self.worker_id << 12) | self.sequence
return id
if __name__ == "__main__":
snowflake = SimpleSnowflake(datacenter_id=1, worker_id=1)
print("简单 Snowflake ID:", snowflake.next_id())
优势:
- 性能优异:本地生成无网络IO,单机QPS可达400万+
- 存储高效:仅需8字节长整型存储
- 索引友好:趋势递增特性大幅提升B+树索引效率
- 时间可溯:可通过ID反解析出生成时间戳
- 业务集成:可将业务信息编码到工作节点ID中
局限性:
- 时钟回拨问题:需要设计时钟同步机制或异常处理策略
- 节点配置:需要维护机器ID的分配
- 扩展限制:单数据中心最多32台机器,每毫秒4096ID的限制
- 寿命限制:41位时间戳约69年后会溢出
使用场景
优先UUID的场景:
- 快速开发,无其他依赖
- 多个系统需要唯一标识符时
- noSQL情况: 如MongoDB
- 临时存储:如会话ID、一次性令牌等
优先雪花的场景:
- 关系型数据库:MySQL InnoDB等聚簇索引存储
- 与时间相关的场景
- 需要携带业务信息
总结:
这两个传统的算法都很原始,但是各种优化版本都没问题,认清楚场景再选择,才能发挥出其最大的作用。