分布式 ID 生成服务的架构演进——从数据库自增到美团 Leaf 的优化实践
分布式 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看似是个小问题但在一线架构中往往成为性能瓶颈的导火索。欢迎分享你的实践经验。

相关新闻

深度学习OCR技术革新:dots.mocr模型解析与应用

深度学习OCR技术革新:dots.mocr模型解析与应用

1. 项目背景与技术价值最近在文档数字化处理领域有个重磅消息——华中科技大学联合小红书HI Lab开源了dots.mocr模型。这个基于深度学习的OCR系统不仅能识别文字,还能完美还原文档的物理结构和逻辑关系,甚至能把图形元素转换成可编辑的SVG格式。作为长期…

2026/7/25 7:27:11 阅读更多 →
【Autosar从入门到精通到进阶实战篇】83 刷写安全:如何防止ECU被“黑”掉?

【Autosar从入门到精通到进阶实战篇】83 刷写安全:如何防止ECU被“黑”掉?

83 刷写安全:如何防止ECU被“黑”掉? 老张上次从双分区回滚的泥潭里爬出来,刚松了口气,客户又来了新需求——“我们的ECU被第三方刷了盗版固件,跑了三天就烧了,赔了十几万。你们得想办法,让不是我们签名的固件根本刷不进去。” 我盯着他手机里那张烧焦的电路板照片,后…

2026/7/25 7:27:11 阅读更多 →
【Autosar从入门到精通到进阶实战篇】82 刷写失败后的恢复策略:如何让ECU“起死回生”

【Autosar从入门到精通到进阶实战篇】82 刷写失败后的恢复策略:如何让ECU“起死回生”

82 刷写失败后的恢复策略:如何让ECU“起死回生” 上周,我接到一个朋友的紧急电话——他在台架上刷写某款量产ECU时,突然停电了。等电源恢复,ECU再也无法正常启动,诊断仪连不上,整个控制器变“砖”。他急得满头大汗:“完了,这块板子废了,得重新焊一个Bootloader出来。…

2026/7/25 7:27:11 阅读更多 →

最新新闻

C++流程控制核心:for、cin与if组合实战指南

C++流程控制核心:for、cin与if组合实战指南

1. 项目概述:从“会写”到“会想”的关键一步如果你已经跟着教程走过了C的基础语法,比如变量、数据类型、运算符,甚至已经能写几个简单的顺序结构程序,那么恭喜你,你已经迈出了坚实的第一步。但你可能也感觉到了&#…

2026/7/25 7:47:17 阅读更多 →
GLM-5.2自部署实战:硬件选型、成本核算与避坑指南

GLM-5.2自部署实战:硬件选型、成本核算与避坑指南

1. 自部署 GLM-5.2 到底能快多少?先看成本和场景 如果你在找一款能自己部署、代码能力强的开源大模型,GLM-5.2 的发布确实值得关注。它最核心的吸引力不是“快”,而是“在特定条件下,自部署的成本效益可能远超官方托管 API”。这个“快”更多体现在对请求的完全控制、无网络…

2026/7/25 7:47:17 阅读更多 →
AI Agent技术演进:从函数调用到多技能协同

AI Agent技术演进:从函数调用到多技能协同

1. 项目概述:AI Agent能力扩展的技术演进在AI技术快速发展的今天,智能体(Agent)的能力边界正在不断拓展。从最初的简单函数调用到如今的复杂技能组合,AI Agent的进化路径清晰地反映了人工智能技术的成熟过程。作为一名…

2026/7/25 7:47:17 阅读更多 →
C++内存碎片化:成因、诊断与实战优化策略

C++内存碎片化:成因、诊断与实战优化策略

1. 项目概述:为什么C开发者必须直面内存碎片化?如果你是一名C开发者,尤其是长期维护大型、长生命周期的服务端应用或游戏引擎,那么“内存碎片化”这个词对你来说,绝对不是一个陌生的概念。它不像内存泄漏那样会立刻导致…

2026/7/25 7:47:17 阅读更多 →
跨境电商内容智能化生产:Sora API与客易云的实践

跨境电商内容智能化生产:Sora API与客易云的实践

1. 跨境电商内容生产的现状与挑战跨境电商行业近年来面临内容同质化严重、制作成本高企、本地化适配不足三大痛点。根据行业调研数据显示,平均每个跨境电商团队需要为单个产品制作15-22种不同形式的内容素材,包括产品主图、场景图、短视频、详情页等。传…

2026/7/25 7:46:17 阅读更多 →
C++异步日志库Quill实战:低延迟配置、缩进样式与性能调优指南

C++异步日志库Quill实战:低延迟配置、缩进样式与性能调优指南

1. 项目概述与核心价值最近在重构一个对性能极其敏感的后台服务,日志模块成了瓶颈。之前用的同步日志库,在高并发场景下I/O阻塞导致请求延迟飙升,profiler一开,时间全耗在写文件上了。试了几个方案,最终把目光锁定在了…

2026/7/25 7:46:17 阅读更多 →

日新闻

突破文档下载限制:kill-doc让你看到的都能保存

突破文档下载限制:kill-doc让你看到的都能保存

突破文档下载限制:kill-doc让你看到的都能保存 【免费下载链接】kill-doc 看到经常有小伙伴们需要下载一些免费文档,但是相关网站浏览体验不好各种广告,各种登录验证,需要很多步骤才能下载文档,该脚本就是为了解决您的…

2026/7/25 0:00:35 阅读更多 →
C++ string类模拟实现:从深拷贝到内存管理的完整指南

C++ string类模拟实现:从深拷贝到内存管理的完整指南

1. 项目概述:为什么我们要“手撕”string类?在C的学习道路上,尤其是从C语言过渡到C的“初阶”阶段,string类绝对是一个绕不开的核心。标准库里的std::string用起来太方便了,、find、substr,几个操作符和函数…

2026/7/25 0:00:35 阅读更多 →
三角洲寻宝鼠工具:高效文件搜索与资源管理实战指南

三角洲寻宝鼠工具:高效文件搜索与资源管理实战指南

1. 先搞清楚“三角洲寻宝鼠”到底是什么工具从名称来看,“三角洲寻宝鼠”更像是一个资源查找或文件检索类工具,而不是游戏或娱乐软件。这类工具的核心价值在于帮助用户快速定位特定资源,比如文档、图片、压缩包或特定格式的文件。如果你经常需…

2026/7/25 0:00:35 阅读更多 →

周新闻

Go语言静态资源打包方案对比与实践指南

Go语言静态资源打包方案对比与实践指南

1. 项目背景与核心需求在Go语言开发中,我们经常需要处理静态资源文件的打包问题。无论是Web应用的模板文件、前端资源,还是配置文件、证书等,都需要随程序一起分发。传统做法是将这些文件与编译后的二进制文件放在同一目录下,但这…

2026/7/25 5:08:22 阅读更多 →
Go语言实现高性能LDAP认证服务的架构与实践

Go语言实现高性能LDAP认证服务的架构与实践

1. 项目背景与核心价值LDAP(轻量级目录访问协议)作为企业级身份认证的黄金标准,已经服务了超过80%的财富500强公司。我在金融科技领域实施统一认证体系时,发现传统Java方案存在启动慢、内存占用高等痛点。而Go语言凭借其协程并发模…

2026/7/25 5:13:53 阅读更多 →
【AI面试官实战指南】:用ChatGPT模拟10类高频技术岗面试,3天提升应答精准度92%

【AI面试官实战指南】:用ChatGPT模拟10类高频技术岗面试,3天提升应答精准度92%

更多请点击: https://intelliparadigm.com 第一章:AI面试官实战指南的核心价值与适用场景 AI面试官并非替代人类HR的“黑箱工具”,而是以可解释、可审计、可迭代的方式,赋能招聘全链路的关键基础设施。其核心价值在于将主观经验沉…

2026/7/24 18:52:18 阅读更多 →

月新闻