在分布式系统架构中,全局唯一ID是贯穿全业务链路的核心基石。从分库分表的分片键、交易订单的流水号,到分布式链路追踪的TraceId,无一不依赖高可靠的分布式ID生成能力。单体架构中依赖数据库自增主键的方案,在分布式场景下暴露出无法解决的瓶颈:多实例自增ID重复、分库分表后主键冲突、高并发场景下性能不足等问题。
本文将从底层原理出发,拆解分布式ID的核心设计准则,深度剖析当前互联网行业最主流的两种方案——雪花算法与号段模式,不仅讲透实现逻辑,更会梳理生产环境中绝大多数开发者都会踩的架构坑,同时提供实现方案与最佳实践。
一、分布式ID的核心设计准则
分布式ID的设计,本质是在唯一性、性能、可用性、业务适配性之间找到最优平衡,行业内公认的核心设计准则包含以下6点:
1. 全局唯一性
这是分布式ID的核心底线,无论多少个服务实例、多少并发请求,生成的ID必须在全系统范围内绝对唯一,不能出现任何重复。
2. 趋势递增性
ID需要满足整体趋势递增的特性。对于InnoDB存储引擎来说,主键索引采用B+树结构,有序的主键可以保证数据插入时的页分裂最小化,大幅提升数据库写入性能;同时,有序ID也更便于按时间范围进行数据检索与归档。
3. 极致高性能
ID生成是高频调用的基础能力,必须保证极低的响应延迟,同时支撑超高的QPS,绝对不能成为系统的性能瓶颈。
4. 原生高可用
分布式系统的核心诉求是容错能力,ID生成服务必须保证高可用,避免单点故障,支持集群部署,即使部分节点宕机,也能持续提供服务。
5. 业务安全性
ID不能包含敏感信息,同时要避免被恶意遍历。例如,纯自增ID会直接暴露业务的订单量、用户量等核心数据,存在严重的业务安全风险。
6. 存储兼容性
ID的长度需要适配主流的存储系统,最佳实践是采用64位长整型(long),可以完美适配Java的基础数据类型、MySQL的bigint字段,避免字符串类型带来的存储开销与索引性能下降。
二、主流分布式ID方案全景对比
当前行业内常见的分布式ID方案,各有其适用场景与局限性,先通过核心特性对比,明确各方案的边界:
| 方案类型 | 核心实现 | 核心优势 | 核心缺陷 |
| UUID/GUID | 基于随机数/ MAC地址生成32位字符串 | 无中心化,本地生成,性能极高 | 无序,导致数据库索引性能极差;字符串存储开销大;存在极低概率的重复风险 |
| 数据库自增主键 | 依赖MySQL的auto_increment特性 | 实现简单,ID绝对连续有序 | 分布式场景下多实例重复;数据库成为性能瓶颈与单点故障;无法支撑分库分表场景 |
| Redis自增 | 基于Redis的INCR命令实现 | 性能高于数据库自增,支持集群 | 依赖Redis的持久化,宕机重启后可能出现ID重复;强依赖Redis集群,运维成本高 |
| 雪花算法 | 基于时间戳+机器ID+序列号的64位结构 | 无中心化,本地生成,性能极高,趋势递增 | 强依赖系统时钟,存在时钟回拨导致ID重复的风险;机器ID分配不当会导致重复 |
| 号段模式 | 基于数据库预分配ID区间,本地内存消费 | 性能极高,无时钟依赖,高可用,业务隔离性好 | 强依赖数据库,ID连续易被遍历;服务重启会造成号段浪费 |
从对比可以看出,雪花算法与号段模式是当前分布式系统中适用性最广、落地性最强的两种方案,接下来我们将深度拆解这两种方案的底层原理、实现细节与架构避坑。
三、雪花算法(Snowflake):无中心化的分布式ID方案
雪花算法是Twitter开源的分布式ID生成算法,核心设计思想是通过拆分64位长整型的存储空间,将ID的生成逻辑拆解为可独立控制的多个维度,实现无中心化的全局唯一ID生成。
3.1 底层结构与核心原理
标准雪花算法将64位有符号长整型(long)拆分为4个独立的段,每一段都承担不同的职责,结构如下:
| 符号位 | 时间戳位 | 工作机器ID位 | 序列号位 |
| 1位 | 41位 | 10位 | 12位 |
各段的详细逻辑如下:
- 1位符号位:固定为0。因为Java中的long类型是有符号数,最高位为符号位,0表示正数,1表示负数,ID均为正数,因此符号位固定为0。
- 41位时间戳位:存储毫秒级时间戳,采用相对时间而非1970年纪元时间。41位无符号整数可以表示2^41个毫秒值,换算成年份约为69年,完全满足绝大多数业务系统的生命周期需求。
- 10位工作机器ID位:用于区分分布式系统中的不同服务节点,通常拆分为5位数据中心ID+5位工作节点ID,最多可以支持1024个服务节点。
- 12位序列号位:用于处理同一毫秒内的并发请求,同一毫秒内序列号从0开始自增,最多可以支持4096个ID/毫秒,换算成单节点QPS最高可达409.6万,完全满足绝大多数高并发业务的需求。
雪花算法的核心逻辑可以总结为:通过时间戳保证ID的趋势递增,通过机器ID保证分布式环境下的全局唯一性,通过序列号保证同一毫秒内的并发能力,三者结合,实现了无中心化、高性能的ID生成。
3.2 标准实现代码
package com.jam.demo.distributed.id.snowflake;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.Assert;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.util.concurrent.atomic.AtomicLong;
/**
* 雪花算法ID生成器
*
* @author ken
*/
@Slf4j
public class SnowflakeIdGenerator {
/**
* 纪元起始时间:2025-01-01 00:00:00
*/
private static final long EPOCH_START = LocalDateTime.of(2025, 1, 1, 0, 0, 0)
.toInstant(ZoneOffset.UTC).toEpochMilli();
/**
* 机器ID位数
*/
private static final long WORKER_ID_BITS = 10L;
/**
* 序列号位数
*/
private static final long SEQUENCE_BITS = 12L;
/**
* 机器ID最大值:2^10 - 1 = 1023
*/
private static final long MAX_WORKER_ID = (1L << WORKER_ID_BITS) - 1;
/**
* 序列号最大值:2^12 - 1 = 4095
*/
private static final long MAX_SEQUENCE = (1L << SEQUENCE_BITS) - 1;
/**
* 机器ID左移位数:12位
*/
private static final long WORKER_ID_SHIFT = SEQUENCE_BITS;
/**
* 时间戳左移位数:10 + 12 = 22位
*/
private static final long TIMESTAMP_SHIFT = SEQUENCE_BITS + WORKER_ID_BITS;
/**
* 最大时钟回拨容忍时间:500毫秒
*/
private static final long MAX_CLOCK_BACKWARD_MS = 500L;
/**
* 工作机器ID
*/
private final long workerId;
/**
* 上次生成ID的时间戳
*/
private final AtomicLong lastTimestamp = new AtomicLong(-1L);
/**
* 当前毫秒内的序列号
*/
private final AtomicLong sequence = new AtomicLong(0L);
/**
* 构造方法
*
* @param workerId 工作机器ID
*/
public SnowflakeIdGenerator(long workerId) {
Assert.isTrue(workerId >= 0 && workerId <= MAX_WORKER_ID,
"workerId must be between 0 and " + MAX_WORKER_ID);
this.workerId = workerId;
log.info("SnowflakeIdGenerator initialized, workerId:{}", workerId);
}
/**
* 生成全局唯一ID
*
* @return 分布式ID
*/
public long nextId() {
long currentTs = getCurrentTimestamp();
long lastTs = lastTimestamp.get();
if (currentTs < lastTs) {
handleClockBackward(currentTs, lastTs);
currentTs = getCurrentTimestamp();
}
if (currentTs == lastTs) {
long currentSequence = sequence.incrementAndGet();
if (currentSequence > MAX_SEQUENCE) {
currentTs = waitUntilNextMillisecond(lastTs);
sequence.set(0L);
}
} else {
sequence.set(0L);
}
lastTimestamp.set(currentTs);
return assembleId(currentTs);
}
/**
* 处理时钟回拨
*
* @param currentTs 当前时间戳
* @param lastTs 上次生成ID的时间戳
*/
private void handleClockBackward(long currentTs, long lastTs) {
long backwardMs = lastTs - currentTs;
if (backwardMs <= MAX_CLOCK_BACKWARD_MS) {
try {
log.warn("Clock backward detected, backward {}ms, waiting...", backwardMs);
Thread.sleep(backwardMs);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException("Clock backward wait interrupted", e);
}
} else {
log.error("Clock backward exceeded max tolerance, backward {}ms", backwardMs);
throw new RuntimeException("Clock backward exceeded max tolerance, cannot generate id");
}
}
/**
* 等待直到下一毫秒
*
* @param lastTs 上次生成ID的时间戳
* @return 下一毫秒的时间戳
*/
private long waitUntilNextMillisecond(long lastTs) {
long currentTs = getCurrentTimestamp();
while (currentTs <= lastTs) {
currentTs = getCurrentTimestamp();
}
return currentTs;
}
/**
* 组装最终的ID
*
* @param currentTs 当前时间戳
* @return 组装后的ID
*/
private long assembleId(long currentTs) {
return ((currentTs - EPOCH_START) << TIMESTAMP_SHIFT)
| (workerId << WORKER_ID_SHIFT)
| sequence.get();
}
/**
* 获取当前系统时间戳
*
* @return 毫秒级时间戳
*/
private long getCurrentTimestamp() {
return System.currentTimeMillis();
}
}
3.3 雪花算法生成流程
3.4 生产环境核心避坑指南
雪花算法的实现看似简单,但在生产环境中,有大量开发者因为忽略了底层细节,导致出现ID重复、服务不可用等严重故障,以下是最核心的6个坑点与解决方案:
坑点1:时钟回拨导致ID重复
这是雪花算法最致命的问题,也是生产环境中最常见的故障原因。问题根源:系统时钟因为NTP时间同步、虚拟机挂起恢复、宿主机时间调整、闰秒等原因,出现时间回退,导致当前时间戳小于上次生成ID的时间戳,进而出现ID重复。解决方案:
- 多层防护策略:代码中实现最大容忍回拨时间,小幅度回拨采用等待策略,大幅度回拨直接抛出异常并触发告警,避免生成重复ID;
- 机器ID动态切换:预分配多个备用机器ID,当时钟回拨超过容忍值时,自动切换到备用机器ID,继续提供服务;
- 时钟同步管控:生产环境关闭服务节点的自动时间同步,采用统一的时钟服务器,避免节点时钟频繁调整;
- 持久化时间戳:将每次生成ID的时间戳持久化到本地文件或配置中心,服务重启时校验当前系统时钟是否大于持久化的时间戳,避免重启后时钟回拨。
坑点2:机器ID重复导致ID重复
机器ID是保证分布式环境下ID唯一的核心,一旦出现两个节点使用相同的机器ID,必然会出现ID重复。问题根源:手动配置机器ID时出现配置错误;容器化部署时,Pod重启后IP变化,基于IP哈希生成的机器ID出现重复;多集群部署时,不同集群的机器ID分配范围重叠。解决方案:
- 基于注册中心动态分配:采用Nacos、Zookeeper、Etcd等注册中心,实现机器ID的自动分配与回收,节点启动时向注册中心申请唯一的机器ID,节点下线时释放,保证全局唯一;
- 固定机器ID分配范围:按集群、机房划分机器ID的分配区间,比如机房A使用0-255,机房B使用256-511,避免跨机房重复;
- 机器ID持久化:将分配到的机器ID持久化到节点的本地配置文件中,节点重启时优先使用本地持久化的机器ID,避免重启后机器ID变化。
坑点3:纪元时间设置不合理导致可用时间不足
很多开发者直接使用1970年作为纪元起始时间,导致ID的可用生命周期大幅缩短。问题根源:41位时间戳的可用时间是69年,若使用1970年作为纪元,到2026年已经使用了56年,仅剩13年的可用时间,无法满足系统的长期运行需求。解决方案:将纪元起始时间设置为系统上线的时间,比如2025-01-01,这样可以获得完整的69年可用时间,完全满足系统的生命周期需求。
坑点4:位数分配不合理导致业务适配不足
标准雪花算法的10位机器ID、12位序列号的分配,并不适用于所有业务场景。问题根源:对于超大规模集群,1024个节点无法满足需求;对于超高并发的单节点业务,4096/毫秒的序列号无法满足峰值需求。解决方案:根据业务场景灵活调整位数分配,例如:
- 超大规模集群场景:将机器ID调整为13位(最多8192个节点),序列号调整为9位(最多512/毫秒);
- 超高并发单节点场景:将机器ID调整为8位(最多256个节点),序列号调整为14位(最多16384/毫秒); 调整的核心原则是:保证时间戳位数不低于40位,确保系统的可用生命周期,同时平衡节点数量与单节点并发能力。
坑点5:忽略跨节点ID的非绝对递增问题
雪花算法只能保证单节点内的ID绝对递增,无法保证跨节点的ID绝对递增。问题根源:不同节点的系统时钟存在毫秒级的偏差,可能出现节点A生成的ID的时间戳小于节点B之前生成的ID的时间戳,导致跨节点的ID出现乱序。解决方案:
- 对于分库分表场景,若采用ID作为分片键,乱序不会影响分片逻辑,无需额外处理;
- 对于需要全局绝对递增ID的场景,不适合使用原生雪花算法,建议采用号段模式;
- 统一集群内所有节点的时钟源,最小化节点间的时钟偏差,降低乱序的概率。
坑点6:ID安全性不足导致业务数据泄露
原生雪花算法的ID可以被反解出时间戳、机器ID等信息,同时连续的ID也存在被遍历的风险。问题根源:攻击者可以通过反解ID的时间戳,获取订单的创建时间、业务的峰值时段等信息;通过连续的ID遍历,爬取业务的全量数据。解决方案:
- 对生成的ID进行轻量级加密,比如采用异或加密、位运算混淆,避免ID被反解;
- 调整序列号的生成逻辑,采用随机起始值而非固定从0开始,避免ID连续;
- 不直接将雪花算法ID暴露给前端,采用加密后的字符串作为对外的流水号。
四、号段模式:高可用强一致的分布式ID方案
号段模式是当前互联网大厂最主流的分布式ID方案,美团Leaf、滴滴TinyId、百度UidGenerator等开源方案,均基于号段模式的核心思想实现。其核心逻辑是通过预分配的方式,将ID生成的压力从数据库转移到本地内存,同时彻底解决了时钟依赖问题,实现了极高的性能与可用性。
4.1 核心原理与架构设计
号段模式的核心思想是:将ID的生成拆分为"号段申请"与"本地分配"两个阶段,不再每次生成ID都访问数据库,而是一次性从数据库申请一段连续的ID区间(号段),加载到本地内存中,业务申请ID时,直接从本地内存的号段中自增获取,当号段耗尽时,再向数据库申请新的号段。
4.1.1 核心数据结构设计
号段模式的核心依赖是一张号段分配表,用于存储不同业务的号段分配信息,MySQL 8.0的表结构设计如下:
CREATE TABLE `leaf_alloc` (
`biz_tag` varchar(128) NOT NULL COMMENT '业务标签,唯一主键',
`max_id` bigint NOT NULL DEFAULT '0' COMMENT '当前已分配的最大ID',
`step` int NOT NULL COMMENT '号段步长',
`description` varchar(256) DEFAULT NULL COMMENT '业务描述',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`version` int NOT NULL DEFAULT '1' COMMENT '乐观锁版本号',
PRIMARY KEY (`biz_tag`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='号段模式ID分配表';
-- 初始化业务数据
INSERT INTO `leaf_alloc` (`biz_tag`, `max_id`, `step`, `description`) VALUES ('order_biz', 0, 10000, '订单业务ID分配');
INSERT INTO `leaf_alloc` (`biz_tag`, `max_id`, `step`, `description`) VALUES ('user_biz', 0, 5000, '用户业务ID分配');
表中核心字段的含义:
- biz_tag:业务标签,是表的主键,用于区分不同的业务线,实现业务之间的隔离,不同业务的号段分配互不影响;
- max_id:当前业务已经分配出去的最大ID,每次申请号段时,会更新为
max_id + step; - step:号段步长,即每次申请的号段长度,决定了访问数据库的频率;
- version:乐观锁版本号,用于解决多实例并发申请号段时的并发问题,保证更新的原子性。
4.1.2 核心架构
架构的核心优势:
- 业务隔离:不同业务通过biz_tag隔离,步长独立配置,互不影响;
- 高性能:绝大多数ID生成本地内存操作,无数据库IO,性能可达百万级QPS;
- 高可用:本地号段可以支撑短时的数据库不可用,配合数据库主从集群,实现极高的可用性;
- 无时钟依赖:彻底解决了时钟回拨问题,不受系统时钟调整的影响。
4.2 双号段缓冲实现方案
单缓冲号段存在一个致命问题:当本地号段耗尽时,业务线程需要同步等待数据库的号段申请,导致请求延迟升高,甚至出现超时。因此,生产环境的号段模式必须采用双号段缓冲+异步预加载的方案,保证本地永远有可用的号段,彻底消除同步申请的阻塞问题。
双号段缓冲的核心逻辑:
- 服务维护两个号段:当前生效号段(currentSegment)与备用号段(nextSegment);
- 业务申请ID时,始终从当前生效号段中获取;
- 当当前生效号段的剩余ID数量达到预加载阈值(通常为步长的10%)时,异步启动线程申请下一个号段,加载到备用号段中;
- 当当前生效号段耗尽时,自动将备用号段切换为当前生效号段,同时清空备用号段,触发下一次预加载。
以下是完整的号段模式实现代码,基于MyBatisPlus实现数据访问,采用双号段缓冲+异步预加载,保证线程安全与高性能:
4.2.1 Maven依赖配置
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.4</version>
<relativePath/>
</parent>
<groupId>com.jam.demo</groupId>
<artifactId>distributed-id-demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>distributed-id-demo</name>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.6</version>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.30</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>33.1.0-jre</version>
</dependency>
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
<version>2.0.52</version>
</dependency>
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>2.5.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>
4.2.2 实体类定义
package com.jam.demo.distributed.id.segment.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDateTime;
/**
* 号段分配实体类
*
* @author ken
*/
@Data
@TableName("leaf_alloc")
@Schema(description = "号段分配实体")
public class LeafAlloc {
@TableId(value = "biz_tag", type = IdType.INPUT)
@Schema(description = "业务标签", requiredMode = Schema.RequiredMode.REQUIRED)
private String bizTag;
@TableField("max_id")
@Schema(description = "当前已分配的最大ID")
private Long maxId;
@TableField("step")
@Schema(description = "号段步长")
private Integer step;
@TableField("description")
@Schema(description = "业务描述")
private String description;
@TableField("update_time")
@Schema(description = "更新时间")
private LocalDateTime updateTime;
@TableField("version")
@Schema(description = "乐观锁版本号")
private Integer version;
}
4.2.3 Mapper接口定义
package com.jam.demo.distributed.id.segment.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.jam.demo.distributed.id.segment.entity.LeafAlloc;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Update;
/**
* 号段分配Mapper接口
*
* @author ken
*/
public interface LeafAllocMapper extends BaseMapper<LeafAlloc> {
/**
* 乐观锁更新号段最大ID
*
* @param bizTag 业务标签
* @param version 当前版本号
* @return 影响行数
*/
@Update("UPDATE leaf_alloc SET max_id = max_id + step, version = version + 1 WHERE biz_tag = #{bizTag} AND version = #{version}")
int updateMaxId(@Param("bizTag") String bizTag, @Param("version") Integer version);
}
4.2.4 号段实体与缓冲实现
package com.jam.demo.distributed.id.segment.model;
import lombok.Getter;
import java.util.concurrent.atomic.AtomicLong;
/**
* 号段实体
*
* @author ken
*/
@Getter
public class Segment {
/**
* 号段起始ID
*/
private final long startId;
/**
* 号段结束ID
*/
private final long endId;
/**
* 当前已分配的ID
*/
private final AtomicLong currentId;
/**
* 号段步长
*/
private final int step;
public Segment(long startId, long endId, int step) {
this.startId = startId;
this.endId = endId;
this.step = step;
this.currentId = new AtomicLong(startId);
}
/**
* 获取下一个ID
*
* @return 下一个ID,若号段耗尽返回-1
*/
public long nextId() {
long id = currentId.incrementAndGet();
if (id > endId) {
return -1;
}
return id;
}
/**
* 获取号段剩余可用ID数量
*
* @return 剩余数量
*/
public long remainingCount() {
long remain = endId - currentId.get();
return Math.max(remain, 0);
}
/**
* 判断号段是否耗尽
*
* @return 耗尽返回true,否则返回false
*/
public boolean isExhausted() {
return currentId.get() > endId;
}
}
4.2.5 号段模式核心服务实现
package com.jam.demo.distributed.id.segment.service;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import com.jam.demo.distributed.id.segment.entity.LeafAlloc;
import com.jam.demo.distributed.id.segment.mapper.LeafAllocMapper;
import com.jam.demo.distributed.id.segment.model.Segment;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import java.util.List;
import java.util.Map;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.locks.ReentrantLock;
/**
* 号段模式ID生成服务
*
* @author ken
*/
@Slf4j
@Service
public class SegmentIdService {
private final LeafAllocMapper leafAllocMapper;
/**
* 预加载阈值:剩余ID占比小于10%时触发预加载
*/
private static final double PRE_LOAD_THRESHOLD = 0.1;
/**
* 乐观锁更新最大重试次数
*/
private static final int MAX_RETRY_TIMES = 3;
/**
* 业务号段缓冲Map
*/
private final Map<String, SegmentBuffer> segmentBufferMap = new ConcurrentHashMap<>();
/**
* 异步预加载线程池
*/
private final ExecutorService preLoadExecutor = new ThreadPoolExecutor(
2,
4,
60L,
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(100),
new ThreadFactoryBuilder().setNameFormat("segment-preload-thread-%d").setDaemon(true).build(),
new ThreadPoolExecutor.CallerRunsPolicy()
);
public SegmentIdService(LeafAllocMapper leafAllocMapper) {
this.leafAllocMapper = leafAllocMapper;
this.initSegmentBuffer();
}
/**
* 初始化号段缓冲
*/
private void initSegmentBuffer() {
List<LeafAlloc> allAllocList = leafAllocMapper.selectList(new LambdaQueryWrapper<>());
if (CollectionUtils.isEmpty(allAllocList)) {
log.warn("No leaf_alloc data found, segment buffer init skipped");
return;
}
for (LeafAlloc leafAlloc : allAllocList) {
segmentBufferMap.put(leafAlloc.getBizTag(), new SegmentBuffer(leafAlloc.getBizTag()));
}
log.info("Segment buffer init completed, biz count:{}", allAllocList.size());
}
/**
* 生成分布式ID
*
* @param bizTag 业务标签
* @return 分布式ID
*/
public long nextId(String bizTag) {
Assert.hasText(bizTag, "bizTag must not be empty");
SegmentBuffer buffer = getSegmentBuffer(bizTag);
return buffer.nextId();
}
/**
* 获取业务对应的号段缓冲
*
* @param bizTag 业务标签
* @return 号段缓冲
*/
private SegmentBuffer getSegmentBuffer(String bizTag) {
SegmentBuffer buffer = segmentBufferMap.get(bizTag);
if (buffer == null) {
synchronized (this) {
buffer = segmentBufferMap.get(bizTag);
if (buffer == null) {
LeafAlloc leafAlloc = leafAllocMapper.selectById(bizTag);
Assert.notNull(leafAlloc, "bizTag:" + bizTag + " not exists");
buffer = new SegmentBuffer(bizTag);
segmentBufferMap.put(bizTag, buffer);
}
}
}
return buffer;
}
/**
* 从数据库申请新的号段
*
* @param bizTag 业务标签
* @return 新的号段
*/
private Segment fetchSegmentFromDb(String bizTag) {
LeafAlloc leafAlloc = null;
int retryTimes = 0;
while (retryTimes < MAX_RETRY_TIMES) {
leafAlloc = leafAllocMapper.selectById(bizTag);
Assert.notNull(leafAlloc, "bizTag:" + bizTag + " not exists");
int updated = leafAllocMapper.updateMaxId(bizTag, leafAlloc.getVersion());
if (updated > 0) {
break;
}
retryTimes++;
log.warn("Update max_id conflict, retry times:{}", retryTimes);
}
if (retryTimes >= MAX_RETRY_TIMES) {
log.error("Update max_id failed after {} retries, bizTag:{}", MAX_RETRY_TIMES, bizTag);
throw new RuntimeException("Fetch segment from db failed, update max_id conflict");
}
long endId = leafAlloc.getMaxId() + leafAlloc.getStep();
long startId = leafAlloc.getMaxId() + 1;
return new Segment(startId, endId, leafAlloc.getStep());
}
/**
* 号段缓冲类,封装双号段逻辑
*/
private class SegmentBuffer {
private final String bizTag;
/**
* 当前生效号段
*/
private volatile Segment currentSegment;
/**
* 备用号段
*/
private volatile Segment nextSegment;
/**
* 预加载状态标记
*/
private final AtomicBoolean preLoading = new AtomicBoolean(false);
/**
* 号段切换锁
*/
private final ReentrantLock lock = new ReentrantLock();
public SegmentBuffer(String bizTag) {
this.bizTag = bizTag;
this.currentSegment = fetchSegmentFromDb(bizTag);
}
/**
* 获取下一个ID
*
* @return 分布式ID
*/
public long nextId() {
long id = currentSegment.nextId();
if (id != -1) {
checkAndTriggerPreLoad();
return id;
}
return switchSegmentAndGetId();
}
/**
* 检查并触发预加载
*/
private void checkAndTriggerPreLoad() {
if (nextSegment == null
&& !preLoading.get()
&& currentSegment.remainingCount() < (long) currentSegment.getStep() * PRE_LOAD_THRESHOLD) {
if (preLoading.compareAndSet(false, true)) {
preLoadExecutor.submit(() -> {
try {
nextSegment = fetchSegmentFromDb(bizTag);
log.info("Preload segment completed, bizTag:{}, startId:{}, endId:{}",
bizTag, nextSegment.getStartId(), nextSegment.getEndId());
} catch (Exception e) {
log.error("Preload segment failed, bizTag:{}", bizTag, e);
} finally {
preLoading.set(false);
}
});
}
}
}
/**
* 切换号段并获取ID
*
* @return 分布式ID
*/
private long switchSegmentAndGetId() {
lock.lock();
try {
if (!currentSegment.isExhausted()) {
return currentSegment.nextId();
}
if (nextSegment == null) {
log.warn("Next segment is null, sync fetch segment, bizTag:{}", bizTag);
currentSegment = fetchSegmentFromDb(bizTag);
} else {
currentSegment = nextSegment;
nextSegment = null;
}
return currentSegment.nextId();
} finally {
lock.unlock();
}
}
}
}
4.2.6 对外接口实现
package com.jam.demo.distributed.id.segment.controller;
import com.jam.demo.distributed.id.segment.service.SegmentIdService;
import com.jam.demo.distributed.id.snowflake.SnowflakeIdGenerator;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 分布式ID生成接口
*
* @author ken
*/
@RestController
@RequestMapping("/id")
@Tag(name = "分布式ID生成接口", description = "提供雪花算法与号段模式的ID生成能力")
public class DistributedIdController {
private final SegmentIdService segmentIdService;
private final SnowflakeIdGenerator snowflakeIdGenerator;
public DistributedIdController(SegmentIdService segmentIdService) {
this.segmentIdService = segmentIdService;
this.snowflakeIdGenerator = new SnowflakeIdGenerator(1);
}
@GetMapping("/snowflake")
@Operation(summary = "生成雪花算法ID", description = "基于雪花算法生成全局唯一ID")
public Long getSnowflakeId() {
return snowflakeIdGenerator.nextId();
}
@GetMapping("/segment/{bizTag}")
@Operation(summary = "生成号段模式ID", description = "基于号段模式生成指定业务的全局唯一ID")
public Long getSegmentId(
@Parameter(description = "业务标签", required = true)
@PathVariable String bizTag) {
return segmentIdService.nextId(bizTag);
}
}
4.3 生产环境核心避坑指南
号段模式虽然解决了雪花算法的时钟依赖问题,但在生产环境落地时,依然有很多细节会导致性能问题、ID重复、服务不可用等故障,以下是最核心的6个坑点与解决方案:
坑点1:步长设置不合理导致性能与浪费的失衡
步长是号段模式最核心的参数,设置不合理会直接影响系统的性能与稳定性。问题根源:步长设置过小,会导致频繁访问数据库,数据库成为性能瓶颈,甚至出现乐观锁冲突;步长设置过大,服务重启时会导致大量未使用的ID被浪费,同时多实例部署时,ID的乱序程度会加剧。解决方案:
- 步长设置原则:根据业务的峰值QPS,设置步长为10-30分钟的ID消耗量,例如业务峰值QPS为1000,步长设置为1000 * 60 * 20 = 120万,保证20分钟才访问一次数据库;
- 动态步长调整:根据业务的QPS动态调整步长,低峰期使用较小的步长,减少ID浪费,高峰期自动增大步长,降低数据库访问频率;
- 按业务独立设置:不同业务根据自身的QPS设置独立的步长,避免低QPS业务使用过大的步长,造成不必要的浪费。
坑点2:单缓冲号段导致业务请求阻塞
单缓冲号段是新手最容易犯的错误,会直接导致业务请求的延迟升高,甚至超时。问题根源:单缓冲号段下,当本地号段耗尽时,业务线程需要同步等待数据库的号段申请,数据库的访问延迟通常在几十毫秒,会直接导致业务请求的延迟升高,高并发场景下甚至会出现线程阻塞。解决方案:采用双号段缓冲+异步预加载的方案,当当前号段的剩余ID达到阈值时,异步预加载下一个号段,保证本地永远有可用的号段,彻底消除同步申请的阻塞问题,这是生产环境的必选方案。
坑点3:乐观锁冲突导致号段申请失败
多实例部署时,多个节点同时申请同一个业务的号段,会出现乐观锁冲突,导致号段申请失败。问题根源:多个节点同时查询到同一个biz_tag的version值,同时执行更新操作,只有一个节点能更新成功,其他节点会更新失败,重试次数不足时会导致号段申请失败,无法生成ID。解决方案:
- 设置合理的重试次数:通常设置3次重试,绝大多数冲突都可以通过重试解决;
- 重试间隔退避:每次重试之间增加随机的延迟时间,避免多个节点同时重试,加剧冲突;
- 集群负载均衡:通过服务注册中心的负载均衡策略,将不同的业务请求路由到不同的节点,减少同一个biz_tag的并发申请次数。
坑点4:数据库单点故障导致服务不可用
号段模式强依赖数据库,数据库的可用性直接决定了ID生成服务的可用性。问题根源:单库部署时,数据库宕机后,所有节点都无法申请新的号段,当本地号段耗尽后,整个ID生成服务就会不可用。解决方案:
- 数据库高可用部署:采用MySQL一主多从、MGR集群等高可用架构,主库宕机后自动切换到从库,保证数据库的可用性;
- 多机房双写部署:跨机房部署两套数据库集群,双向同步数据,单机房故障时,自动切换到另一个机房的数据库;
- 本地缓冲兜底:合理设置步长,保证本地号段至少可以支撑30分钟以上的业务流量,即使数据库短时不可用,也不会影响服务的正常运行。
坑点5:业务隔离不足导致业务间互相影响
多个业务共用同一个biz_tag,会导致业务间的互相影响,甚至出现性能问题。问题根源:高QPS业务与低QPS业务共用同一个biz_tag,高QPS业务会频繁申请号段,导致乐观锁冲突加剧,影响低QPS业务的号段申请;同时,步长无法按业务特性设置,导致性能与浪费的失衡。解决方案:严格执行一个业务线一个biz_tag的原则,不同业务之间完全隔离,独立设置步长,互不影响,这是号段模式的核心设计原则。
坑点6:ID连续导致业务数据泄露
号段模式生成的ID是连续的,存在严重的业务安全风险。问题根源:连续的ID可以被攻击者轻易遍历,爬取业务的全量订单、用户数据;同时,通过ID的增长速度,可以轻易推算出业务的订单量、用户量等核心商业数据。解决方案:
- 对外暴露时进行加密处理:不直接将原始ID暴露给前端,采用加密算法对ID进行加密,生成加密后的字符串作为对外的流水号;
- 号段随机偏移:申请到号段后,对号段的起始ID进行随机偏移,避免ID完全连续;
- 增加随机位:在ID的末尾增加几位随机数,牺牲少量的ID容量,换取ID的非连续性,提升安全性。
五、雪花算法 vs 号段模式:选型指南与场景适配
雪花算法与号段模式没有绝对的优劣,只有是否适合业务场景,我们通过核心维度的对比,明确两种方案的适用边界,帮助开发者做出最适合的选型决策。
5.1 核心维度对比
| 对比维度 | 雪花算法 | 号段模式 |
| 全局唯一性 | 机器ID不重复的前提下100%唯一 | 基于数据库乐观锁,100%唯一 |
| 有序性 | 单节点绝对递增,跨节点趋势递增 | 单实例趋势递增,多实例整体趋势递增 |
| 性能 | 单节点最高409.6万QPS,纯内存操作,无任何IO | 本地内存操作QPS百万级,仅号段耗尽时访问数据库 |
| 高可用 | 无中心化架构,节点完全独立,只要有一个节点可用就能提供服务 | 依赖数据库高可用架构,本地缓冲可支撑短时数据库不可用 |
| 时钟依赖 | 强依赖系统时钟,时钟回拨会导致ID重复 | 无时钟依赖,完全不受时钟回拨影响 |
| 外部依赖 | 仅需机器ID分配,无强外部依赖 | 强依赖数据库,需要配套的数据库高可用架构 |
| ID连续性 | 单节点连续,跨节点不连续 | 单实例连续,多实例不连续 |
| 业务隔离性 | 无原生业务隔离能力,需额外实现 | 原生支持业务隔离,不同业务独立配置 |
| 安全性 | ID不易被遍历,无业务数据泄露风险 | ID连续,易被遍历,存在业务数据泄露风险 |
| 运维成本 | 极低,仅需管理机器ID分配 | 较高,需要维护数据库高可用集群 |
| 部署成本 | 极低,可集成到业务服务中,无需独立部署 | 通常需要独立部署ID生成服务,配套数据库集群 |
5.2 场景化选型指南
场景1:中小规模微服务架构,服务节点数少于100个
推荐选型:雪花算法中小规模微服务架构下,服务节点数不多,机器ID的管理成本极低,雪花算法可以直接集成到业务服务中,无需独立部署服务,无外部依赖,运维成本极低,性能完全满足业务需求。
场景2:大规模分布式系统,超大规模集群,分库分表场景
推荐选型:号段模式大规模分布式系统中,服务节点数众多,机器ID的管理成本极高,雪花算法的时钟回拨、机器ID重复的风险被放大;号段模式无时钟依赖,业务隔离性好,支持超大规模集群部署,完美适配分库分表场景,是互联网大厂的首选方案。
场景3:对ID长度有严格要求,需要适配long类型存储
推荐选型:雪花算法雪花算法生成的ID是标准的64位long类型,长度固定,完美适配所有支持long类型的存储系统;号段模式若要提升安全性,需要增加随机位或加密,会导致ID长度超出64位,无法适配long类型存储。
场景4:对可用性要求极高,不允许出现服务不可用
推荐选型:号段模式号段模式基于数据库的持久化存储,配合主从集群、多机房部署,可以实现99.999%的可用性;雪花算法在出现大规模时钟回拨时,可能会出现服务不可用,可用性保障能力弱于号段模式。
场景5:容器化、Serverless部署,节点动态扩缩容
推荐选型:号段模式容器化、Serverless场景下,节点会动态扩缩容,IP、实例标识频繁变化,雪花算法的机器ID分配难度极大,极易出现机器ID重复;号段模式无节点标识的依赖,完美适配动态扩缩容的场景。
六、生产环境落地最佳实践
无论是雪花算法还是号段模式,生产环境落地时,都需要遵循行业内经过验证的最佳实践,避免踩坑,保证服务的稳定运行。
6.1 雪花算法落地最佳实践
- 机器ID动态分配:采用Nacos、Zookeeper等注册中心实现机器ID的自动分配与回收,绝对禁止手动配置机器ID,避免出现配置错误导致的ID重复;
- 时钟回拨多层防护:代码层面实现等待、告警、异常抛出的多层防护,运维层面关闭节点的自动时间同步,采用统一的时钟服务器,最小化时钟回拨的概率;
- 纪元时间定制:将纪元起始时间设置为系统上线时间,最大化ID的可用生命周期,绝对禁止使用1970年作为纪元时间;
- 位数按需调整:根据业务的集群规模、并发需求,灵活调整机器ID与序列号的位数,平衡节点数量与单节点并发能力;
- ID加密混淆:对外暴露的ID需要进行加密混淆处理,避免ID被反解、遍历,保护业务数据安全;
- 监控告警:对时钟回拨、序列号溢出、机器ID冲突等异常事件配置监控告警,及时发现问题,避免故障扩大。
6.2 号段模式落地最佳实践
- 双号段预加载必选:生产环境必须采用双号段缓冲+异步预加载的方案,绝对禁止使用单缓冲号段,避免业务请求阻塞;
- 业务严格隔离:一个业务线一个biz_tag,独立配置步长,绝对禁止多个业务共用同一个biz_tag;
- 步长动态调整:根据业务的QPS动态调整步长,平衡数据库访问频率与ID浪费,步长最小不低于10分钟的峰值消耗量;
- 数据库高可用:采用MySQL一主多从、MGR集群等高可用架构,跨机房部署,保证数据库的可用性,绝对禁止单库部署;
- 乐观锁重试机制:配置合理的重试次数与退避策略,解决多实例并发申请的乐观锁冲突问题;
- 全链路监控:对号段使用率、数据库更新频率、ID生成耗时、乐观锁冲突次数等核心指标配置监控告警,及时发现性能瓶颈与异常问题;
- 安全防护:对外暴露的ID必须进行加密处理,避免ID连续导致的业务数据泄露。
七、总结
分布式ID是分布式系统的基础核心组件,看似简单的ID生成,背后却隐藏着大量的底层逻辑与生产环境的坑点。本文深度拆解了雪花算法与号段模式的底层原理,提供了可直接落地的实现方案,同时梳理了生产环境中绝大多数开发者都会踩的坑点与解决方案。
对于开发者而言,分布式ID的选型没有绝对的最优解,只有最适合业务场景的方案。我们需要从业务的规模、并发需求、可用性要求、运维成本等多个维度出发,选择最适合的方案,同时严格遵循生产环境的最佳实践,才能保证分布式ID服务的稳定、高性能、高可用运行。