分布式 ID 生成服务的架构演进——从数据库自增到美团 Leaf 的优化实践一、为什么需要分布式 ID 生成服务在单体应用时代使用数据库自增主键AUTO_INCREMENT就能满足所有 ID 生成需求。但进入微服务和分库分表架构后单一数据库的自增序列无法跨库、跨服务保证全局唯一性。业务对 ID 的需求也从唯一升级到了更多维度趋势递增方便索引、高可用不允许单点故障、高性能支撑每秒数十万生成量、信息安全不希望被外部猜测业务量。我们团队的分布式 ID 生成方案经历了四个阶段的演进每个阶段解决的是不同规模下的核心矛盾。二、方案演进全景三、第一阶段数据库自增早期方案最简单每个业务表独立使用数据库自增主键。当进行分库分表时采用不同的起始值和步长来避免冲突。-- 分库分表时的自增ID策略 -- 库1从1开始步长为21, 3, 5, 7... SET auto_increment_offset 1; SET auto_increment_increment 2; -- 库2从2开始步长为22, 4, 6, 8... SET auto_increment_offset 2; SET auto_increment_increment 2;缺陷非常明显每次扩容都需要调整步长配置运维复杂ID 生成强依赖数据库无法支撑高并发场景业务量通过ID趋势直接暴露信息安全有隐患。四、第二阶段号段模式美团 Leaf-Segment号段模式的核心思路是从数据库批量获取一个ID号段如1~1000缓存在本地内存中业务直接从内存中分配ID。号段用完后再向数据库请求下一个号段。/** * 号段模式的分布式ID生成器 * 核心思路预分配号段本地内存分配减少DB交互 */ Component public class SegmentIdGenerator { /** ID业务标识区分不同业务线的ID序列 */ private final String bizTag; /** 每次从DB批取的号段长度 */ private final int segmentSize; /** 当前号段的起始值 */ private volatile long currentStart; /** 当前号段的下一个分配位置 */ private final AtomicLong cursor; /** 数据源用于持久化号段进度 */ private final DataSource dataSource; public SegmentIdGenerator(String bizTag, int segmentSize, DataSource dataSource) { this.bizTag bizTag; this.segmentSize segmentSize; this.dataSource dataSource; this.cursor new AtomicLong(0); // 初始化时直接从DB加载号段 loadSegment(); } /** * 获取下一个ID * return 全局唯一的趋势递增ID */ public synchronized long nextId() { long next cursor.incrementAndGet(); // 当前号段用完加载下一个号段 if (next currentStart segmentSize) { loadSegment(); next cursor.incrementAndGet(); } return next; } /** * 从数据库加载下一个号段 */ private void loadSegment() { try (Connection conn dataSource.getConnection()) { conn.setAutoCommit(false); try { // 使用 SELECT ... FOR UPDATE 保证并发安全 String selectSql SELECT max_id FROM id_segment WHERE biz_tag ? FOR UPDATE ; PreparedStatement selectStmt conn.prepareStatement(selectSql); selectStmt.setString(1, bizTag); ResultSet rs selectStmt.executeQuery(); long oldMaxId 0; if (rs.next()) { oldMaxId rs.getLong(max_id); } long newMaxId oldMaxId segmentSize; // 更新最大ID String updateSql UPDATE id_segment SET max_id ?, update_time NOW() WHERE biz_tag ? ; PreparedStatement updateStmt conn.prepareStatement(updateSql); updateStmt.setLong(1, newMaxId); updateStmt.setString(2, bizTag); updateStmt.executeUpdate(); conn.commit(); // 更新本地号段信息 this.currentStart oldMaxId; this.cursor.set(oldMaxId); } catch (SQLException e) { conn.rollback(); throw new IdGenerateException(号段加载失败, bizTag bizTag, e); } } catch (SQLException e) { throw new IdGenerateException(数据库连接异常, e); } } }五、第三阶段双Buffer优化号段模式存在的问题是当号段耗尽时需要同步等待DB分配下一个号段导致短暂的ID分配阻塞。双Buffer机制通过异步预加载下一个号段解决了这一问题。核心设计是维护两个号段Buffer当前Buffer正在使用的号段业务直接从中分配备用Buffer异步从DB加载的下一个号段在当前Buffer用完时无缝切换/** * 双Buffer号段分发器 * 解决号段耗尽时的阻塞等待问题 */ public class DualBufferSegmentDispatcher { /** 当前使用的号段 */ private volatile SegmentBuffer current; /** 备用的号段异步预加载 */ private volatile SegmentBuffer backup; /** 号段是否准备就绪 */ private final AtomicBoolean ready new AtomicBoolean(false); /** 异步加载线程池 */ private final ScheduledExecutorService loader; public DualBufferSegmentDispatcher(String bizTag, int segmentSize, DataSource dataSource) { this.loader Executors.newSingleThreadScheduledExecutor(r - { Thread t new Thread(r, segment-loader- bizTag); t.setDaemon(true); return t; }); // 首次加载两个号段 this.current loadSegmentFromDb(bizTag, segmentSize, dataSource); this.backup loadSegmentFromDb(bizTag, segmentSize, dataSource); this.ready.set(true); // 定时检查备用Buffer状态提前异步加载 this.loader.scheduleWithFixedDelay(() - { if (backup null) { backup loadSegmentFromDb(bizTag, segmentSize, dataSource); } }, 0, 1, TimeUnit.SECONDS); } /** * 获取下一个ID无阻塞 */ public long nextId() { if (!ready.get()) { throw new IllegalStateException(号段分发器未就绪); } long id current.nextId(); // 当前号段用完切换到备用号段 if (current.isExhausted()) { synchronized (this) { if (current.isExhausted()) { current backup; backup null; // 触发异步加载 } } id current.nextId(); } return id; } }六、第四阶段Snowflake 优化对于不需要存储ID号段进度、追求极致性能的场景我们基于Snowflake算法做了适应性的优化。原始Snowflake算法是 1位符号 41位时间戳 10位工作机器ID 12位序列号。我们调整为41位时间差相对于2024-01-01 10位机器ID 12位序列号这样做的好处是ID可用到2089年同时避免时钟回拨时的ID冲突。/** * 优化的Snowflake算法实现——支持时钟回拨保护 */ public class OptimizedSnowflakeIdGenerator { /** 自定义起始时间戳 (2024-01-01 00:00:00) */ private static final long START_EPOCH 1704067200000L; /** 机器ID占用的位数 */ private static final long WORKER_ID_BITS 10L; /** 序列号占用的位数 */ private static final long SEQUENCE_BITS 12L; /** 最大机器ID */ private static final long MAX_WORKER_ID ~(-1L WORKER_ID_BITS); /** 序列号掩码 */ private static final long SEQUENCE_MASK ~(-1L SEQUENCE_BITS); private final long workerId; private long sequence 0L; private long lastTimestamp -1L; /** 时钟回拨容忍阈值毫秒 */ private static final long CLOCK_BACKWARD_TOLERANCE 5L; public OptimizedSnowflakeIdGenerator(long workerId) { if (workerId MAX_WORKER_ID || workerId 0) { throw new IllegalArgumentException( workerId 必须在 0 到 MAX_WORKER_ID 之间); } this.workerId workerId; } /** * 生成下一个ID线程安全 */ public synchronized long nextId() { long currentTimestamp System.currentTimeMillis(); // 时钟回拨检测与保护 if (currentTimestamp lastTimestamp) { long offset lastTimestamp - currentTimestamp; if (offset CLOCK_BACKWARD_TOLERANCE) { // 在容忍范围内等待时钟追上 try { Thread.sleep(offset); } catch (InterruptedException e) { Thread.currentThread().interrupt(); throw new IdGenerateException(等待时钟同步时被中断, e); } currentTimestamp System.currentTimeMillis(); } else { throw new IdGenerateException( 时钟回拨超过阈值: offset ms); } } if (currentTimestamp lastTimestamp) { // 同一毫秒内序列号自增 sequence (sequence 1) SEQUENCE_MASK; if (sequence 0) { // 序列号用完等待下一毫秒 currentTimestamp waitNextMillis(lastTimestamp); } } else { sequence 0L; } lastTimestamp currentTimestamp; // 组装ID: 时间差左移22位 | 机器ID左移12位 | 序列号 return ((currentTimestamp - START_EPOCH) (WORKER_ID_BITS SEQUENCE_BITS)) | (workerId SEQUENCE_BITS) | sequence; } /** * 等待到下一毫秒 */ private long waitNextMillis(long lastTimestamp) { long timestamp System.currentTimeMillis(); while (timestamp lastTimestamp) { timestamp System.currentTimeMillis(); } return timestamp; } }七、总结与建议四个阶段的演进路径本质上是在性能、可用性、数据安全、运维复杂度四个维度之间的取舍。对于大部分团队来说号段模式已经能满足99%的场景需求。只有在绝对不依赖外部存储、且需要极低延迟的场景下才有必要引入Snowflake方案。值得强调的是无论选择哪种方案都要预留ID反解的能力——通过ID能反向推导出生成时间、所属机器等信息这在问题排查和数据分析时极其有用。八、方案选型的量化参考在实际技术选型时需要结合业务的具体QPS要求和可用性SLA来做决策。以下是我们在不同业务场景下的实测数据对比方案峰值QPS单点故障风险P99延迟适用场景数据库自增 500高5-10ms单体应用、小流量内部系统号段模式单Buffer 50,000中1-3ms中小规模微服务号段模式双Buffer 200,000低 1ms大流量核心业务Snowflake 1,000,000低 0.5ms极致性能需求、无状态服务需要注意的是号段模式的QPS上限受限于号段大小和分配频率。如果将号段长度从1000调整到10000单次从DB加载号段的间隔延长10倍能显著提升可用性和峰值承载能力但会牺牲一定的ID连续性监控看板上的ID趋势跳跃会更明显。这是典型的工程Trade-off——在可接受的范围内优先保障可用性。分布式ID看似是个小问题但在一线架构中往往成为性能瓶颈的导火索。欢迎分享你的实践经验。