Java版雪花算法生成ID实用工具类完整实例
前言
ID的生成一般可以是序列递增、雪花算法、UUID等等
各个特点如下:
序列自增
✅ 优点:
- 占用空间小:自增ID通常使用较小的数据类型(如INT),占用存储空间较小。
- 性能好:自增ID是顺序生成的,可以连续存储在磁盘上,有利于提高插入操作的性能,减少碎片化。
- 易于排序和索引:自增ID可以快速排序和索引,有利于提高查询效率。
❌ 缺点:
- 局部唯一性:在单数据库实例中是唯一的,但在分布式系统中或在多个数据库之间使用时需要额外的机制来确保全局唯一性(例如使用UUID或其他分布式ID生成策略)。
- 需要数据库级别的唯一性检查:每次插入前需要检查是否已存在相同的ID,这会增加数据库的负担。
- 重用问题:如果在删除记录后不重新使用ID,会导致ID的浪费。虽然可以通过一些策略(如预留ID池)解决,但这增加了实现的复杂性。
雪花算法
雪花算法(Snowflake Algorithm)是由 Twitter 设计并开源的一种分布式唯一 ID 生成算法,用于在高并发、分布式环境下高效生成全局唯一、时间有序的 64 位整数 ID(Long 类型)。其核心思想是将一个 64 位二进制数划分为多个功能段,通过“时间戳 + 机器标识 + 序列号”组合生成 ID。
64 位雪花 ID 位分配图解(标准版)
0 41 51 63
┌────────────┬─────────┬───────────┐
│ timestamp │ dataCtr │ worker │ sequence
│ (41 bits) │ (5 bits)│ (5 bits) │ (12 bits)
└────────────┴─────────┴───────────┘
↑
64-bit signed long (最高位为符号位,始终为 0 → 实际可用 63 位)| 字段 | 长度 | 取值范围 | 说明 |
|---|---|---|---|
timestamp | 41 位 | 0 ~ 2⁴¹−1 ms ≈ 69.7 年 | 自 EPOCH 起的毫秒差;决定 ID 时间序和生命周期 |
datacenterId | 5 位 | 0 ~ 31 | 标识数据中心(如:北京=1、上海=2) |
workerId | 5 位 | 0 ~ 31 | 标识该中心内的具体机器(进程) |
sequence | 12 位 | 0 ~ 4095 | 同一毫秒内自增序号;支持单机峰值 4096 ID/ms |
🔍 示例 ID 解析(十进制):
1829347561234567168
→ 转二进制后截取对应字段,即可还原生成时间、机房、机器与序号。
✅ 优点
| 维度 | 说明 |
|---|---|
| 全局唯一性 | 基于时间戳(毫秒级)+ 数据中心 ID + 机器 ID + 序列号,理论上在集群规模合理、时钟不回拨前提下,ID 绝对唯一。 |
| 高性能 & 低延迟 | 完全本地内存运算,无网络/数据库依赖,单机 QPS 可达数万甚至数十万。 |
| 时间有序性(近似单调递增) | 高位为时间戳,生成的 ID 随时间大致递增,有利于数据库(如 MySQL)索引优化(减少页分裂)、范围查询等。 |
| 结构化 & 可解析 | ID 可直接解码出生成时间、所属节点等信息,便于问题追踪与运维分析。 |
| 轻量易集成 | 无外部依赖,主流语言均有成熟实现(Java/Go/Python 等),易于嵌入微服务架构。 |
❌ 缺点与局限性
| 维度 | 说明 |
|---|---|
| 强依赖系统时钟(时钟回拨问题) | 若服务器时间发生回拨(如 NTP 校正、手动修改),可能导致 ID 重复或阻塞(取决于实现策略)。需配合时钟监控、容忍策略(如等待、抛异常、降级为随机 ID)等机制缓解。 |
| ID 位数固定,存在理论上限 | 64 位中:41 位时间戳(约 69 年)、10 位机器标识(最多 1024 节点)、12 位序列号(每毫秒最多 4096 个 ID)。时间戳耗尽后需升级位分配(如扩展为 65+ 位,但破坏兼容性)。 |
| 机器 ID 需人工/中心化分配 | datacenterId 和 workerId 需全局唯一且预先配置,大规模动态扩缩容时管理成本上升(需结合 ZooKeeper、Etcd 或数据库注册中心解决)。 |
| 非密码学安全 | ID 可被预测(尤其已知部分参数和时间范围时),不适用于需要防猜测、防枚举的场景(如订单号、优惠券码等)。 |
| 单机吞吐受限于序列号位宽 | 每毫秒最多生成 2^12 = 4096 个 ID;若瞬时并发超限,会阻塞等待下一毫秒(导致少量延迟毛刺)。 |
🔍 补充说明(常见变种优化)
- 百度 UidGenerator:基于雪花改进,引入 RingBuffer 缓存 + 时间自增补偿,缓解时钟回拨与序列号瓶颈。
- 美团 Leaf(Segment 模式 / Snowflake 模式):提供两种模式,Snowflake 模式即增强版雪花,增加动态 workerId 分配与健康检测。
- Twitter 官方已弃用:Twitter 后期转向更灵活的混合方案(如基于 Kafka 的日志序号 + 时间戳),但雪花因其简洁性仍被业界广泛采用。
⚙️ ID 生成方案对比表(Snowflake vs 其他主流方案)
| 维度 | Snowflake | UUID v4 | DB 自增主键 | Redis INCR | Leaf-Segment(美团) |
|---|---|---|---|---|---|
| 唯一性 | 全局唯一(依赖配置+时钟) | 全局唯一(概率极低重复) | 单库唯一,分布式需分库分表/号段 | 单 Redis 实例唯一,集群需协调 | 全局唯一(号段预分配+双 Buffer) |
| 有序性 | ✅ 近似时间有序(利于索引) | ❌ 完全无序(字符串,B+树效率低) | ✅ 严格递增 | ✅ 严格递增 | ✅ 逻辑有序(号段内有序) |
| 性能 | ✅ 极高(纯内存,μs 级) | ✅ 高(本地生成) | ❌ 依赖 DB 写入(ms 级,有锁) | ✅ 高(但依赖 Redis RTT) | ✅ 高(号段缓存,批量获取) |
| 可用性 | ✅ 无外部依赖 | ✅ 无依赖 | ❌ DB 故障即不可用 | ❌ Redis 故障即不可用 | ⚠️ 依赖 MySQL(号段管理)或 ZooKeeper |
| ID 长度/类型 | 64 位整数(紧凑、易排序) | 128 位字符串(32 字符,存储/索引开销大) | 64 位整数 | 64 位整数 | 64 位整数 |
| 可读性/可解析性 | ✅ 可解析出时间、节点信息 | ❌ 完全随机,无业务含义 | ❌ 仅序号,无时间/节点信息 | ❌ 仅序号 | ⚠️ 可解析时间,但节点信息弱化 |
| 扩展性 | ⚠️ 机器 ID 需预分配,动态扩容较重 | ✅ 天然无状态,无限扩展 | ❌ 分库分表复杂,全局有序难保证 | ⚠️ Redis 集群需路由一致性 | ✅ 支持动态扩容(号段自动分配) |
| 适用场景 | 推荐:订单 ID、消息 ID、日志 traceId 等高并发有序需求 | 适合:临时 token、文件名、非索引场景 | 适合:单体应用、低并发后台系统 | 适合:简单计数、限流等非核心 ID | 推荐:大型电商/金融系统(兼顾性能、容灾、扩展) |
💡 选型建议:
- 初创/中小系统 → 优先 Snowflake(简单可靠);
- 对可用性要求极高 → Leaf-Segment(MySQL 故障仍可发号数小时);
- 禁止 ID 泄露业务规模 → 避免 Snowflake/自增,改用加密 UUID 或混淆 ID(如 Base62 编码 + 盐值哈希)。
雪花算法和UUID的核心区别在于:雪花算法生成的是具有时间有序性的64位整数,而UUID生成的是通常无序的128位标识符(常以字符串形式表示),这导致了它们在数据库性能、存储效率以及适用场景上的显著差异。
实用工具类如下:
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
/**
* 雪花ID生成器
*
*/
public class SnowflakeIdGenerator {
// ==================== 常量定义 ====================
private static final long EPOCH = 1609459200000L; // 自定义起始时间:2021-01-01 00:00:00 UTC(毫秒)
private static final int DATA_CENTER_ID_BITS = 5;
private static final int WORKER_ID_BITS = 5;
private static final int SEQUENCE_BITS = 12;
private static final long MAX_DATA_CENTER_ID = ~(-1L << DATA_CENTER_ID_BITS); // 31
private static final long MAX_WORKER_ID = ~(-1L << WORKER_ID_BITS); // 31
private static final long MAX_SEQUENCE = ~(-1L << SEQUENCE_BITS); // 4095
private static final long WORKER_ID_SHIFT = SEQUENCE_BITS; // 12
private static final long DATA_CENTER_ID_SHIFT = SEQUENCE_BITS + WORKER_ID_BITS; // 17
private static final long TIMESTAMP_LEFT_SHIFT = SEQUENCE_BITS + WORKER_ID_BITS + DATA_CENTER_ID_BITS; // 22
// ==================== 运行时变量 ====================
private final long dataCenterId;
private final long workerId;
private long sequence = 0L;
private long lastTimestamp = -1L;
// ==================== 构造函数 ====================
public SnowflakeIdGenerator(long dataCenterId, long workerId) {
if (dataCenterId < 0 || dataCenterId > MAX_DATA_CENTER_ID) {
throw new IllegalArgumentException("dataCenterId must be in [0," + MAX_DATA_CENTER_ID + "]");
}
if (workerId < 0 || workerId > MAX_WORKER_ID) {
throw new IllegalArgumentException("workerId must be in [0," + MAX_WORKER_ID + "]");
}
this.dataCenterId = dataCenterId;
this.workerId = workerId;
}
// ==================== 核心方法 ====================
public synchronized long nextId() {
long timestamp = currentTimeMillis();
// ⚠️ 时钟回拨处理:严格模式(抛异常) or 容忍模式(等待/告警)
if (timestamp < lastTimestamp) {
throw new RuntimeException(
String.format("Clock moved backwards. Refusing to generate id for %d milliseconds",
lastTimestamp - timestamp));
}
if (lastTimestamp == timestamp) {
// 同一毫秒内,序列号自增
sequence = (sequence + 1) & MAX_SEQUENCE;
if (sequence == 0) {
// 当前毫秒序列已满,阻塞至下一毫秒
timestamp = tilNextMillis(lastTimestamp);
}
} else {
// 新毫秒,序列号重置为 0
sequence = 0L;
}
lastTimestamp = timestamp;
// 拼接 ID:(timestamp - epoch) << 22 | dataCenterId << 17 | workerId << 12 | sequence
return ((timestamp - EPOCH) << TIMESTAMP_LEFT_SHIFT)
| (dataCenterId << DATA_CENTER_ID_SHIFT)
| (workerId << WORKER_ID_SHIFT)
| sequence;
}
private long tilNextMillis(long lastTimestamp) {
long timestamp = currentTimeMillis();
while (timestamp <= lastTimestamp) {
timestamp = currentTimeMillis();
}
return timestamp;
}
private long currentTimeMillis() {
return System.currentTimeMillis();
}
// ==================== 辅助方法:解析 ID(可选) ====================
public static class IdInfo {
public final long timestamp; // 时间戳(毫秒)
public final long dataCenterId; // 数据中心 ID
public final long workerId; // 工作节点 ID
public final long sequence; // 序列号
public IdInfo(long timestamp, long dataCenterId, long workerId, long sequence) {
this.timestamp = timestamp;
this.dataCenterId = dataCenterId;
this.workerId = workerId;
this.sequence = sequence;
}
public IdInfo(long id) {
this.timestamp = (id >> TIMESTAMP_LEFT_SHIFT) + EPOCH;
this.dataCenterId = (id >> DATA_CENTER_ID_SHIFT) & MAX_DATA_CENTER_ID;
this.workerId = (id >> WORKER_ID_SHIFT) & MAX_WORKER_ID;
this.sequence = id & MAX_SEQUENCE;
}
}
public static void main(String[] args) {
SnowflakeIdGenerator idGenerator = new SnowflakeIdGenerator(1, 2);
List<Long> ids = new ArrayList<>();
for (int i = 0; i < 10; i++) {
long id = idGenerator.nextId();
ids.add(id);
System.out.println(id);
}
long id = ids.get(8);
IdInfo info = new IdInfo(id);
System.out.println(info);
//SnowflakeIdGenerator.IdInfo info = SnowflakeIdGenerator.parse(id);
System.out.println("时间戳: " + new Date(info.timestamp));
System.out.println("数据中心 ID: " + info.dataCenterId);
System.out.println("工作节点 ID: " + info.workerId);
System.out.println("序列号: " + info.sequence);
}
/**
* 解析 Snowflake ID
*
* @param id Snowflake ID
* @return 包含时间戳、数据中心 ID、工作节点 ID 和序列号的结构体
*/
public static IdInfo parse(long id) {
long timestamp = (id >> TIMESTAMP_LEFT_SHIFT) + EPOCH;
long dataCenterId = (id >> DATA_CENTER_ID_SHIFT) & MAX_DATA_CENTER_ID;
long workerId = (id >> WORKER_ID_SHIFT) & MAX_WORKER_ID;
long sequence = id & MAX_SEQUENCE;
return new IdInfo(timestamp, dataCenterId, workerId, sequence);
}
}UUID递增方式
优点:
- 全局唯一性:UUID确保在任何情况下都能生成唯一的标识符,这对于分布式系统尤为重要。
- 无需数据库级别的唯一性检查:插入数据时不需要检查其他记录,减少了数据库的负担。
- 易于迁移:在数据库架构迁移或数据迁移时,UUID的唯一性保证了数据的完整性。
缺点:
- 占用空间大:UUID通常以128位表示,转换为字符串后长度较长(36个字符,包括4个破折号),占用更多的存储空间。
- 性能问题:在大量插入操作时,UUID的随机性和无序性可能导致数据库性能下降,因为它们不遵循顺序插入,可能会引起索引分裂和碎片化。
- 不适合作为主键进行排序:由于UUID的随机性,基于UUID的排序效率较低。
核心结构与生成原理
- 数据结构与表示形式:雪花算法生成的ID是一个64位的长整型数字,其二进制结构通常包含时间戳、机器标识(或数据中心ID)和序列号等部分。而UUID是一个128位的全局唯一标识符,通常以32个十六进制数字组成的字符串形式呈现(例如
550e8400-e29b-41d4-a716-446655440000)。 - 生成机制:雪花算法的唯一性依赖于系统时间戳、预设的机器/数据中心编号以及同一毫秒内的序列号的组合,这使其生成过程需要一定的配置(如分配节点ID)。UUID的生成算法则更为多样(有多个版本),其原理可能基于MAC地址、时间戳、随机数或命名空间等,生成过程简单,通常无需中心化协调或复杂配置。
性能与适用场景差异
- 有序性与数据库性能:这是两者最关键的差异。雪花算法生成的ID基本按时间趋势递增,具有时间有序性。当这种有序ID作为数据库主键时,新记录会顺序插入B+树索引的末尾,大大提升了写入性能并减少了页分裂。相反,传统的UUID(如UUIDv4)是完全无序的,作为主键插入会导致随机写入,可能严重拖慢数据库操作。值得注意的是,较新的UUIDv7版本引入了时间戳,也具备了时间有序性。
- 存储与传输效率:雪花算法的64位整数在数据库中通常用
BIGINT(8字节)存储,空间占用小,索引结构更紧凑。UUID若以字符串形式(如CHAR(36))存储,则需占用更多空间;即便使用二进制格式(BINARY(16),16字节),其长度仍是雪花算法的两倍,在存储和网络传输中开销更大。 - 分布式部署简易性:UUID无需任何节点标识配置,在任何地方均可独立生成,天生适合高度解耦的分布式架构。雪花算法在分布式部署时,需要为每个生成节点分配唯一的工作ID(WorkId),这引入了额外的管理复杂度。
- 信息可解析性与安全性:雪花算法ID的组成部分(如时间戳、机器ID)可以被反解出来,便于问题排查和业务分析,但也可能泄露部署信息。传统UUID(如基于随机数的v4)内容不可解析,不包含敏感信息。
考量与实践建议
- 根据系统架构与需求选择:
- 优先选择雪花算法的场景:需要高性能数据库写入与查询、ID按时间排序(如评论、订单时序展示)、以及对存储空间有严格要求的分布式系统。
- 优先选择UUID的场景:系统架构简单、或对生成简便性和部署灵活性要求极高、且对数据库索引性能不敏感的场景。若需要有序性但不想管理节点ID,可考虑UUIDv7。
- 注意潜在风险与优化:
- 雪花算法强依赖系统时钟。若发生时钟回拨(如NTP校准),可能导致ID重复。实践中需通过记录最大ID、暂停服务或使用逻辑时钟等手段防范。
- 使用UUID作为数据库主键时,建议采用 BINARY(16) 存储以减少空间占用,并评估其对写入性能的实际影响。
总结
到此这篇关于Java版雪花算法生成ID实用工具类的文章就介绍到这了,更多相关Java雪花算法生成ID工具类内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!
相关文章
SpringBoot LocalDateTime格式转换方案详解(前端入参)
这篇文章主要介绍了SpringBoot LocalDateTime格式转换(前端入参),本文用示例介绍SpringBoot全局格式配置,将前端传过来的时间自动转化为LocalDateTime,需要的朋友可以参考下2023-04-04
Java ObjectMapper的使用和使用过程中遇到的问题
在Java开发中,ObjectMapper是Jackson库的核心类,用于将Java对象序列化为JSON字符串,或者将JSON字符串反序列化为Java对象,这篇文章主要介绍了Java ObjectMapper的使用和使用过程中遇到的问题,需要的朋友可以参考下2024-07-07
Java基础MAC系统下IDEA连接MYSQL数据库JDBC过程
最近一直在学习web项目,当然也会涉及与数据库的连接这块,这里就总结一下在IDEA中如何进行MySQL数据库的连接,这里提一下我的电脑是MAC系统,使用的编码软件是IDEA,数据库是MySQL2021-09-09
使用TraceId在Spring Cloud中实现线上问题快速定位
在微服务架构中,服务间的互相调用使得问题定位变得复杂,在此背景下,TraceId为我们提供了一个在复杂环境中追踪请求路径和定位问题的工具,本文不仅介绍TraceId的基本概念,还将结合真实场景,为您展示如何在Spring Cloud中应用它2023-09-09
浅谈@RequestBody和@RequestParam可以同时使用
这篇文章主要介绍了@RequestBody和@RequestParam可以同时使用,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教2022-03-03


最新评论