百度 65岁的侯二河说着一口武安话,可官兵们却听得津津有味。大家好,我是IT孟德,You can call me Aman(阿瞒,阿弥陀佛的ē,Not阿门的ā),一个喜欢所有对象(热爱技术)的男人。我正在创作架构专栏,秉承ITer开源精神分享给志同道合(爱江山爱技术更爱美人)的朋友。专栏更新不求速度但求质量(曹大诗人传世作品必属精品,请脑补一下《短歌行》:对酒当歌,红颜几何?譬如媳妇,吾不嫌多...青青罗裙,一见动心,但为佳人,挂念至今...),通过朴实无华、通俗易懂的图文将十六载开发和架构实战经验娓娓道来,让读者茅塞顿开、相见恨晚...如有吹牛,不吝赐教。关注wx公众号:IT孟德,一起修炼吧!?
专栏文章推荐:
1、什么是分布式ID?
????????分布式ID是指在分布式系统或集群环境中,用于全局唯一地标识一个对象(如用户、订单、设备等)的标识符。其主要特征如下:
-
全局唯一性: 在整个系统(跨节点、跨服务、跨时间)中,ID必须唯一,避免冲突。这是数据正确性和系统一致性的基础,绝对不能出现重复。
-
可扩展性: 支持水平扩展,即使新增节点,也能保证ID生成的唯一性和效率。
-
高性能: ID的生成速度快,低延迟,不影响系统整体性能。
-
有序性(可选):某些场景要求ID趋势递增(不一定连续),有利于数据库索引优化或按时间排序。
-
安全性(可选): ID本身避免包含业务敏感信息或可能导致敏感信息泄露的可预测模式,如用户ID连续可能导致数据泄露风险。
-
去中心化(可选):生成算法减少依赖外部服务(如数据库或协调服务),提升可用性。
-
空间占用合理(可选): ID避免过长(如使用128位UUID虽然唯一但空间占用大,可能对存储和索引效率产生影响),应尽可能短小精悍。
2、为什么要用分布式ID?
????????在单体应用或单数据库业务系统中,通常使用数据库的自增字段(如MySQL的AUTO_INCREMENT或PostgreSQL的SERIAL)就能很好地满足唯一ID的需求。但分布式系统中数据被拆到多个节点/数据库,依赖数据库自增存在很多局限性:
-
ID冲突:分布式系统中,数据通常会分散存储(比如分库分表:订单业务拆分为32个库、每个库32张表),或由多个服务独立生成数据(比如微服务架构中多个订单服务同时创建订单),如果用传统数据库自增ID,每个数据库节点都有自己的自增序列,生成的 ID只在本地唯一,合并后全局范围很可能会出现大量重复;不同服务生成的ID也可能重复。而分布式ID的核心作用就是在整个分布式系统中,为任意数据生成全局唯一的标识,避免重复。
-
性能瓶颈: 分布式系统通常有高并发场景(如秒杀、大促)。如果依赖单一的主数据库或某个中心化服务来生成所有ID(使用其自增功能或序列),这个中心节点就会面临巨大的并发压力,极易成为系统的性能瓶颈,且一旦这个中心节点宕机,整个系统的所有业务都受影响。分布式D方案(如雪花算法、Redis 自增、分布式发号器等)会通过 “无锁设计”“集群部署” 等方式保证高性能和高可用。
-
安全性低:传统自增ID有一个隐患,容易泄露业务数据量。如看到订单ID=100000,能猜到平台总共才100000个订单,暴露业务规模;攻击者可能通过自增ID遍历数据(比如按 ID=1,2,3... 批量爬取用户信息)。而分布式 ID 可以通过 “无规则结构”(如混合时间戳、机器标识、随机数)避免这个问题。
3、分布式ID方案有哪些?
3.1、UUID/GUID
原理:GUID(Globally Unique Identifier) 和 UUID(Universally Unique Identifier)在技术上是完全相同的概念,都是指基于时间戳、机器信息、随机数等生成一个128位(16字节)的全局唯一标识符。其标准格式为:xxxxxxxx-xxxx-Mxxx-Nxxx-xxxxxxxxxxxx(M为版本号,表示算法、N为变体标识,表示编码),常见版本如下:
版本 | 算法 | 示例 |
v1 | 时间戳 + MAC地址 | 550e8400-e29b-11d4-a716-446655440000 |
v4 | 随机数 | 550e8400-e29b-41d4-a716-446655440000 |
v3/v5 | 命名空间 + 名称 + 哈希(MD5/SHA1) | 550e8400-e29b-31d4-a716-446655440000 |
v7 | 时间戳+随机数 | 017f5e9a-3b8e-7c30-b73c-9d46450a28d3 |
优点:
-
去中心化: 本地生成,无任何网络开销和外部依赖,高可用。
-
简单易用:大多数编程语言和数据库(如PostgreSQL、MySQL)内置 UUID 支持。
-
性能好:本地CPU计算,生成速度非常快。
-
隐私保护:V4版本完全随机,不暴露时间或设备信息。
缺点:
-
存储开销大:128位16字节),比Int或BigInt类型自增ID(4~8字节)占用更多空间,存储和传输成本高(如数据库主键索引占用空间大)。
-
无序性:ID无递增趋势(尤其是随机数实现的UUID v4),无法作为数据库主键(B+树索引插入无序会导致大量页分裂,性能下降)。
-
可读性差:16进制字符串对人类不友好。
-
版本差异:不同版本的UUID适用场景差异大,完全随机的v4版本在极端高并发下存在极小重复概率。
适用场景:适合对ID有序性、长度无要求,且无需依赖外部服务的场景(如临时凭证、一次性令牌、文件命名),不适合需要数据库索引优化或对ID可读性有要求的场景(如订单ID)。
3.2、数据库自增ID
基础方案(单库自增):直接使用数据库的AUTO_INCREMENT(MySQL)或SERIAL(PostgreSQL)生成自增ID,通过单库单表保证唯一性。
改进方案(分库分表场景):若分库分表(多库多表),直接用单库自增会因多库重复导致ID冲突,需通设置自增步长+初始偏移量改进:
-
为每个分库/分表分配独立的“自增区间”:例如分2个库,库1自增ID从1开始、步长为2(生成1,3,5...),库2从2开始、步长为2(生成2,4,6...)。
-
按分表编号设置偏移:分表1偏移0,分表2偏移1000万,避免ID重叠。
号段方案:仍然利用数据库的自增字段能力,但不是每次取一个ID,而是批量获取一个号段(ID区间),本地缓存使用,用完后再从数据库申请新号段;美团Leaf Segment模式采用双Buffer(双号段)预加载下一个号段,减少分配延迟和请求阻塞。
优点:
-
实现简单:依赖数据库原生功能,无需额外开发。
-
趋势递增:天然符合数据库索引优化需求。
-
可读性好:ID为数字且连续,便于人工识别和排查问题。
缺点:
-
性能瓶颈:单库自增依赖数据库单点,高并发下写入压力集中(即使分库分表,ID生成仍依赖数据库,无法脱离数据库瓶颈)。
-
扩展性差:分库分表时步长/偏移量固定,新增分库分表需重新调整规则,易导致ID冲突。
-
可用性风险:依赖数据库可用性,数据库宕机则无法生成ID。
-
号段浪费: 应用进程意外重启,号段模式可能导致尚未使用的号段被浪费。
适用场景:适合低并发、小规模、对ID连续性、可读性要求高,且分库分表规模固定的业务,如内部管理等稳定优先、简单优先的系统。
3.3、基于Redis的自增ID
原理:利用Redis的INCR(自增)或INCRBY(指定步长自增)命令的原子性,生成全局唯一ID。
-
通过Redis维护一个ID计数器,每次生成ID时调用INCR key,返回的结果即为新ID。
-
为避免Redis压力,可批量生成(如一次获取1000个ID,本地缓存使用,用完再从Redis取)。
优点:
-
高性能:Redis单节点QPS可达10万+,支持批量获取,性能远超数据库。
-
趋势递增:ID严格递增,适合数据库索引。
-
实现简单:依赖Redis原生命令,无需复杂逻辑。
缺点:
-
强依赖Redis:强依赖Redis的高可用和数据持久化。Redis节点故障或主从切换时,服务不可用;且需平衡性能与数据可靠性(同步持久化严重影响性能,异步有丢数据风险)。
-
内存成本: 存储序列键本身需要占用内存,尤其当需要为大量不同用途生成独立序列时。
-
ID连续性问题:若Redis重启且未持久化(或持久化数据丢失),可能导致ID跳变(如从1000跳到1,重复风险);即使持久化(RDB/AOF),也可能因最后一次自增未写入磁盘导致小范围跳变。
适用场景: 对性能要求极高且可容忍少量潜在ID丢失或不连续风险的场景;已有成熟Redis高可用集群(Sentinel/Cluster)架构;并发量很大的系统。需谨慎评估其对Redis的强依赖和数据可靠性要求。
3.4、雪花算法(Snowflake)
原理:由Twitter开源的分布式ID生成算法,基于“时间戳+机器ID+序列号”生成64位长整型ID。
优点:
-
高性能:去中心化本地生成,无网络依赖,QPS可达百万级。
-
趋势递增:时间戳在高位,ID按时间递增,适合数据库索引;ID本身可反解析出大致创建时间(可节省额外存储时间戳字段)
-
可控性强:可自定义各段位数(如调整机器ID位数支持更多节点)。
-
高可用:不依赖数据库、Redis等服务,可用性极高;各节点独立工作,系统整体扩展性好。
-
空间占用小: 64位整数,存储传输效率较高。
缺点:
-
时钟回拨问题:若机器时钟回拨(如NTP同步调整、服务器重启),可能生成重复ID(例如:A机器在10:00生成ID,时钟回拨到9:59后,会生成与9:59时重复的ID)。
-
机器ID管理成本:需手动分配机器ID(如通过配置文件、注册中心),若重复分配会导致ID冲突;在动态弹性环境下(如K8s容器频繁启停),如何可靠、唯一地分配和回收机器ID(WorkerID/DatacenterID) 是个挑战。
-
时间戳耗尽: 41位时间戳在选定的起点后约69年耗尽,需提前规划。
适用场景:适用于几乎一切中大型分布式系统(电商、社交、金融等),尤其是对性能、有序性、存储空间要求高的场景。需要做好机器ID管理和时钟回拨防护。 雪花算法变种众多(如百度的UidGenerator、美团的Leaf-snowflake),优化了时钟和机器ID问题。
4、雪花算法(Snowflake)
4.1、原理解析
????????雪花算法实现简单、适配性强,无论是电商订单、日志追踪还是分布式存储,都能满足 “唯一、有序、高效、可扩展” 的核心需求,因此成为分布式ID主流选择。雪花算法生成的ID是一个64位的整数,由多段不同意义的数字拼接而成,这种分段设计让每个ID既带着时间印记,又能规避多机器冲突,就像身份证通过地址码、出生日期码、顺序码等分段信息实现全国唯一标识,既有序又精准。
符号位 + 时间戳 + 数据中心ID + 机器ID + 序列号
-
符号位(1位):始终为0(表示正数)。这保证了生成的 ID 是正整数。
-
时间戳(41位):雪花算法的核心部分, 记录生成ID时的毫秒级时间戳(当前时间减去起始时间的差值),该部分保证了ID的大体有序性。41 位能表示的时间范围约为 2^41 毫秒 ≈ 69年。使用一个最近的起始时间(如 2025-08-04 00:00:00),可以大幅减少时间戳占用的位数。
-
数据中心ID(5位):用于标识生成ID的逻辑数据中心,允许最多 2^5 = 32个数据中心。
-
工作节点ID(5位):用于标识数据中心内的具体工作节点(机器、服务进程、Pod 等),允许每个数据中心最多 2^5 = 32个工作节点。在实际开发中,数据中心(高位)+ 工作节点(低位)经常被视为一个整体10位的机器ID,用于标识集群中的唯一节点(机器/服务实例),最多允许 2^10 = 1024个唯一节点。
-
序列号(12位):用来解决同一节点在同一毫秒内生成多个 ID 时的冲突问题。每个节点在每毫秒内都可以独立地从0开始递增生成序列号,当序列号用完(达到 4095)后,会强制等待到下一毫秒再继续生成。对于12位序列号,单节点每毫秒最多生成4096个ID,要达到这个并发量很极端(单节点超过400万QPS),现实中很难溢出。
????????以下为java实现的雪花算法代码示例(未考虑时钟回拨),起始时间决定了算法能生成ID的有效时长,通常将起始时间设为项目上线日期。
public class SnowflakeIdGenerator {
// 起始时间戳,这里以2025-08-04 00:00:00为基准
private final long startTimeStamp = 1751299200000L;
// 机器ID所占位数
private final long workerIdBits = 5L;
// 数据中心ID所占位数
private final long dataCenterIdBits = 5L;
// 序列号所占位数
private final long sequenceBits = 12L;
// 机器ID最大值 31
private final long maxWorkerId = -1L ^ (-1L << workerIdBits);
// 数据中心ID最大值 31
private final long maxDataCenterId = -1L ^ (-1L << dataCenterIdBits);
// 机器ID向左移位数
private final long workerIdShift = sequenceBits;
// 数据中心ID向左移位数
private final long dataCenterIdShift = sequenceBits + workerIdBits;
// 时间戳向左移位数
private final long timestampShift = sequenceBits + workerIdBits + dataCenterIdBits;
// 序列号掩码 4095
private final long sequenceMask = -1L ^ (-1L << sequenceBits);
// 工作机器ID
private final long workerId;
// 数据中心ID
private final long dataCenterId;
// 序列号
private long sequence = 0L;
// 上次生成ID的时间戳
private long lastTimestamp = -1L;
// 构造函数
public SnowflakeIdGenerator(long workerId, long dataCenterId) {
if (workerId > maxWorkerId || workerId < 0) {
throw new IllegalArgumentException("Worker ID 不能大于 " + maxWorkerId + " 或小于 0");
}
if (dataCenterId > maxDataCenterId || dataCenterId < 0) {
throw new IllegalArgumentException("数据中心 ID 不能大于 " + maxDataCenterId + " 或小于 0");
}
this.workerId = workerId;
this.dataCenterId = dataCenterId;
}
// 生成下一个ID
public synchronized long nextId() {
long currentTimestamp = System.currentTimeMillis();
if (currentTimestamp == lastTimestamp) {
sequence = (sequence + 1) & sequenceMask;
if (sequence == 0) {
// 当前毫秒内序列号已用完,等待下一毫秒
currentTimestamp = waitNextMillis(lastTimestamp);
}
} else {
// 时间戳改变,重置序列号
sequence = 0L;
}
lastTimestamp = currentTimestamp;
// 按规则组合生成ID
return ((currentTimestamp - startTimeStamp) << timestampShift) |
(dataCenterId << dataCenterIdShift) |
(workerId << workerIdShift) |
sequence;
}
// 等待下一毫秒
private long waitNextMillis(long lastTimestamp) {
long timestamp = System.currentTimeMillis();
while (timestamp <= lastTimestamp) {
timestamp = System.currentTimeMillis();
}
return timestamp;
}
// 测试示例
public static void main(String[] args) {
SnowflakeIdGenerator idGenerator = new SnowflakeIdGenerator(1, 1);
for (int i = 0; i < 10; i++) {
System.out.println(idGenerator.nextId());
}
}
}
4.2、为什么会出现重复ID?
????????雪花算法虽代码量少、实现简单,却并非万无一失。不少研发人员常常直接从网上拷贝现成的工具类,或是用大模型生成代码后直接用于生产环境 —— 直到某天突然收到用户反馈:自己账号的数据出现了错乱,明明只买了一件衣服,订单却显示多个其他辣眼的商品,还附带陌生的收货地址。手忙脚乱一顿排查后,竟发现数据库中出现了少量订单SN重复的异常数据,不由得心生疑惑:雪花算法不是每毫秒能生成4096个不重复编号吗?订单服务部署了十几个节点,但业务量真有这么大吗?到底为什么会出现重复呢?我们一起来一探究竟。
4.2.1、机器ID重复
-
为什么重复: 多个运行的节点使用相同的数据中心ID(datacenter-id)和工作节点ID(worker-id)。即在同一毫秒内,如果多个节点的机器ID相同、系统时间戳相同,序列号就可能从相同起点开始分配并重叠,导致生成完全相同的ID三元组 (时间戳, 机器ID, 序列号)。
-
典型现象: 多数研发人员会将数据中心ID和工作节点ID硬编码在代码中,或在配置文件里设置了相同的 datacenter-id与worker-id,这直接导致无论部署多少个节点,机器ID都完全一致。
-
如何解决:
????????核心原则必须确保整个分布式集群中,任何两个同时工作的节点,它们的 (数据中心ID, 工作节点ID) 二元组(或者将二者视为10位合并的“机器ID”)必须是唯一的!
(1)手动配置文件:在启动服务前,为每个节点的配置文件(如 application.properties, application.yml, configmap 等)显式配置一个唯一的 datacenter-id 和 worker-id。该方案简单直观,但繁琐,易出错(配置冲突),适合小型、静态集群,不适用于节点动态伸缩的集群。
(2)系统环境变量:在部署节点(物理机、虚拟机、容器)时,通过启动脚本、容器编排系统(如K8s Deployment/StatefulSet 的env)为每个实例设置唯一的 SNOWFLAKE_DATACENTER_ID和SNOWFLAKE_WORKER_ID环境变量,服务启动时读取这些环境变量。
(3)利用基础设施的唯一性:
-
Kubernetes StatefulSet会为每个 Pod 分配一个固定且有序的唯一索引(从0开始)。比如名为snowflake-app的StatefulSet有3个Pod:snowflake-app-0, snowflake-app-1, snowflake-app-2。应用程序可以读取 spec.podName(通常是 HOSTNAME环境变量),解析末尾的数字索引,将这个索引直接用作工作节点ID。若业务需要扩容至超过worker-id最大阈值(如32个以上Pod),直接使用索引会导致worker-id重复,需结合数据中心ID(datacenter-id)拆分(如用 StatefulSet 名称哈希作为datacenter-id)。
-
公有云(如阿里云、华为云、腾讯云ECS)会为每个虚拟机实例分配一个唯一ID,Pod(如Deployment)运行时也有自己的ID。应用程序可以在启动时通过查询实例/容器的元数据服务获取这个唯一ID,然后对这个较长的ID进行哈希并取模,映射到可用的datacenter-id和worker-id范围内(如总ID%1024,得到 0-1023的一个值)。该方案需要依赖特定平台的 API/服务。哈希取模存在极小冲突风险,需要设计好映射逻辑。
-
利用IP地址 (网络标识):应用程序直接获取其运行环境(Pod、容器、虚拟机、物理机)的IP地址,对整个IP地址字符串或二进制表示计算哈希值取模,然后取模,映射为datacenter-id和worker-id。在Kubernetes 中,在Kubernetes中,Pod通常可以通过status.podIP获得,Deployment Pod重建通常会获得新IP;虚拟机/物理机IP也可能因维护、迁移或网络配置变更而改变。该方案同样存在极小概率ID冲突,且需容忍获取IP的性能开销和失败风险。
// 获取机器ID
private static long getNodeId() {
try {
InetAddress address = findFirstNonLoopbackAddress();
String ip = address.getHostAddress();
int hash = ip.hashCode();
// 确保非负数并取模最大节点ID
long nodeId = (hash & 0x7FFFFFFF) % (MAX_NODE_ID + 1);
System.out.println("使用IP地址: " + ip + " 生成机器ID: " + nodeId);
return nodeId;
} catch (Exception e) {
// 异常时随机生成节点ID
long nodeId = new Random().nextInt((int) (MAX_NODE_ID + 1));
System.out.println("获取IP失败,随机生成机器ID: " + nodeId);
return nodeId;
}
}
// 查找第一个非环回IPv4地址
private static InetAddress findFirstNonLoopbackAddress() throws SocketException {
Enumeration<NetworkInterface> interfaces = NetworkInterface.getNetworkInterfaces();
while (interfaces.hasMoreElements()) {
NetworkInterface iface = interfaces.nextElement();
if (iface.isLoopback() || iface.isVirtual() || !iface.isUp()) {
continue;
}
Enumeration<InetAddress> addresses = iface.getInetAddresses();
while (addresses.hasMoreElements()) {
InetAddress addr = addresses.nextElement();
if (addr instanceof Inet4Address && !addr.isLoopbackAddress()) {
return addr;
}
}
}
throw new RuntimeException("未找到非环回IPv4地址");
}
(4)外部协调服务:使用分布式协调服务(如ZooKeeper, etcd, Redis, 数据库)来注册节点并分配唯一的机器 ID。如Leaf-Snowflake改进了雪花算法,机器ID由Zookeeper协调分配、百度UID Generator启动时向DB注册节点分配唯一worker_id。
-
流程示例:
-
节点启动时,连接到协调服务。
-
如果节点宕机或与协调服务断开连接(session超时),协调服务会自动删除其对应的临时节点,该机器ID被释放,可以被新节点申请使用。
-
节点将这个唯一的序号作为它的机器ID(或从中计算 datacenter-id 和 worker-id,如序号 % 1024)。序号在服务运行期间保持不变。
-
节点读取自己创建的节点的序号(如 0000000005)。
-
协调服务保证创建的有序节点的名称(包含一个单调递增的序号)是唯一的。
-
节点尝试在一个预设的路径下(如 /snowflake/workers)创建一个临时有序节点。
-
优点: 无需预配置,自动处理节点加入/离开,ID分配唯一且可靠,支持大规模集群。
-
缺点: 增加了外部依赖和复杂度。
(5)设计机器ID位数的考虑:默认10位能支持 1024 个节点,对大多数公司规模通常够用。可依据业务规模灵活调整:
- 并发量高但集群规模不大(节点少): 可以减少datacenter-id和worker-id 总位数(比如降到8位甚至更少),把节省出来的位数加到sequence序列号上。这样每个节点每毫秒可以生成更多的ID。
- 集群规模巨大(超过1024节点): 需要增加datacenter-id和worker-id总位数(比如设为12位)。这时需要牺牲timestamp或sequence 的位数(如时间戳减到40,序列号减到11位)。牺牲时间戳位数会缩短系统的可用年限;牺牲序列号会降低单节点/毫秒的最大并发量。
4.2.2、时钟回拨
-
为什么重复:系统时间因为NTP同步失败、闰秒调整、虚拟机/容器挂起恢复、人为设置错误等原因发生了向后跳跃,导致雪花算法生成ID时使用了之前已生成ID的时间戳部分,进而可能产生重复ID。
-
如何解决:大部分雪花算法的优秀实现都包含了时钟回拨检测和处理机制,如抛出异常、短暂等待、使用备用逻辑。
(1)预防为主:禁止手动时间修改;NTP通过频率调整、分散度控制、时钟筛选、步进限制等机制防止时间回拨,如使用chrony进行平滑时间调整(stepping → slewing)、配置clock slew而非 jump避免突变。
(2)抛出异常:当检测到时钟回拨时,直接抛出异常,停止生成ID,等待人工干预或时间恢复正常。该方案简单安全,但影响业务连续性。
//处理时钟回拨
if (currentTimestamp < lastTimestamp) {
throw new ClockBackwardException(
"Clock moved backwards. Refusing to generate id for " +
(lastTimestamp - currentTimestamp) + " milliseconds");
}
(3)等待时钟恢复(适合毫秒级轻度回拨):若发现回拨,不立即报错,而是阻塞等待 ,直到系统时间 ≥ lastTimestamp。该方案短暂阻塞,可能影响性能。
// 处理时钟回拨
if (currentTimestamp < lastTimestamp) {
long offset = lastTimestamp - currentTimestamp;
// 回拨时间小于1秒,阻塞等待
if (offset <= MAX_BACKWARD_TIME) {
currentTimestamp = waitForClockRecovery(lastTimestamp);
} else {
// 回拨时间超过1秒,抛出异常
throw new RuntimeException("Clock moved backwards too much: " + offset + "ms");
}
}
private long waitForClockRecovery(long lastTimestamp) {
long timestamp = System.currentTimeMillis();
while (timestamp < lastTimestamp) {
//短暂休眠避免CPU空转
try {
Thread.sleep(1);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException("Interrupted while waiting for clock recovery", e);
}
timestamp = System.currentTimeMillis();
}
System.out.println("Clock recovered after " + (timestamp - lastTimestamp) + "ms");
return timestamp;
}
(4)回拨补偿:通过累积所有历史回拨时间,使生成器内部时间永远领先于系统时间,可避免使用Thread.sleep()造成的性能瓶颈。
// 可容忍的最大时钟回拨(毫秒)
private static final long MAX_BACKWARD_MS = 1000;
// 发生时钟回拨
if (currentTimestamp < lastTimestamp) {
long backwardMs = lastTimestamp - currentTimestamp;
// 超过容忍阈值则抛出异常
if (backwardMs > MAX_BACKWARD_MS) {
throw new IllegalStateException("Clock moved backwards by " + backwardMs + " ms, exceeding maximum allowed value");
}
// 记录回拨时间用于补偿
clockOffset += backwardMs;
// 补偿当前时间戳
currentTimestamp = System.currentTimeMillis()+clockOffset;
}
(5)扩展位机制(秒级以上严重回拨):修改雪花算法结构,预留几位用于表示“是否处于回拨状态”或“回拨次数”。当发生回拨时,增加“回拨版本号”,即使时间戳相同,版本不同也能区分ID。
5、Snowflake变种
????????美团 Leaf是由美团点评开源的一款高性能、高可用的分布式ID生成服务。它提供Web控制台,可以查看当前节点状态、TPS、WorkerID分配情况等,便于排查问题。Leaf支持Segment号段和Snowflake两种模式。
(1)Leaf-Snowflake:对原生Snowflake做了优化,使用ZooKeeper管理节点ID;当检测到系统时间回拨时,会抛出告警或进入等待/拒绝策略,防止ID冲突。
(2)Leaf-Segment(主流推荐)
原理:
- DB预分配号段:类似于数据库批量取号段,服务每次从数据库获取一个连续的ID范围缓存在内存中消费,减少数据库压力。
- 双Buffer优化:异步填充下一个号段,避免分配阻塞。
优点:
- 完全回避时钟问题:ID基于DB自增,与时间无关。
- 高吞吐低延迟:本地缓存号段,QPS可达数万级。
缺点:
- 依赖DB:DB需高可用(主从+监控),故障影响ID生成。
-
号段跨步浪费:服务重启时缓存未使用号段会丢弃。
????????百度UID Generator的目标是解决高并发场景下的性能瓶颈,同时降低运维成本。它基于Snowflake改进,提供两种生成器:
-
DefaultUidGenerator:基础版,调整了Snowflake的结构(用 “秒级时间戳” 替代毫秒级,32位秒级时间戳可使用约136年;16 位机器ID+16位序列号),降低时间戳精度以换取更长的可用周期。
-
CachedUidGenerator:高性能版,引入 “RingBuffer(环形缓冲区)” 预生成 ID。启动时在内存中初始化一个RingBuffer,后台线程提前生成一批ID并填充到缓冲区;生成ID时直接从缓冲区 “取走” 即可(无需实时计算),缓冲区快空时自动触发补充。这种 “预生成 + 缓存” 机制大幅提升了性能。
优点:
- 性能超高:取ID仅内存操作,单机QPS可达百万级。
-
无时钟问题:ID由DB序列生成,时间戳只作为ID字段。
缺点:
- 重启初始化慢:首次启动需填充整个缓存(如200万ID)。
-
缓存未消费浪费:服务关闭时未使用ID丢失。
维度 | 原生Snowflake | 美团Leaf | 百度UID Generator |
定位 | Twitter 开源,分布式 ID 生成的经典方案,面向无外部依赖的轻量场景 | 美团内部开源,面向业务灵活性设计,支持多模式适配不同场景(兼顾性能与易用性) | 百度内部开源,基于 Snowflake优化,面向高并发场景,强调自动化与高性能 |
原理 | 基于「时间戳 + 机器 ID + 序列号」的 64 位 ID 生成,纯内存计算 | 支持两种模式: | 基于 Snowflake 改进,支持两种生成器: |
ID结构 | 常规结构: | 号段模式:无固定结构(由 DB 号段 + 本地递增组成,可自定义位数) | DefaultUidGenerator:32位时间戳+16位机器ID+16位序列号 |
机器ID分配 | 需手动配置(如通过配置文件 / 部署脚本指定,需保证集群内唯一) | 号段模式:无需机器ID(依赖 DB 号段隔离) | 自动分配(基于DB维护 workerID表,启动时从 DB获取,支持动态扩容) |
时钟回拨处理 | 无原生处理逻辑(时钟回拨时可能生成重复 ID) | 号段模式:无时钟依赖(ID 基于号段递增,与时间无关) | 记录历史最大时间戳: |
性能表现 | 纯内存计算,无外部交互,单机性能极高(可达10万+/ 秒) | 号段模式:减少DB交互(一次取一段ID,本地生成),性能较好(万级 / 秒,取决于号段大小) | CachedUidGenerator:基于 RingBuffer 预生成ID,无实时计算开销,性能最优(单机百万级/秒) |
外部依赖 | 无任何外部依赖(仅依赖本地系统时钟) | 号段模式:依赖关系型数据库(如MySQL,用于存储号段) | 依赖关系型数据库(用于存储workerID 分配信息) |
ID连续性 | 不连续(时间戳 / 机器 ID 变化会导致 ID 跳变) | 号段模式:局部连续(同号段内 ID 连续,跨号段跳变) | 不连续(时间戳 / 序列号递增,但存在机器 ID 隔离导致的跳变) |
ID有序性 | 全局大致有序(按时间戳递增,同毫秒内按序列号递增) | 号段模式:局部有序(同号段内严格递增) | 全局大致有序(时间戳 + 序列号递增,缓存预生成不影响最终顺序) |
优缺点 | 优点:无外部依赖、实现简单、性能极高 | 优点:支持两种模式(灵活适配场景)、号段模式可兼容旧系统 | 优点:机器ID自动分配(无需手动配置)、高性能(缓存预生成)、时钟回拨处理完善 |
适用场景 | 无 DB 依赖需求、节点数量固定(≤1024)、对 ID生成性能要求高的场景(如分布式系统基础 ID) | 需灵活切换模式(如部分场景需连续 ID、部分需无DB依赖)、可接受 DB依赖的业务 | 高并发场景(如秒杀、订单)、节点动态扩容(无需手动配置机器 ID)、需严格避免ID重复 |
????????原生Snowflake、美团Leaf和百度UID Generator三者没有绝对优劣,核心是匹配业务对 “依赖、性能、连续性” 的优先级 —— 分布式ID的设计本质,是对 “唯一、有序、性能、无依赖” 的取舍与平衡。
专栏文章推荐: