单纯用System.currentTimeMillis()生成全局唯一ID不可靠,因其毫秒级精度在高并发下易重复;可靠方案是将其作为核心基础,组合毫秒内序列号和机器ID构成结构化ID。

单纯用 System.currentTimeMillis() 生成全局唯一 ID 是不可靠的——它只精确到毫秒,高并发下同一毫秒内多次调用必然重复。但它是构建高质量业务流水号的**核心基础**,关键在于“怎么用”,而不是“能不能用”。
为什么时间戳必须搭配其他因子
Java 中 System.currentTimeMillis() 返回的是自 1970-01-01 00:00:00 UTC 起的毫秒数,13 位数字,天然带时间序和可读性。但它有硬伤:
- 单 JVM 内,多线程同时执行会拿到相同值
- 跨 JVM 或多实例时,完全无法区分来源
- 毫秒级精度在现代服务中已显不足(QPS 过万就极易碰撞)
所以必须补充“毫秒内序号”和“机器/进程标识”,把时间戳从“可能重复”变成“结构化锚点”。
推荐组合结构:13位时间戳 + 3–5位序列号 + 2–4位机器ID
这是兼顾唯一性、长度可控(通常 ≤ 20 位)、便于日志排查和数据库索引的实用结构:
-
时间戳部分:用
System.currentTimeMillis() - START_EPOCH做偏移(如减去 2024-01-01 的毫秒值),压缩为 13 位以内,避免过长 -
序列号部分:用
AtomicInteger在每毫秒内计数,上限设为 999(3 位)或 99999(5 位),溢出时等待下一毫秒或丢弃重试 -
机器标识部分:可取本机 IP 后两位哈希、Docker 容器 ID 截取、或配置固定字符串(如
"01"),2–4 位足够区分同机房常见部署规模
示例输出:182456789012304201(13 位时间 + 3 位序号 + 2 位机器号)
不建议的两种常见错误用法
以下做法看似简单,实则埋坑:
-
直接拼接 UUID:
System.currentTimeMillis() + UUID.randomUUID().toString()→ 结果含横线、字母、长度超 36 字符,难检索、占索引、前端展示不友好 -
裸用时间戳转字符串:
Long.toString(System.currentTimeMillis())→ 毫秒级重复率在微服务调用链中极高,订单、支付等场景极易出错
若需引入 UUID 的随机性,建议仅取其 leastSignificantBits 转 Base32 或紧凑数字串(12–14 位),作为“扰动因子”嵌入后缀,而非整个 UUID。
轻量单机可用代码示意
无需外部依赖,适合非严格跨集群但要求高内聚的场景:
public class BusinessSeqGenerator {
private static final long START_EPOCH = 1704067200000L; // 2024-01-01
private static final AtomicInteger seqInMs = new AtomicInteger(0);
private static final int MAX_SEQ = 999;
private static final String MACHINE_ID = "01";
<pre class='brush:java;toolbar:false;'>public static String generate() {
long currentMs = System.currentTimeMillis() - START_EPOCH;
int seq = seqInMs.incrementAndGet() % (MAX_SEQ + 1);
if (seq == 0) {
try { Thread.sleep(1); } catch (InterruptedException e) { }
return generate();
}
return String.format("%013d%03d%s", currentMs, seq, MACHINE_ID);
}}
跨 JVM 场景只需将 MACHINE_ID 替换为真实实例标识(如 Spring Cloud 的 spring.application.instance-id 或 Consul 注册 ID)即可升级为近似全局唯一。

















