高并发、分布式场景下的ID生成策略目录[[#为什么需要设计ID生成|为什么需要设计ID生成]][[#UUID|UUID]][[#雪花算法 Snowflake|雪花算法 Snowflake]][[#数据库自增ID|数据库自增ID]][[#Redis原子计数|Redis原子计数]][[#号段模式 Segment|号段模式 Segment]][[#Leaf 分布式ID生成系统|Leaf 分布式ID生成系统]][[#方案对比与选择建议|方案对比与选择建议]]为什么需要设计ID生成在很多项目刚开始的时候生成 ID 似乎是一件非常简单的事情。数据库里的AUTO_INCREMENT往往就能满足需求开发者也很少会专门去思考这个问题。但当系统逐渐发展访问量越来越大甚至开始拆分成多个服务、部署在多台机器上时一个看似简单的问题就会变得复杂起来在高并发、分布式的环境下如何生成一个既唯一又高效的 ID如果处理不好就可能出现 ID 冲突、数据库性能瓶颈甚至影响整个系统的稳定性。因此在实际的后端开发中ID 生成往往会被单独设计成一套机制。从最简单的数据库自增到使用 Redis 计数器再到像 Snowflake 这样的分布式算法不同的方案都有各自的适用场景。UUIDUUIDUniversally Unique Identifier是一个128位的全局唯一标识符通常以32个十六进制数字表示分为5组例如550e8400-e29b-41d4-a716-446655440000。Java中可以使用UUID.randomUUID().toString()生成版本4随机生成。优点本地生成完全在本地生成无需网络请求或数据库查询全局唯一理论上重复概率极低适用于分布式环境简单易用API简单无需额外配置缺点无序性生成的ID完全随机作为数据库主键时会导致B树索引频繁分裂严重影响写入性能长度大128位16字节存储空间较大作为索引效率较低无业务含义ID本身不携带任何时间、机器等业务信息适用场景生成临时令牌Token、会话IDSession ID文件上传时的临时文件名不入库的临时标识符分布式追踪IDTrace ID注意虽然UUID可以解决分布式唯一性问题但由于其无序性和长度问题通常不建议作为数据库主键使用。在高并发写入场景下性能影响显著。雪花算法 (Snowflake)雪花算法是 Twitter 开源的分布式 ID 生成算法生成的 ID 是一个 64 位的长整数具有趋势递增、全局唯一的特点适合在高并发分布式环境中使用。算法原理雪花算法生成的 64 位 ID 由以下几部分组成位数含义说明1符号位保证为正数固定为041时间戳毫秒级时间戳从自定义 epoch 开始计算10机器ID可配置为机房ID 机器ID唯一标识节点12序列号同一毫秒内的自增序列0-4095时间戳41位保证 ID 随时间递增最多可使用约 69 年2^41/1000/60/60/24/365机器ID10位最多支持 1024 个节点保证分布式环境下不冲突序列号12位同一毫秒内最多生成 4096 个 IDID 生成步骤获取当前时间戳毫秒检查时钟回拨如果当前时间小于上次生成时间抛出异常或等待生成序列号如果当前时间戳与上次相同序列号 1超过最大值则等待下一毫秒如果不同序列号重置为 0组合各部分(时间戳 22) | (机器ID 12) | 序列号伪代码实现/** * 雪花算法 ID 生成器 */ public class SnowflakeIdGenerator { private final long epoch 1609459200000L; // 2021-01-01 00:00:00 private final long workerIdBits 10L; private final long sequenceBits 12L; private final long maxWorkerId ~(-1L workerIdBits); // 1023 private final long maxSequence ~(-1L sequenceBits); // 4095 private final long workerIdShift sequenceBits; private final long timestampShift sequenceBits workerIdBits; private long lastTimestamp -1L; private long sequence 0L; public synchronized long nextId() { long timestamp currentTimeMillis(); // 检查时钟回拨 if (timestamp lastTimestamp) { throw new RuntimeException(时钟回拨异常拒绝生成ID); } if (timestamp lastTimestamp) { sequence (sequence 1) maxSequence; if (sequence 0) { // 同一毫秒内序列号用尽等待下一毫秒 timestamp waitNextMillis(lastTimestamp); } } else { sequence 0L; } lastTimestamp timestamp; return ((timestamp - epoch) timestampShift) | (workerId workerIdShift) | sequence; } private long waitNextMillis(long lastTimestamp) { long timestamp currentTimeMillis(); while (timestamp lastTimestamp) { timestamp currentTimeMillis(); } return timestamp; } }优缺点分析优点高性能完全在本地内存计算无网络开销高并发单机单毫秒可生成 4096 个 ID分布式下性能线性扩展趋势递增ID 随时间递增适合作为数据库索引可解析ID 可反解析出生成时间、机器ID等信息缺点时钟回拨问题服务器时间回退会导致 ID 重复解决方案等待时间追平适合小范围回拨使用备用时间源如 NTP 服务器记录最近时间戳拒绝服务直到时间恢复使用 Leaf-Snowflake 等改进方案机器ID分配需要手动或通过协调服务分配机器ID分配方案配置文件手动指定使用 ZooKeeper/Etcd 动态分配基于数据库分配和持久化性能指标单机 QPS理论最大 409.6 万/秒实际受限于系统时钟精度支持节点数最多 1024 个节点可用年限约 69 年从自定义 epoch 开始数据库自增ID数据库自增ID是最简单的ID生成方式通过在表中设置AUTO_INCREMENT字段由数据库自动维护ID的递增。单机场景在单机MySQL中使用AUTO_INCREMENT非常简单CREATE TABLE orders ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, data VARCHAR(255), PRIMARY KEY (id) ) ENGINEInnoDB;工作原理MySQL通过自增锁保证并发安全同一时刻只有一个事务能获取自增值自增ID保证连续递增在事务回滚时会有空洞性能较高但存在单点瓶颈分布式场景的问题在分布式数据库分库分表环境中直接使用AUTO_INCREMENT会导致ID冲突每个分片独立自增会产生重复ID无法保证全局唯一性和有序性分布式解决方案存根表通过一个独立的存根表Stub Table集中分配ID解决分布式ID冲突问题。表结构设计CREATE TABLE sequence_id ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, stub CHAR(1) NOT NULL DEFAULT , PRIMARY KEY (id), UNIQUE KEY stub (stub) ) ENGINEInnoDB COMMENTID分配存根表;stub业务标识如 ‘order’、‘user’唯一索引保证每个业务独立序列id当前最大ID自增字段ID分配流程插入或更新存根表获取新ID-- 原子操作如果stub存在则id1不存在则插入 INSERT INTO sequence_id (stub) VALUES (order) ON DUPLICATE KEY UPDATE id LAST_INSERT_ID(id 1);使用获取的ID插入业务表INSERT INTO orders (id, data) VALUES (LAST_INSERT_ID(), 订单数据);关键机制LAST_INSERT_ID()LAST_INSERT_ID()是MySQL的会话级函数返回当前连接最后生成的自增ID即使在高并发下每个连接都能正确获取自己生成的ID不会冲突事务安全连接断开后失效优缺点分析优点简单可靠利用数据库原生能力实现简单绝对有序ID严格连续递增高可用数据库具备主从复制、故障恢复能力缺点性能瓶颈每次生成ID都需要数据库交互QPS受限单点故障存根表数据库成为单点扩展性差无法线性扩展性能受数据库限制优化方案批量获取一次获取多个ID在应用层缓存类似号段模式多存根表按业务拆分多个存根表分散压力数据库集群使用分布式数据库或读写分离适用场景中小型系统ID生成频率不高 1000 QPS对数据库依赖较强的传统架构。Redis原子计数Redis 作为高性能内存数据库提供原子递增命令可以用于实现分布式环境下的全局ID生成。核心命令# 基本递增返回递增后的值 INCR id:counter # 指定步长递增 INCRBY id:counter 100 # 设置初始值如果key不存在 SET id:counter 1000 NX实现方案1. 简单计数器publicclassRedisIdGenerator{privateJedisjedis;publicLongnextId(StringbizType){Stringkeyid:counter:bizType;returnjedis.incr(key);}}2. 带时间戳的ID结合时间戳保证ID趋势递增publicLongnextId(StringbizType){StringdateLocalDate.now().format(DateTimeFormatter.BASIC_ISO_DATE);// yyyyMMddStringkeyid:counter:bizType:date;Longseqjedis.incr(key);// 组合日期 序列号如 202603160001returnLong.parseLong(dateString.format(%04d,seq));}优点1. 原子性保证Redis单线程执行命令INCR操作天然原子性无需额外锁机制高并发下不会出现竞态条件2. 高性能内存操作响应时间通常在毫秒级单节点QPS可达10万3. 灵活性支持按业务类型、日期等维度拆分计数器可设置过期时间自动清理历史数据缺点与挑战1. 中心化瓶颈所有ID生成请求集中到Redis单节点/集群在极高并发10万QPS下可能成为性能瓶颈2. 网络开销每次生成ID都需要网络往返增加了延迟通常1-2ms3. 可用性问题Redis故障会导致ID生成服务不可用需要部署Redis集群、持久化、备份等高可用方案4. 数据持久化计数器需要持久化避免重启后ID重复RDB/AOF持久化可能影响性能优化方案1. 本地缓存批处理应用层缓存一批ID减少Redis访问频率类似号段模式但使用Redis作为存储后端2. Redis集群分片不同业务使用不同Redis分片使用Hash Tag保证同一业务计数器在同一个分片3. 多级缓存L1: 应用本地缓存号段L2: Redis集群L3: 数据库持久化性能指标单节点QPS8-12万取决于网络和Redis配置延迟0.5-2ms同机房支持业务数理论上无限制通过key设计适用场景中小型系统QPS 5万临时ID生成会话ID、验证码等多维度ID需要按日期、业务等维度生成的ID快速原型快速实现分布式ID生成注意对于超高频ID生成场景10万QPS建议采用号段模式或雪花算法避免Redis成为瓶颈。号段模式 (Segment)号段模式Segment是一种批量预分配的ID生成方案核心思想是从数据库批量获取一段ID如1001~2000缓存在应用本地然后逐个分配。这样将数据库的每次ID申请压缩为批量申请极大减少数据库压力。数据库设计CREATE TABLE id_generator ( biz_tag VARCHAR(50) PRIMARY KEY COMMENT 业务标识, max_id BIGINT NOT NULL COMMENT 当前已分配的最大ID, step INT NOT NULL COMMENT 步长每次分配的ID数量, version BIGINT NOT NULL DEFAULT 0 COMMENT 版本号乐观锁, update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ) ENGINEInnoDB COMMENTID生成器配置表;字段说明biz_tag业务类型如’order’、‘user’唯一标识不同业务序列max_id当前已分配的最大ID下次分配从max_id1开始step步长决定每次预分配的ID数量version乐观锁版本防止并发更新冲突号段分配策略1. 固定步长根据业务量预估设置固定步长-- 高频业务订单步长10万 INSERT INTO id_generator (biz_tag, max_id, step) VALUES (order, 0, 100000); -- 低频业务用户步长1000 INSERT INTO id_generator (biz_tag, max_id, step) VALUES (user, 0, 1000);2. 动态步长智能调整根据历史消耗速率动态调整步长监控号段消耗速度自动调整步长如消耗快则增大步长避免频繁数据库访问同时减少ID浪费号段申请流程步骤1原子更新max_idUPDATE id_generator SET max_id max_id step, version version 1, update_time NOW() WHERE biz_tag order AND version #{oldVersion};并发控制使用乐观锁version字段避免更新冲突更新失败时重试或等待步骤2查询分配的号段范围SELECT max_id, step FROM id_generator WHERE biz_tag order;假设查询结果max_id 100000, step 100000则分配的号段为[100001, 200000]步骤3本地缓存与分配publicclassSegmentIdGenerator{privateAtomicLongcurrentId;// 当前分配的IDprivateLongmaxId;// 当前号段最大值publicsynchronizedLongnextId(){if(currentId.get()maxId){returncurrentId.getAndIncrement();}else{// 号段用完申请新号段allocateNewSegment();returnnextId();}}privatevoidallocateNewSegment(){// 从数据库申请新号段Segmentsegmentdb.allocateSegment(order);currentId.set(segment.getStartId());// 100001maxIdsegment.getEndId();// 200000}}号段模式流程示意图优化方案1. 双Buffer机制Leaf方案问题号段用尽时才申请新号段导致请求阻塞。解决方案Buffer A当前正在使用的号段Buffer B预备号段提前异步加载当Buffer A使用到阈值如80%时异步加载Buffer BBuffer A用尽后无缝切换到Buffer B2. 多级缓存L1应用内存缓存当前号段L2Redis缓存备用号段L3数据库持久化存储3. 监控与告警监控号段消耗速率预测号段用尽时间提前预警自动调整步长优缺点分析优点高性能99.9%的ID从内存分配响应时间1ms高可用数据库短暂不可用不影响ID生成有缓存号段可扩展水平扩展应用节点数据库压力不增加灵活配置可按业务设置不同步长缺点ID不连续号段用尽时ID会有跳跃如200000→300001数据库依赖仍需数据库持久化但压力大幅降低重启丢号应用重启时未使用的缓存ID会丢失解决方案定期持久化已分配位置或使用共享存储Redis性能指标单机QPS10万纯内存操作数据库QPS降低100-1000倍取决于步长支持节点数理论上无限制ID连续性段内连续段间不连续适用场景高频ID生成电商订单、支付流水、日志ID多业务隔离不同业务需要独立ID序列数据库压力敏感需要减少数据库访问的场景最佳实践步长设置需要权衡太小导致频繁访问数据库太大导致ID浪费和重启丢失更多ID。建议根据业务QPS设置步长为5-10分钟的消耗量。Leaf 分布式ID生成系统Leaf 是美团开源的一套分布式ID生成系统提供了Leaf-Segment和Leaf-Snowflake两种模式在实际生产环境中广泛应用。系统架构Leaf 采用RESTful API提供服务支持以下特性高可用多节点部署无单点故障高性能单机QPS可达10万可监控提供管理后台实时监控ID生成状态易扩展支持水平扩展动态调整节点Leaf-Segment核心改进在基础号段模式上Leaf-Segment 引入了以下优化1. 双Buffer机制publicclassDoubleBuffer{privateSegmentBuffercurrentBuffer;// 当前使用的BufferprivateSegmentBuffernextBuffer;// 预备BufferpublicsynchronizedLongnextId(){if(currentBuffer.hasId()){returncurrentBuffer.nextId();}if(nextBuffer!nullnextBuffer.isReady()){// 切换BuffercurrentBuffernextBuffer;nextBuffernull;// 异步加载下一个BufferloadNextBufferAsync();returncurrentBuffer.nextId();}// 两个Buffer都为空等待加载waitForBuffer();returnnextId();}privatevoidloadNextBufferAsync(){executor.submit(()-{Segmentsegmentdb.allocateSegment(bizTag);SegmentBufferbuffernewSegmentBuffer(segment);nextBufferbuffer;});}}工作流程Buffer A为当前使用Buffer当Buffer A消耗到阈值默认80%时由一个独立的线程去执行loadNextBufferAsync()Buffer A用尽后无缝切换到Buffer BBuffer A用尽后立即异步加载下一个Buffer2. 动态步长调整根据历史消耗速率自动调整步长监控最近N次号段消耗时间计算平均消耗速率ID/秒动态调整步长 平均速率 × 缓存时间如10分钟3. 监控与告警实时监控各业务号段消耗情况预测号段用尽时间提前告警可视化配置管理界面表结构设计CREATE TABLE leaf_alloc ( biz_tag VARCHAR(128) NOT NULL PRIMARY KEY COMMENT 业务标识, max_id BIGINT NOT NULL COMMENT 当前最大ID, step INT NOT NULL COMMENT 步长, description VARCHAR(256) COMMENT 业务描述, update_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ) ENGINEInnoDB;Leaf-SnowflakeLeaf-Snowflake 在原生雪花算法基础上解决了时钟回拨和机器ID分配两大痛点。1. 机器ID动态分配传统问题手动配置机器ID容易冲突扩缩容需要重新配置机器ID回收困难Leaf解决方案基于ZooKeeper的机器ID分配publicclassWorkerIdAssigner{privateZooKeeperzk;publicintassignWorkerId(){// 1. 在ZooKeeper创建临时顺序节点Stringpathzk.create(/leaf/snowflake/worker-,null,ZooDefs.Ids.OPEN_ACL_UNSAFE,CreateMode.EPHEMERAL_SEQUENTIAL);// 2. 解析节点序号作为workerIdintworkerIdparseWorkerId(path);// 3. 注册监听节点删除时重新分配zk.exists(path,event-{if(event.getType()EventType.NodeDeleted){reassignWorkerId();}});returnworkerId;}}优势自动分配节点启动时自动获取唯一workerId自动回收节点下线后workerId自动释放容错处理节点异常退出临时节点自动删除2. 时钟回拨处理Leaf-Snowflake 采用多层次时钟回拨处理策略轻度回拨 100ms等待时钟追平记录回拨事件监控告警中度回拨100ms ~ 1s使用备用时间源NTP服务器校验如果确认回拨等待追平如果备用时间源也回拨进入重度处理重度回拨 1s暂停服务拒绝ID生成请求告警升级通知运维人员干预人工处理可能需要重启服务或修复时钟代码实现publicclassLeafSnowflakeIdGenerator{privatelonglastTimestamp-1L;publicsynchronizedlongnextId(){longtimestamptimeGen();// 时钟回拨检测if(timestamplastTimestamp){longoffsetlastTimestamp-timestamp;if(offset100){// 轻度回拨等待waitUntilReach(lastTimestamp);timestamptimeGen();}elseif(offset1000){// 中度回拨校验备用时钟if(checkBackupClock()){waitUntilReach(lastTimestamp);timestamptimeGen();}else{thrownewClockMovedBackwardsException(时钟回拨过大);}}else{// 重度回拨暂停服务serviceStatusServiceStatus.SUSPENDED;thrownewClockMovedBackwardsException(严重时钟回拨服务暂停);}}// ... 正常生成ID逻辑}}生产环境部署1. 高可用架构客户端 → 负载均衡器 → [Leaf节点1, Leaf节点2, Leaf节点3] ↓ [ZooKeeper集群] ←→ [MySQL集群]2. 监控指标QPS/TPSID生成速率成功率ID生成成功率延迟P50/P95/P99响应时间号段水位各业务号段使用比例时钟状态时钟回拨告警次数3. 容灾方案多机房部署Leaf节点跨机房部署数据库主从MySQL主从复制读写分离ZooKeeper集群至少3节点避免单点故障客户端降级ID生成失败时降级到本地模式性能对比特性Leaf-SegmentLeaf-SnowflakeQPS10万5万延迟1ms2ms连续性段内连续段间跳跃趋势连续依赖MySQLZooKeeper 时钟服务适用场景业务维度ID高频生成全局唯一ID需要时间信息总结Leaf 系统将学术界分布式ID生成理论转化为工业级解决方案主要贡献在于工程化实现解决了原生算法的生产环境问题高可用设计多级容错故障自动恢复可观测性完善的监控、告警、管理界面易用性提供RESTful API客户端集成简单GitHub地址https://github.com/Meituan-Dianping/Leaf生产建议对于大多数互联网公司Leaf-Segment方案已能满足90%以上的场景。如果对ID的时间信息有要求或需要严格的全局递增可以考虑Leaf-Snowflake。方案对比与选择建议方案对比总览方案唯一性有序性性能可用性复杂度适用场景UUID全局唯一无序极高极高极低临时令牌、会话ID、不入库标识数据库自增单库唯一严格递增低依赖DB中单点低单机应用、小型系统数据库存根表全局唯一严格递增中依赖DB中单点中中小型分布式系统Redis原子计数全局唯一严格递增高依赖Redis中Redis集群低中小型系统QPS5万雪花算法全局唯一趋势递增极高本地高依赖时钟中大型分布式系统需要时间信息号段模式全局唯一段内连续极高内存高依赖DB中高频ID生成电商、支付Leaf-Segment全局唯一段内连续极高内存极高高可用高生产环境需要完善监控管理Leaf-Snowflake全局唯一趋势递增高本地极高高可用高生产环境需要时间信息选择决策树各场景推荐方案1. 小型项目/创业初期推荐数据库自增ID 或 UUID理由实现简单快速上线注意预留扩展空间考虑未来迁移2. 中型分布式系统QPS 1万推荐Redis原子计数 或 数据库存根表理由平衡性能与复杂度注意Redis需要高可用部署3. 大型电商/支付系统QPS 1万~10万推荐号段模式 或 Leaf-Segment理由高性能可水平扩展注意需要监控号段消耗合理设置步长4. 需要时间信息的系统如日志追踪推荐雪花算法 或 Leaf-Snowflake理由ID携带时间信息可反解析注意解决时钟回拨问题5. 临时标识/不入库数据推荐UUID理由简单无需存储注意不要作为数据库主键性能优化建议1. 分库分表场景策略在ID中嵌入分片信息示例[时间戳][分片ID][序列号]优点避免跨分片查询提升性能2. 多数据中心场景策略在ID中嵌入数据中心ID示例雪花算法中分配几位作为数据中心ID优点支持多机房部署容灾3. ID压缩与传输策略将64位长整型转为更短字符串示例Base62编码减少传输体积优点节省网络带宽前端友好监控与运维关键监控指标ID生成速率QPS/TPS异常波动告警ID重复率定期检查ID唯一性服务可用性成功率、错误率资源使用数据库连接、Redis内存容量规划预估业务增长根据业务规划预估ID需求设置合理步长号段步长 预估QPS × 缓存时间建议5-10分钟定期评估每季度评估ID方案是否仍适用迁移方案从简单方案迁移到复杂方案时双写阶段新旧方案同时生成ID记录映射关系数据迁移逐步将历史数据关联新ID读迁移先读新ID逐步切换写迁移最后切换到新ID生成方案验证阶段验证数据一致性和性能总结选择ID生成方案时需要综合考虑业务规模当前和未来的QPS需求团队能力运维复杂度与团队技能匹配成本预算硬件、运维成本业务特性是否需要时间信息、是否分库分表黄金法则没有最好的方案只有最适合的方案。从小规模开始随着业务增长逐步演进避免过度设计。