导读:本期聚焦于小伙伴创作的《如何模拟实现一个简单分布式ID生成器:时间戳与序列号组合逻辑详解》,敬请观看详情。在分库分表或微服务环境下,数据库自增主键容易冲突,手工拼ID又常出现重复。一种轻量做法是将毫秒时间戳左移,再拼接本地递增序列号,单机每毫秒可发数万号且跨节点不易撞车。本文拆解这种组合算法的位运算规则:用41位存时间差、10位存机器号、12位存序列,说明时钟回拨时如何阻塞等待或抛错。配合Java与Python两段可运行代码,演示如何取当前毫秒、掩码限制序列溢出、移位叠加节点编号。你会看到无第三方依赖时,怎样用AtomicLong保序,以及高并发下序列耗尽后的自旋策略,适合作为分布式组件底层逻辑的入门练习。

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

如何模拟实现一个简单分布式ID生成器:时间戳与序列号组合逻辑详解

一、ID结构设计与位分配

一个64位长整型足够容纳大部分业务所需的ID空间。常见的练习分配为:最高1位符号位固定为0,接着41位存放相对于自定义纪元的时间戳(约可用69年),再10位存放机器ID(支持1024个节点),最后12位存放序列号(每毫秒4096个)。这种布局兼顾了时间有序与节点隔离。

为什么不直接用System.currentTimeMillis()拼接随机数?因为随机部分在并发下仍可能重复,且ID无序会导致数据库页分裂。位运算组合能保证同节点同毫秒内严格递增,跨节点因机器位不同也不会冲突。下面用表格列出字段含义:

字段位数说明
符号位1恒为0,保证正数
时间戳41当前毫秒减去基准毫秒
机器ID10部署时静态配置或取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

免责声明:​ 已尽一切努力确保本网站所含信息的准确性。网站内容多为原创整理与精心编撰,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们处理。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。