尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
百度UidGenerator核心原理与SpringBoot集成实战
聊到分布式ID很多团队的第一反应就是自己撸一个雪花算法变体。早期我也这么干过但后来发现真正写起来容易后面要处理时钟回拨、workerId分配、机器上线下线、高并发下的性能瓶颈全是坑。百度UidGenerator是业界最早一批把雪花算法做成可配置、带缓存预生成、带workerId自动分配的开源方案在SpringBoot2.X下集成顺手不用自己造轮子也避开了大部分常见坑。这篇主要聊它的原理、参数设计和完整集成步骤适合正在做基础组件选型、或者已经踩过自研雪花算法坑的开发同学直接照着搭一套自己的分布式ID服务。1. 为什么折腾一圈又选回百度UidGenerator1.1 分布式ID方案现状盘点在聊UidGenerator之前先把市面上常见的方案摊开看一看。UUID最简单但生成出来的ID没有业务含义长度大在MySQL里做InnoDB主键还会因为随机性引发页分裂性能其实是负优化。数据库自增ID简单可靠但一旦分库分表多实例各自维护自增位置要用步长或者号段才能做还需要额外的分配表维护后续扩容时要重新调整步长麻烦。Redis自增解决了一部分问题但Redis一挂整套ID生成就跟着挂而且要想在Redis里生成高效ID还得用Lua脚本维护成本也不低。真正在互联网业务里用得多的还是雪花算法这一族。它的核心思想很简单用一个long型64位整数分成时间戳段、机器ID段、序列号段各段按位组合拼出一个趋势递增、全局唯一的ID。本地生成、无网络依赖、性能高这是它的最大优点。但原生雪花算法暴露的问题也很集中机器ID得手工分配扩容时运维要记录谁用了哪个ID时间被回拨时生成的ID可能重复而且很多实现把位数写死工作节点多或者单节点并发高的时候表现并不是很好。百度UidGenerator本质上就是针对这几个问题做了一轮工程化改造。1.2 百度UidGenerator到底省了什么百度UidGenerator提供了两种实现一个是DefaultUidGenerator基于标准雪花算法的位运算方案另一个是CachedUidGenerator它用RingBuffer缓存预生成的ID批量取号、异步填充极大降低了对时间的实时依赖也大幅提高了并发吞吐。它把时间戳、workerId、序列号三段位数都做成可配置适配不同规模的业务workerId可以通过数据库表自动分配节点启动后自动注册一个记录取ID不用人肉去维护IP到workerId的映射。对于大多数中小团队来说这几个点正好就是自研时最头疼的部分。我见过太多团队从GitHub拉一个雪花算法工具类往项目里一放第一版跑得很开心半年之后节点扩容运维在Excel里手工分配workerId某次部署漏了一台机器两个节点用了同一个ID半夜日志里出现重复主键排查了半天。UidGenerator的数据库自动分配机制解决的正是这个问题这也是我后来坚持用它的一个很重要原因。1.3 技术选型边界何时适合它不是所有场景都适合UidGenerator。如果你只有单库单表数据库自增主键就够用如果系统刚起步并发要求不高也能接受性能问题UUID也无妨。但如果你的系统已经走向微服务化多个服务多个实例需要一个全局唯一的、趋势递增的、可用性高的顺序ID而且不想引入额外中间件那UidGenerator就是性价比很高的选择成本就是一张worker node表和一个Jar包依赖。另外需要注意它是用数据库表做workerId分配的如果没有任何数据库环境这套方案就不是最优解不如直接上Redis或者让各节点用静态配置。2. 核心原理与参数拆解2.1 位运算生成器一段看懂的ID结构先看DefaultUidGenerator。它把64位long型ID拆成三段除去最高位符号位剩下63位由三个配置共同组成timeBits、workerBits、seqBits。三个值的总和必须是63这是整道题的约束前提。假设我配置timeBits29、workerBits21、seqBits13合成后ID的二进制大致长这样最高位0占一位接下来29位是相对某个固定起始时间的毫秒偏移量再接下来21位是workerId最后13位是同一个毫秒内的自增序列号。这样拆完29位的时间戳能表示2的29次方毫秒约536,870,912毫秒折算约17.1年。21位的workerId能支撑2,097,152个节点对绝大多数公司来说绰绰有余。13位序列号意味着单个节点单毫秒能生成8192个ID换算成每秒是820万的量级配合非缓存模式也已经远超高并发业务的日常需求。计算一下shift就知道为什么它灵活workerIdShift等于seqBitstimestampShift等于workerBits加seqBits。生成的时候先取相对时间戳左移timestampShift位再与workerId左移workerIdShift后的值按位或最后把序列号按位或进低位。使用起来很简单long id uidGenerator.nextId();这种模式适合业务量稳定、并发不算特别极端的场景最大的优点是生成速度极快没有锁竞争完全本机运算。2.2 缓存生成器RingBuffer是性能关键CachedUidGenerator才是精华。它的实现思路是不再每一次调用都现场计算一个ID而是利用一个环形数组把未来一段时间内会使用到的ID预先生成好塞进缓冲区调用方直接从缓冲区里取取完一个后台或者下一次取号时发现剩余量不足再批量补一批进去。这个环形数组内部用了tail和cursor两个指针tail指向当前写位置cursor指向当前消费位置。初始化时它会根据当前配置和并发参数算出需要的槽位大小比如容纳未来若干秒产生的ID再向上对齐到2的幂避免取模运算的性能损耗。每次消费一个IDcursor就往前走一格当剩余可用槽位跌到一个阈值它就以更大的一批数量重新填充这个扩容的动作由boostPower控制。假设boostPower3一次扩容会准备8倍的基础槽位数减少填充频率。paddingFactor则是那个触发阈值默认是50表示缓冲区剩余不足50%就开始补充。还有scheduleInterval这个定时器配置单位是秒。它是个兜底机制就算没有任何消费动作触发补货定时器也会周期性检查并主动填充防止某个时段突然大流量打过来时缓冲区见底。这背后还带来了一个隐藏收益因为RingBuffer里装的都是预生成好的未来ID时间戳都已经固定了所以对微小时刻的时钟回拨有天然的容忍能力回拨范围没超过预生成窗口取到的还是缓冲区里那批不重复的ID不会立刻爆出重复主键。2.3 WorkerId自动分配与数据库表的角色UidGenerator里的workerId不是靠配置文件写死的。它内置了一个WorkerIdAssigner接口官方提供的实现是依靠一张WORKER_NODE表完成自动注册。每台应用节点启动时把当前主机的hostName、port、节点类型、启动日期等信息插入WORKER_NODE表表里的自增主键ID就作为该节点的workerId。因为主键全局递增只要节点注册时不冲突拿到的workerId自然全局唯一。这张表很轻量跟业务表完全隔离一般单独放一个库也完全没问题。它的存在保证了三个比较重要的场景一是新节点扩容不需要人工分配workerId启动即自动获取二是节点重启如果host和port都相同可以复用原有的workerId记录避免每次重启都在表里插一条导致workerId不断膨胀三是多机部署时只要hostName加port能区分开就不会有重复分配的风险。需要注意的是如果整个系统没有数据库连接这套分配机制就无从谈起。所以用它的前提是应用本身已经有依赖数据库的基础设施。3. SpringBoot2.X集成实操3.1 Maven与数据库准备我已经基于SpringBoot2.5版本实践过整套流程先把依赖准备好。UidGenerator本身没有发布到中央仓库的所有版本不同版本情况不一稳妥做法是把源码clone下来自己构建然后安装到本地Maven仓库mvn clean install -DskipTests如果你的工程能直接从中央仓库拉到对应坐标就省去这一步。否则在项目pom里引入本地仓库产物dependency groupIdcom.baidu.fsg/groupId artifactIduid-generator/artifactId version1.0.0/version /dependency dependency groupIdorg.mybatis.spring.boot/groupId artifactIdmybatis-spring-boot-starter/artifactId version2.1.4/version /dependency数据库准备很简单前提是有workernode这张表CREATE TABLE WORKER_NODE ( ID BIGINT NOT NULL AUTO_INCREMENT, HOST_NAME VARCHAR(64) NOT NULL, PORT VARCHAR(64) NOT NULL, TYPE INT NOT NULL, LAUNCH_DATE DATE NOT NULL, MODIFIED TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, CREATED TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (ID) ) ENGINEINNODB COMMENTWorkerId分配节点表;字段就是官方那套ID自增主键即workerIdHOST_NAME和PORT用于唯一标识一个节点LAUNCH_DATE记录启动日期MODIFIED和CREATED是审计时间。这张表不需要任何外键不需要额外索引保持极简。3.2 实体类与Mapper映射先定义WorkerNode实体字段跟表一一对应Java侧偏好Long、String、Integer、Date的类型组合public class WorkerNode { private Long id; private String hostName; private String port; private Integer type; private Date launchDate; private Date modified; private Date created; // getter/setter省略 }Mapper接口直接用MyBatis注解方式节省XML文件的路径配置。查询一个节点用hostName加port定位插入节点需要返回自增主键所以用Options(useGeneratedKeystrue)Mapper public interface WorkerNodeMapper { Select(SELECT ID, HOST_NAME, PORT, TYPE, LAUNCH_DATE, MODIFIED, CREATED FROM WORKER_NODE WHERE HOST_NAME #{hostName} AND PORT #{port}) WorkerNode selectByHostAndPort(Param(hostName) String hostName, Param(port) String port); Options(useGeneratedKeys true, keyProperty id) Insert(INSERT INTO WORKER_NODE(HOST_NAME, PORT, TYPE, LAUNCH_DATE, MODIFIED, CREATED) VALUES(#{hostName}, #{port}, #{type}, #{launchDate}, NOW(), NOW())) int insert(WorkerNode workerNode); }扫描这个Mapper注意SpringBoot启动类上要有MapperScan或者直接在接口上标注Mapper。实际集成时我习惯统一放到一个独立的dao包路径下比如com.example.demo.dao用MapperScan(“com.example.demo.dao”)扫进去避免和业务Mapper混在一起。3.3 核心配置类与Bean注入接下来最关键的是把生成器装配成Spring Bean。我建议不用官方那个过于内部的默认装配方式而是自己实现一个WorkerIdAssigner逻辑完全可控。先实现自定义分配器Component public class DatabaseWorkerIdAssigner implements WorkerIdAssigner { Resource private WorkerNodeMapper workerNodeMapper; Value(${server.port:8080}) private String port; Override public long assignWorkerId() { String hostName getHostName(); WorkerNode workerNode workerNodeMapper.selectByHostAndPort(hostName, port); if (workerNode ! null) { return workerNode.getId(); } WorkerNode insertNode new WorkerNode(); insertNode.setHostName(hostName); insertNode.setPort(port); insertNode.setType(1); insertNode.setLaunchDate(new Date()); workerNodeMapper.insert(insertNode); return insertNode.getId(); } private String getHostName() { try { return InetAddress.getLocalHost().getHostAddress(); } catch (Exception e) { return System.getenv(HOSTNAME) null ? unknown : System.getenv(HOSTNAME); } } }注意一点同一个host和port在表里已经存在记录时直接复用原来那条的ID不要重新插入。这个逻辑是保证重启环境变量不变时workerId稳定的关键。我遇到的不少重复ID问题恰恰是有些人把这一步写成了无条件插入。然后装配生成器这里要绑定配置参数。SpringBoot里可以用ConfigurationProperties简化ConfigurationProperties(prefix uid) public class UidProperties { private int timeBits; private int workerBits; private int seqBits; private String epochStr; private Cache cache new Cache(); public static class Cache { private int boostPower 3; private int paddingFactor 50; private int scheduleInterval 60; // getter/setter省略 } // getter/setter省略 }装配BeanConfiguration EnableConfigurationProperties(UidProperties.class) public class UidConfig { Bean public UidGenerator uidGenerator(DatabaseWorkerIdAssigner assigner, UidProperties properties) { CachedUidGenerator generator new CachedUidGenerator(); generator.setWorkerIdAssigner(assigner); generator.setTimeBits(properties.getTimeBits()); generator.setWorkerBits(properties.getWorkerBits()); generator.setSeqBits(properties.getSeqBits()); generator.setEpochStr(properties.getEpochStr()); generator.setBoostPower(properties.getCache().getBoostPower()); generator.setPaddingFactor(properties.getCache().getPaddingFactor()); generator.setScheduleInterval(properties.getCache().getScheduleInterval()); return generator; } }这里我用了CachedUidGenerator而不是DefaultUidGenerator。原因很简单它的性能上限更高还带缓冲兜底对时钟回拨的容忍度更好。Blog里如果业务并发量真的小到只有每秒几十个其实DefaultUidGenerator也够用但要切换也简单代码里把实现类换成DefaultUidGenerator即可其余配置一样。3.4 application.yml参数与常见配置参数配置我一般这样写uid: time-bits: 29 worker-bits: 21 seq-bits: 13 epoch-str: 2024-01-01 cache: boost-power: 3 padding-factor: 50 schedule-interval: 60三个位数加起来是63这个是硬约束。epochStr字段是整个算法的时间起点格式是yyyy-MM-dd建议选一个接近系统上线的时间。这样时间戳位的偏移量会小可用时间窗口更长。如果你选得太早比如1990年那29位毫秒数可能在某个时刻就溢出了ID会直接跳到负数这是我在实际项目里踩过一次的教训。有人会问为什么workerBits要配21其实完全取决于你的节点规模。如果只有几十台机器配置10到12位足够如果未来可能会有几万个容器实例那就给高一点。但要注意workerBits越大timeBits或者seqBits就得压缩因为总和固定是63。我这里是按中型团队设计的支撑209万个节点、单节点每毫秒8192个ID、整体可用17年。实际业务基本都能套进去。3.5 业务侧调用与验证在业务服务里注入UidGenerator调用nextId就行Service public class OrderIdService { Resource private UidGenerator uidGenerator; public Long nextOrderId() { return uidGenerator.nextId(); } }启动SpringBoot应用后第一次调用会自动完成初始化并生成ID。我建议启动完成后写一个简单的自测接口把生成的ID打出来人工看一眼ID的长度和趋势。一个合法的UidGenerator生成的ID是19位左右的长整数而且连续生成多次高位会随时间推移缓慢递增。如果想做初步压测可以在单元测试或者临时接口里跑一轮百万次生成long start System.currentTimeMillis(); for (int i 0; i 1_000_000; i) { uidGenerator.nextId(); } long cost System.currentTimeMillis() - start; System.out.println(cost: cost ms, qps: (1_000_000L * 1000 / cost));以我普通开发机的实测结果CachedUidGenerator跑百万ID基本在几十毫秒级别整体吞吐轻松过百万QPS。这并不是我故意夸大而是缓存预生成模式下取一个ID就是从数组里拿一个元素再移动一下指针瓶颈根本不在算法本身。在正式环境里真正需要关注的是初始化阶段预生成耗时以及数据库分配workerId的短连接开销。4. 调优经验与避坑清单4.1 参数如何根据业务测算先把三个核心参数的约束背下来timeBits workerBits seqBits 63。在这个前提下每个参数都对应一个业务上限。timeBits决定可用年限。计算方式是2的timeBits次方毫秒数除以86,400,000毫秒得到天数再除以365得到年数。比如29位就是约17.1年30位是约34.2年。如果业务注定活不过十年配29就够如果要更长时间配30到31但其他位要相应扣减。workerBits决定节点上限。2的21次方是2,097,152如果只算3年内的扩展规模可能2的15次方即32768就足够省下来的位数给seqBits或timeBits。seqBits决定单节点并发上限。sequence位数是n那么单毫秒内可以生成2的n次方个ID每秒就是2的n次方乘1000。13位是每毫秒8192个真实跑起来已经非常夸张。如果单机每秒只有几千的请求量给10位甚至8位都够了。实际调优案例曾经有个项目预计未来5年内扩展到200台实例、单实例峰值TPS大约3万那么workerBits选8位足够支撑256节点seqBits选10位支撑每毫秒1024个即每秒约100万此时timeBits可以拿45位可用年限直接拉满到一万多年都没有溢出的忧虑。但因为总和限制是63位4581063完美。这个案例说明不是越大越好而是各位需要动态平衡。4.2 我的踩坑记录5个真实问题问题一启动直接报“Failed to configure a DataSource”。这是因为DatabasWorkerIdAssigner初始化需要数据库而应用的数据源配置有问题。排查思路先确认数据源是否正常再看Mapper扫描是否生效。问题二首次启动生成ID正常重启后workerId变化。这是因为无条件插入逻辑导致每次重启都新建记录新的自增ID顶上来了。复用workerId会让节点重启后的ID整体保持稳定。问题三某次大促后ID出现负数。原因是epochStr配得太早timeBits不够用时间戳部分溢出到符号位。复盘下来上线时不应该随手配一个远古日期而应该按上面公式反向计算出一个合理的epochStr。问题四CachedUidGenerator首次调用耗时很长。首次调用会完成RingBuffer初始化并预生成一批ID如果机器性能差这个耗时可能达到几百毫秒。解决方式是在应用启动阶段主动预热一次把uidGenerator.nextId()在启动后立即调用一次比如在ApplicationRunner里执行一个热身循环避免第一个业务请求承担初始化开销。问题五多个服务共用同一个库存表导致workerId重复。如果一个库表被多套环境、多个独立生成器共用它们的自增主键混在一起分配逻辑会被打乱。必须保证一个WORKER_NODE表只服务一套UidGenerator集群。4.3 测试与上线注意事项上线之前至少要过一遍三个检查。第一个检查是参数总和三个bits加起来必须是63不然运行期会直接抛异常。第二个检查是workerId上限从数据库拿到的自增ID必须小于2的workerBits次方否则位运算会直接吞掉高位两个节点可能生成相同ID。如果你部署的节点数不多这个问题概率很低但如果你未来真的想用几千个实例就一定要配置足够大的workerBits并且在上线环节加上启动检查long maxWorkerId ~(-1L workerBits); if (workerId maxWorkerId) { throw new IllegalStateException(workerId 超出配置上限); }第三个检查是时钟同步。CachedUidGenerator能容忍轻微回拨但你不能指望业务服务器时钟偏差很大还能绝对安全。分布式系统的标准姿势就是NTP时间同步这一点无论用哪套ID生成器都逃不掉。我在运维侧会把时钟同步加入所有业务节点的初始化脚本后面就再没遇到过因为时钟回拨导致的重复ID。还有一个小细节如果你要让生成器做到高可用不要只部署单节点。UidGenerator自带数据库分配workerId天然适合多节点部署常态应该是两台以上同时提供服务单台故障时流量打到另一台上workerId不同ID自然也不会冲突。这比单机自建UUID要高一个Level。写在最后这套方案我前前后后在好几个SpringBoot2.X项目里落地过最大的体会是不要被“百度UidGenerator”这个名字唬住它本质就是一个把雪花算法做得更工程化的组件引入成本很低、收益却很直观。最让我省心的就是workerId自动分配再也不用在扩容的时候拿着表格分配ID新机器启动就直接拿到自己的身份几乎零运维。如果再让我选一次小规模新项目我依然会先上CachedUidGenerator而不是又去写一个雪花工具类。真到哪天并发量高到RBUFFER预生成都不够用了再换其他专门的高性能方案也不迟但到那个阶段你已经把分布式ID这条路趟得明明白白了。
RELATED

相关推荐

SpringBoot配置加密实战:Jasypt 3.0.5 + SpringBoot 2.7.x安全集成指南

SpringBoot配置加密实战:Jasypt 3.0.5 + SpringBoot 2.7.x安全集成指南

1. 项目概述:为什么SpringBoot项目必须加密yml配置文件在某公司实际交付的三个中型微服务项目里,我连续遇到过两次因配置文件泄露导致的紧急事故:一次是测试环境数据库密码被爬虫抓取后批量爆破,另一次是开发人员误将含Redis密码的…

📅 2026/10/16 14:34:49
手把手教你|投票制作怎么做:数据导出 + 防刷票,年会 / 比赛评选该选哪个(附对照表)

手把手教你|投票制作怎么做:数据导出 + 防刷票,年会 / 比赛评选该选哪个(附对照表)

后台问得最多的三个问题是:投票页面到底怎么做出来?票数能不能导出给自己做二次统计?刷票怎么防得住?其实投票制作就三步——先定规则、再选工具、最后导数据收尾。多数人卡在第二步:工具挑错了,视频传不上…

📅 2026/10/16 14:29:46
Java+AI全栈开发工程师:核心能力、技术栈与实战指南

Java+AI全栈开发工程师:核心能力、技术栈与实战指南

1. 这个岗位到底在招什么人“JavaAI 全栈开发工程师”这个岗位名称,这两年在招聘市场上出现的频率越来越高。我翻了不少岗位描述,也跟几个正在招这类人的团队负责人聊过,发现大家对这个角色的期待其实相当一致:你既要能扛住传统Ja…

📅 2026/10/16 14:29:46
MORE NEWS

更多资讯

📰

考研复试资料高效使用指南:从学长经验到历年真题的拆解方法

简介:这份资料包面向备战东华大学计算机专业考研复试的考生,汇集历年复试上机真题、学长学姐上岸经验与多渠道复习资源,帮助解决复试环节信息不对称、复习方向不明确的问题。内容围绕上机编程测试、面试技巧、心态调整与时间规划展开&#xf…

📰

从零搭建AI日报自动化系统:信息聚合、去重聚类与摘要生成实战

1. 当"日报"变成"日更":一个内容聚合项目的起点做内容的人都有一个共同的痛点:信息太多,时间太少。每天打开各种信息流,光是筛选"今天到底发生了什么值得关注的事"就要花掉一两个小时,等…

📰

局域网聊天课设:为什么用服务器中转比真P2P更合适

简介:本资源是一份面向计算机网络课程设计实践的完整技术文档,适用于高校软件工程、网络工程等专业本科生开展P2P局域网通信项目开发。文档围绕“基于P2P架构的局域网聊天程序”展开,系统覆盖需求分析、总体与详细设计、模块划分(…

📰

新代eHMI下载实战:从固件级部署到产线批量回滚

简介:本资源为台湾新代(Syntec)数控系统配套的客制化人机界面开发软件eHMI官方版本V3.27.0,面向CNC设备二次开发工程师、自动化系统集成商及工业HMI应用开发者,用于快速构建定制化操作界面、实现PLC逻辑可视化与设备参…

📰

基于SpringBoot与微信小程序的UGC旅游分享平台实战解析

做这个项目的起因其实很朴素——我自己去丽江玩了一圈,发现出行前到处翻攻略的体验非常痛苦。订票平台上有酒店和机票,正经的游记内容却埋在广告和种草笔记里,想要找"最近去过的人怎么说""这几天索道到底开没开"这种真实…

📰

Spring Cloud 升级篇:GateWay 网关、Config 配置中心与 Bus 总线实践指南

Spring Cloud 玩到第十章这个阶段,其实已经不是入门了。前面的注册中心、服务调用、负载均衡、熔断降级这些基础组件都通了之后,真正让人头疼的是两件事:一是所有微服务的流量入口太散,每个服务都要单独管鉴权、限流、跨域&#x…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬