分布式系统中,全局唯一且大致递增的ID有助于分库分表路由、链路追踪和排序。不依赖Snowflake等现成框架,我们也可以用时间戳与序列号的组合来模拟一个简单可用的分布式ID生成器。其核心思路是:以某个基准时间算毫秒差,将其放在高位;用节点编号区分不同机器;用自增序列应对同一毫秒内的并发请求。

一、ID结构设计与位分配
一个64位长整型足够容纳大部分业务所需的ID空间。常见的练习分配为:最高1位符号位固定为0,接着41位存放相对于自定义纪元的时间戳(约可用69年),再10位存放机器ID(支持1024个节点),最后12位存放序列号(每毫秒4096个)。这种布局兼顾了时间有序与节点隔离。
为什么不直接用System.currentTimeMillis()拼接随机数?因为随机部分在并发下仍可能重复,且ID无序会导致数据库页分裂。位运算组合能保证同节点同毫秒内严格递增,跨节点因机器位不同也不会冲突。下面用表格列出字段含义:
| 字段 | 位数 | 说明 |
|---|---|---|
| 符号位 | 1 | 恒为0,保证正数 |
| 时间戳 | 41 | 当前毫秒减去基准毫秒 |
| 机器ID | 10 | 部署时静态配置或取IP末段 |
| 序列号 | 12 | 同毫秒内AtomicLong自增 |
二、Java版组合逻辑实现
在Java里可用AtomicLong保存序列,用位或和左移完成拼接。注意序列达到上限要等到下一毫秒再清零,避免溢出到机器位。以下代码演示单节点生成逻辑,机器ID写死为1,实际可改为启动参数。
代码中对时间戳回拨做了简单处理:若当前时间小于上一次时间,直接抛异常,生产环境可改为短暂睡眠等待。这样练习能体会分布式时钟不一致时的边界情况。
public class SimpleIdGenerator {
private static final long EPOCH = 1700000000000L; // 自定义基准时间
private static final long MACHINE_ID = 1L;
private static final long MACHINE_BITS = 10L;
private static final long SEQ_BITS = 12L;
private static final long MAX_SEQ = (1L << SEQ_BITS) - 1; // 4095
private static final long MACHINE_SHIFT = SEQ_BITS;
private static final long TIME_SHIFT = SEQ_BITS + MACHINE_BITS;
private long lastTime = -1L;
private long sequence = 0L;
public synchronized long nextId() {
long now = System.currentTimeMillis();
if (now < lastTime) {
throw new RuntimeException("时钟回拨,拒绝生成");
}
if (now == lastTime) {
sequence = (sequence + 1) & MAX_SEQ;
if (sequence == 0) { // 当前毫秒序列用尽,自旋等下一毫秒
while ((now = System.currentTimeMillis()) == lastTime) {}
}
} else {
sequence = 0L;
}
lastTime = now;
long timeDiff = now - EPOCH;
return (timeDiff << TIME_SHIFT) | (MACHINE_ID << MACHINE_SHIFT) | sequence;
}
public static void main(String[] args) {
SimpleIdGenerator gen = new SimpleIdGenerator();
for (int i = 0; i < 5; i++) {
System.out.println(gen.nextId());
}
}
}
三、Python版练习代码
Python无原生长整型限制,用threading.Lock代替synchronized即可。下面示例把机器ID通过环境变量读入,序列用普通整数配合锁自增,逻辑与Java版一致,适合在脚本服务中做轻量ID发放。
需要留意的是Python的time.time()返回秒级浮点,应乘1000并转int取毫秒;多进程部署时各进程需分配不同机器ID,否则会出现交叉重复。
import time
import threading
import os
EPOCH = 1700000000000
MACHINE_ID = int(os.environ.get('MACHINE_ID', 1))
SEQ_BITS = 12
MACHINE_BITS = 10
MAX_SEQ = (1 << SEQ_BITS) - 1
TIME_SHIFT = SEQ_BITS + MACHINE_BITS
MACHINE_SHIFT = SEQ_BITS
lock = threading.Lock()
last_time = -1
sequence = 0
def next_id():
global last_time, sequence
with lock:
now = int(time.time() * 1000)
if now < last_time:
raise RuntimeError('时钟回拨')
if now == last_time:
sequence = (sequence + 1) & MAX_SEQ
if sequence == 0:
while (now := int(time.time() * 1000)) == last_time:
pass
else:
sequence = 0
last_time = now
time_diff = now - EPOCH
return (time_diff << TIME_SHIFT) | (MACHINE_ID << MACHINE_SHIFT) | sequence
if __name__ == '__main__':
for _ in range(5):
print(next_id())
四、优劣与练习要点
这种时间戳加序列的方案优点在于零依赖、易读懂、ID趋势递增。缺点是机器ID需人工规划,时钟回拨处理简单粗暴,且41位时间约69年后会耗尽。练习时应重点理解掩码(位与)如何约束序列循环,以及左移位数如何由其他字段宽度推导。
进一步可尝试把机器ID改为从ZooKeeper或配置文件动态分配,或引入时钟回拨时的备用机器位。只要掌握移位与掩码,就能依业务自由调整各段位数,比如缩短时间位、加长序列位来适应更高并发的单体应用。
distributed_idtimestamp_sequenceid_generator修改时间:2026-08-11 05:48:29