SpringBoot实现高并发分布式ID生成的完整方案

 更新时间:2026年10月01日 06:52:38   作者:(farerboy)  
最近做订单系统,ID 生成这事儿看着简单,真要整好,坑不少,尤其是高并发场景,一个破 UUID 都能把数据库索引干趴下,所以打算把雪花算法从原理到落地捋一遍,顺便聊聊 Spring Boot 里怎么封装、怎么处理时钟回拨、怎么压榨性能,需要的朋友可以参考下

最近做订单系统,ID 生成这事儿看着简单,真要整好,坑不少。尤其是高并发场景,一个破 UUID 都能把数据库索引干趴下。所以打算把雪花算法从原理到落地捋一遍,顺便聊聊 Spring Boot 里怎么封装、怎么处理时钟回拨、怎么压榨性能。这篇文章是我实际写代码的经验总结,不是那种教科书式的贴代码,想到哪儿写到哪儿,但技术点都是真刀真 枪的。

1. 业务场景与方案选择

需要全局唯一 ID 的地方太多了,订单号、消息 ID、链路追踪的 TraceId,还有日志 ID、支付流水号。这些场景要求高并发、全局唯一、最好趋势递增,而且生成过程不能成为瓶颈。

常见的生成方案:

  • UUID:最大的问题是无序、太长。字符串乱序,数据库索引性能差,排序也难受。优点是本地生成,没网络开销。
  • 数据库自增 ID:数字有序,但依赖数据库,分布式环境下有单点问题,并发一高就是瓶颈。
  • 号段模式:每次从数据库拿一批号,性能不错,但依赖数据库,重启可能丢号段。
  • 雪花算法:不依赖外部组件,趋势递增,性能极高,缺点也明显——时钟回拨会产生重复 ID,还需要维护 workerId。

综合来看,雪花算法是主流,但得针对时钟回拨做增强。下面从原理开始。

2. 雪花算法结构解析

雪花算法最早是 Twitter 搞出来的,核心思想很简洁:把一个 64 位整数按位拆开,分别表示时间戳、机器 ID 和序列号。

 0  1 - 41        42 - 51        52 - 63
 ┌─┬─────────────┬─────────────┬─────────────┐
 │0│  时间戳(ms)  │ 机器ID(10bit)│ 序列号(12bit)│
 └─┴─────────────┴─────────────┴─────────────┘
  • 符号位:永远是 0,保证 ID 是正数。
  • 时间戳:41 位毫秒时间戳。2^41 毫秒差不多 69 年,实际用的时候要设一个起始时间,比如从 2024-01-01 开始,这样可以用到 2093 年。
  • 机器 ID:10 位,可以拆成 5 位机房 + 5 位机器,支持 32 个机房,每个机房 32 台机器,总共 1024 个节点。
  • 序列号:12 位,同一毫秒内最多生成 4096 个 ID。序列号用完了就等下一毫秒。

这个设计的好处很明显:时间戳 + 机器 ID + 序列号组合起来,只要时钟不回拨、机器 ID 不冲突,全局唯一;整体趋势递增,因为毫秒递增,同一毫秒内序列号递增;纯内存运算,没网络开销。

3. Spring Boot 实现一个雪花生成器

先建一个基础版,把位运算核心逻辑写出来。

public class SnowflakeIdGenerator {

    // 起始时间戳:2024-01-01 00:00:00
    private static final long START_TIMESTAMP = 1704067200000L;

    // 各部分位数
    private static final long SEQUENCE_BITS = 12L;
    private static final long WORKER_ID_BITS = 10L;

    // 最大值
    private static final long SEQUENCE_MASK = ~(-1L << SEQUENCE_BITS);
    private static final long WORKER_ID_MASK = ~(-1L << WORKER_ID_BITS);

    // 位移量
    private static final long WORKER_ID_SHIFT = SEQUENCE_BITS;
    private static final long TIMESTAMP_SHIFT = SEQUENCE_BITS + WORKER_ID_BITS;

    private final long workerId;
    private long lastTimestamp = -1L;
    private long sequence = 0L;

    public SnowflakeIdGenerator(long workerId) {
        if (workerId < 0 || workerId > WORKER_ID_MASK) {
            throw new IllegalArgumentException("workerId must be between 0 and " + WORKER_ID_MASK);
        }
        this.workerId = workerId;
    }

    public synchronized long nextId() {
        long currentTimestamp = System.currentTimeMillis();

        if (currentTimestamp < lastTimestamp) {
            // 先留空,后面处理时钟回拨
            throw new IllegalStateException("Clock moved backwards");
        }

        if (currentTimestamp == lastTimestamp) {
            sequence = (sequence + 1) & SEQUENCE_MASK;
            if (sequence == 0) {
                currentTimestamp = waitNextMillis(currentTimestamp);
            }
        } else {
            sequence = 0L;
        }

        lastTimestamp = currentTimestamp;

        return ((currentTimestamp - START_TIMESTAMP) << TIMESTAMP_SHIFT)
                | (workerId << WORKER_ID_SHIFT)
                | sequence;
    }

    private long waitNextMillis(long currentTimestamp) {
        while (currentTimestamp <= lastTimestamp) {
            currentTimestamp = System.currentTimeMillis();
        }
        return currentTimestamp;
    }
}

配置里加数据中心和机器 ID。我习惯把 10 位拆成 5 位机房 + 5 位机器,最终 workerId = (dataCenterId << 5) | machineId。

@Component
@ConfigurationProperties(prefix = "snowflake")
public class SnowflakeProperties {
    private long dataCenterId = 1;
    private long machineId = 1;

    // getter/setter 省略

    public long getWorkerId() {
        return (dataCenterId << 5) | machineId;
    }
}

配置类里生成 Bean:

@Configuration
public class IdGeneratorConfig {

    @Bean
    public SnowflakeIdGenerator snowflakeIdGenerator(SnowflakeProperties properties) {
        return new SnowflakeIdGenerator(properties.getWorkerId());
    }
}

这样就能直接 @Autowired 使用了。但还没处理时钟回拨,接下来是关键。

4. 时钟回拨问题处理

雪花算法最怕系统时间往回跳,一回到过去,同一毫秒内就可能生成重复 ID。业界常见的招数有三招:等待、抛错、历史时间补偿。

4.1 等待

如果回拨时间不长(比如 5 秒内),就让线程睡一会儿,等系统时间追上来。

if (currentTimestamp < lastTimestamp) {
    long offset = lastTimestamp - currentTimestamp;
    if (offset <= 5000) {
        try {
            Thread.sleep(offset * 2);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
        currentTimestamp = System.currentTimeMillis();
        while (currentTimestamp < lastTimestamp) {
            currentTimestamp = System.currentTimeMillis();
        }
    } else {
        throw new IllegalStateException("Clock moved backwards too much");
    }
}

思路很直白,但回拨频繁的话,线程会被反复阻塞,请求堆积。

4.2 直接抛错

回拨超过阈值,直接抛异常,让上层决定是降级还是拒绝。

if (currentTimestamp < lastTimestamp) {
    long offset = lastTimestamp - currentTimestamp;
    if (offset > 100) {
        throw new IllegalStateException("Clock moved backwards. Refusing to generate ID for " + offset + " ms");
    }
    // 小回拨,继续处理
}

这种策略容易导致服务不可用,但能保证不生成重复 ID。

4.3 历史时间补偿

这个思路比较巧妙。既然当前时钟回拨了,那就假装时间还在上次生成 ID 的时候,序列号继续往上加,直到加满 4096 个为止。这样避免等待,也不抛错。

public synchronized long nextId() {
    long currentTimestamp = System.currentTimeMillis();

    if (currentTimestamp < lastTimestamp) {
        sequence = (sequence + 1) & SEQUENCE_MASK;
        if (sequence == 0) {
            throw new IllegalStateException("Sequence exhausted while clock moved backwards");
        }
        // 继续用 lastTimestamp,序列号递增
        return ((lastTimestamp - START_TIMESTAMP) << TIMESTAMP_SHIFT)
                | (workerId << WORKER_ID_SHIFT)
                | sequence;
    }

    // 正常流程...
}

但注意,这个方案只能补偿短时间回拨。如果回拨太久,序列号用尽就只能抛错了。所以生产上我更喜欢“等待 + 补偿”的组合:

public synchronized long nextId() {
    long currentTimestamp = System.currentTimeMillis();
    long offset = lastTimestamp - currentTimestamp;

    if (offset > 0) {
        if (offset > 5000) {
            throw new IllegalStateException("Clock moved backwards, offset=" + offset + "ms");
        }
        try {
            Thread.sleep(offset + 1);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
        currentTimestamp = System.currentTimeMillis();
    }

    if (currentTimestamp == lastTimestamp) {
        sequence = (sequence + 1) & SEQUENCE_MASK;
        if (sequence == 0) {
            currentTimestamp = waitNextMillis(currentTimestamp);
        }
    } else {
        sequence = 0L;
    }

    lastTimestamp = currentTimestamp;
    return ((currentTimestamp - START_TIMESTAMP) << TIMESTAMP_SHIFT)
            | (workerId << WORKER_ID_SHIFT)
            | sequence;
}

另外,如果你们有 Redis 或者 ZooKeeper,可以把最近生成 ID 的时间戳存一份,本机时间小于全局最大时间戳时,直接拿全局时间戳用。不过这会引入网络开销,我一般不用。

5. 预生成 ID 池与双缓冲

雪花算法本身延迟已经很低了,但 System.currentTimeMillis() 是有系统调用开销的,再加上 synchronized 锁,高并发下还是会被拖死。有一个土办法:先把 ID 批量生成好放在本地队列里,用的时候直接从队列取,一步到位。

public class IdPool {
    private final SnowflakeIdGenerator generator;
    private final BlockingQueue<Long> queue = new LinkedBlockingQueue<>(10000);
    private final ExecutorService executor = Executors.newSingleThreadExecutor();

    public IdPool(SnowflakeIdGenerator generator) {
        this.generator = generator;
        fill();
    }

    private void fill() {
        for (int i = 0; i < 10000; i++) {
            queue.offer(generator.nextId());
        }
    }

    public Long nextId() throws InterruptedException {
        if (queue.size() < 5000) {
            executor.submit(this::fill);  // 异步补充,注意防重
        }
        return queue.take();
    }
}

生产环境别这么裸,至少加个 AtomicBoolean 防止重复提交填充任务。也可以用双缓冲:两个队列,一个消费一个后台填充,消费到一半就交换。

public class DoubleBufferIdPool {
    private final SnowflakeIdGenerator generator;
    private final int bufferSize;
    private volatile Queue<Long> currentBuffer;
    private Queue<Long> nextBuffer;
    private final ExecutorService executor = Executors.newSingleThreadExecutor();

    public DoubleBufferIdPool(SnowflakeIdGenerator generator, int bufferSize) {
        this.generator = generator;
        this.bufferSize = bufferSize;
        this.currentBuffer = new ArrayDeque<>();
        this.nextBuffer = new ArrayDeque<>();
        fillBuffer(currentBuffer);
    }

    private void fillBuffer(Queue<Long> buffer) {
        for (int i = 0; i < bufferSize; i++) {
            buffer.offer(generator.nextId());
        }
    }

    public Long nextId() {
        Long id = currentBuffer.poll();
        if (id == null) {
            synchronized (this) {
                if (currentBuffer.isEmpty()) {
                    Queue<Long> tmp = currentBuffer;
                    currentBuffer = nextBuffer;
                    nextBuffer = tmp;
                    fillBuffer(nextBuffer);
                }
                id = currentBuffer.poll();
            }
        } else {
            if (currentBuffer.size() <= bufferSize / 2 && nextBuffer.size() < bufferSize) {
                executor.submit(() -> fillBuffer(nextBuffer));
            }
        }
        return id;
    }
}

预生成会浪费一些 ID,但相比性能提升,这点浪费值得。另外,批量接口可以一次返回多个 ID,进一步减少调用次数。

6. 集成 MyBatis-Plus

MyBatis-Plus 默认的 ASSIGN_ID 就是雪花算法,但我们可以换成自己的生成器,做到统一管理。只需要实现 IdentifierGenerator 接口。

@Component
public class MyBatisPlusIdGenerator implements IdentifierGenerator {

    @Autowired
    private IdPool idPool; // 或者直接用 SnowflakeIdGenerator

    @Override
    public Number nextId(Object entity) {
        return idPool.nextId();
    }
}

实体类主键用 @TableId(type = IdType.ASSIGN_ID) 就行。如果想生成字符串订单号,可以单独写个服务:

@Service
public class OrderIdService {
    @Autowired
    private SnowflakeIdGenerator generator;

    public String generateOrderId() {
        return "ORDER" + new SimpleDateFormat("yyyyMMdd").format(new Date())
                + generator.nextId();
    }
}

7. 美团 Leaf 的设计思路

美团 Leaf 是业界很经典的方案,两种模式都值得学习。

号段模式:从数据库拿一个号段(比如 1~1000),进程内按顺序分配。数据库表里有 biz_tag、max_id、step 这些字段。当号段快用完时,后台异步去数据库加载下一个号段。优点是对数据库压力小,ID 趋势递增;缺点是要维护号段表,数据库挂了也玩不转。

雪花模式:Leaf 做雪花模式的时候解决了两个痛点。一个是 workerId 动态分配,通过 ZooKeeper 注册节点,启动时获取递增序号作为 workerId,关闭时释放。另一个是时钟回拨,Leaf 有 checkTimestamp 机制,检测到回拨就抛异常,然后告警人工处理。

参考 Leaf,我们在生产环境可以考虑用 Redis 动态分配 workerId,但大多数团队规模用配置文件就行,只要不搞混。

8. 压测与监控

我拿 8C16G 的机器压了一次(JMeter 100 线程,60 秒),基础雪花算法 QPS 大概 3.5 万,平均响应 2.8ms,P99 8.1ms。加了 ID 池双缓冲之后,QPS 能到 12 万,平均响应降到 0.8ms,P99 也只有 2.3ms。差距很明显。

生产环境建议重点监控这几个指标:

  • 生成耗时:平均耗时、TP99。
  • 池水位:剩余 ID 数量,低于阈值就告警。
  • 时钟回拨次数:记下每次回拨的时间差和频率。
  • QPS:评估容量和扩缩容。

用 Micrometer + Prometheus + Grafana 就能搞定,代码里埋点记录一下 Timer 和 Counter 就行。

9. 总结

雪花算法不是什么银弹,但掌握了原理和变体,大部分场景都能应对。我的建议是:

  • 起始时间戳设得远一点,保证 ID 用几十年。
  • workerId 别重复,运维要统一登记。
  • 开启 NTP 同步,加 -x 参数避免时间跳变。
  • 日志里记下时钟回拨详情,出了问题好排查。
  • ID 生成服务要做限流降级,别让上游突起压垮。

以上就是SpringBoot实现高并发分布式ID生成的完整方案的详细内容,更多关于SpringBoot高并发分布式ID生成的资料请关注脚本之家其它相关文章!

相关文章

  • IDEA创建SpringBoot的maven项目的方法步骤

    IDEA创建SpringBoot的maven项目的方法步骤

    这篇文章主要介绍了IDEA创建SpringBoot的maven项目的方法步骤,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2021-04-04
  • 详解Spring Boot 添加JSP支持

    详解Spring Boot 添加JSP支持

    本篇文章主要介绍了详解Spring Boot 添加JSP支持,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2017-05-05
  • java中@ModelAttribute注解的作用

    java中@ModelAttribute注解的作用

    本文主要介绍了java中@ModelAttribute注解的作用。具有很好的参考价值,下面跟着小编一起来看下吧
    2017-02-02
  • springboot自动装配TypeNotPresentExceptionProxy异常排查解决

    springboot自动装配TypeNotPresentExceptionProxy异常排查解决

    这篇文章主要为大家介绍了springboot自动装配TypeNotPresentExceptionProxy异常排查解决,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2023-09-09
  • Java中containsKey 、 contains 方法详解及代码举例详解

    Java中containsKey 、 contains 方法详解及代码举例详解

    在Java中,containsKey和contains方法常用于集合操作,但它们的应用场景和功能有所不同,本文详细介绍这两种方法的用法及区别,并提供丰富的代码实例,感兴趣的朋友一起看看吧
    2025-09-09
  • SpringCloud集成zookeeper实现服务注册并访问功能

    SpringCloud集成zookeeper实现服务注册并访问功能

    zookeeper和eureka一样,是用于充当服务注册功能服务器的一个springcloud插件,这篇文章主要介绍了SpringCloud集成zookeeper实现服务注册并访问,需要的朋友可以参考下
    2022-06-06
  • Spring的构造器注入全过程

    Spring的构造器注入全过程

    这篇文章主要介绍了Spring的构造器注入全过程,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教
    2024-09-09
  • SpringBoot 过滤器与拦截器实例演示

    SpringBoot 过滤器与拦截器实例演示

    本文通过示例代码给大家讲解SpringBoot 过滤器与拦截器的相关知识,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友参考下吧
    2021-11-11
  • Java注解示例详解(含底层原理)

    Java注解示例详解(含底层原理)

    注解在现代Java开发中扮演着重要角色,广泛应用于框架开发、代码生成和运行时功能扩展,下面这篇文章主要介绍了Java注解底层原理的相关资料,文中通过代码介绍的非常详细,需要的朋友可以参考下
    2025-10-10
  • SpringAop源码及调用过程概述

    SpringAop源码及调用过程概述

    这篇文章主要介绍了SpringAop源码及调用过程概述,Spring AOP(面向切面编程)是Spring框架的一个重要特性,它提供了一种在程序运行期间动态地将额外的行为织入到代码中的方式,需要的朋友可以参考下
    2023-10-10

最新评论