分布式 ID 服务实战

一、分布式 ID 核心需求

1. ID 本身要求

  • 全局唯一性:最基础要求,分布式场景下绝对不能重复
  • 有序性:趋势递增 / 单调递增,保证数据库 B + 树索引写入性能
  • 信息安全:部分场景(如订单 ID)要求无规则,防止竞对推算业务数据
  • 存储友好:长度适中,适配数据库主键设计(MySQL 推荐短主键)

2. 生成系统要求

  • 高性能:平均延迟、TP999 延迟极低,支撑高 QPS
  • 高可用:可用性达到 5 个 9(99.999%),核心业务强依赖
  • 可扩展:支持集群部署,水平扩容无瓶颈

二、分布式 ID 常见实现方案

2.1 UUID

核心信息
  • 标准格式:32 个 16 进制数,分 5 段(8-4-4-4-12),共 36 字符,示例:550e8400-e29b-41d4-a716-446655440000
  • Java 实现:JDK 自带java.util.UUID,可通过replaceAll("-", "")去除中划线
优缺点
优点 缺点
本地生成,无网络消耗,性能极高 16 字节 128 位,存储 / 传输成本高
实现简单,无需依赖中间件 基于 MAC 地址的算法可能泄露 MAC
- 无序性:MySQL InnoDB 聚集索引下,频繁引起数据页分裂,严重影响写入性能
- 不符合 MySQL 官方短主键建议
面试常问

Q:为什么分布式场景下不推荐 UUID 作为数据库主键?

A:①UUID 过长,违反 MySQL 短主键设计原则;②无序性导致 InnoDB B + 树索引频繁分裂,写入性能急剧下降;③部分算法存在 MAC 地址泄露风险。

2.2 雪花算法(Snowflake)

Twitter 开源的 64 位分布式 ID 生成算法,是分布式 ID 的核心考察点

核心结构(64 位 Long 型)
字段详解
  1. 1 位符号位:固定为 0,标识正数,无实际意义
  2. 41 位时间戳 :可支撑2^41毫秒(约 69 年),需指定起始时间戳(epoch)
  3. 10 位 WorkerID:默认分 5 位机房 ID+5 位机器 ID,支持 32 个机房 ×32 台机器 = 1024 个节点
  4. 12 位序列号 :毫秒内自增,最大值2^12-1=4095,单机器单毫秒最多生成 4096 个 ID
性能指标

理论 QPS:1000毫秒 × 4096个/毫秒 = 409.6w/s,满足绝大多数高并发场景

优缺点
优点 缺点
趋势递增,适配数据库索引写入 强依赖机器时钟,时钟回拨会导致 ID 重复 / 服务不可用
本地生成,性能高,无中间件依赖 需手动维护 WorkerID,集群扩容时配置成本高
可自定义 bit 位分配,适配业务场景 无内置容灾,节点故障需手动处理
64 位 Long 型,存储友好 -
衍生实现
  • 美团 Leaf-snowflake
  • 百度 UidGenerator(2018 年后停止维护)
  • Seata 内置io.seata.common.util.IdWorker
面试常问

Q1:雪花算法时钟回拨问题如何解决?

A:①启动时校验节点时间,与集群平均时间对比,偏移超阈值则启动失败;②运行时检测时钟回拨,小偏移(如≤5ms)等待时钟追上,大偏移直接返回异常;③本机缓存 WorkerID,弱依赖注册中心;④时钟回拨时触发报警,自动摘除异常节点。

Q2:雪花算法的 WorkerID 如何分配?

A:①小型集群:手动配置;②大型集群:基于 Zookeeper 持久顺序节点自动生成,节点启动时注册 ZK 并获取唯一顺序号作为 WorkerID。

Q3:为什么雪花算法是趋势递增 而非单调递增

A:不同机器的时钟存在微小偏差,跨机器的 ID 可能出现时间戳逆序,因此整体是趋势递增,单机器内是单调递增。

2.3 数据库生成方案

基础方案(单库单表)
  1. 建表语句:创建自增主键表,stub字段为占位符

    CREATE TABLE sequence_id (
    id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
    stub char(10) NOT NULL DEFAULT '',
    PRIMARY KEY (id),
    UNIQUE KEY stub (stub)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

  2. 获取 ID:通过REPLACE INTO+LAST_INSERT_ID()实现原子获取

    BEGIN;
    REPLACE INTO sequence_id (stub) VALUES ('stub');
    SELECT LAST_INSERT_ID();
    COMMIT;

优化方案(Flickr 方案)
  • 核心思路:多机部署,每台机器设置不同初始值 + 相同步长(步长 = 机器数)
  • 示例:2 台机器,步长 = 2,机器 1 初始值 = 1(1,3,5...),机器 2 初始值 = 2(2,4,6...)
优缺点
优点 缺点
实现简单,依赖 DBA 专业维护 单库存在单点问题,集群扩容复杂度高
ID 单调递增,存储友好 并发量低,每次获取 ID 需访问数据库,压力大
无额外中间件依赖 ID 无业务含义,连续 ID 存在信息泄露风险
- 水平扩容复杂,机器数变更需重新配置初始值 / 步长
面试常问

Q:REPLACE INTOINSERT INTO的区别?为什么数据库生成 ID 用REPLACE INTO

A:INSERT INTO遇主键 / 唯一索引冲突会报错;REPLACE INTO遇冲突会先删除原有记录,再插入新记录,保证原子性获取自增 ID,避免插入失败。

2.4 Redis 生成方案

核心原理

通过 Redis 的incr/incrby命令实现原子性的 ID 自增,天然支持分布式场景。

基础使用
复制代码
127.0.0.1:6379> incr sequence_id_order  # 订单ID自增
(integer) 1
127.0.0.1:6379> incrby sequence_id_user 100  # 批量生成,步长100
(integer) 101
优化方案
  • 高可用:部署 Redis Cluster,避免单点问题
  • 高性能:结合 incrby 实现批量获取,减少网络请求
  • 持久化:开启 RDB+AOF 混合持久化,降低数据丢失风险
优缺点
优点 缺点
性能高,支持高 QPS,原子性保证 持久化仍存在数据丢失风险,可能导致 ID 重复
ID 有序递增,适配数据库索引 依赖 Redis 中间件,增加系统复杂度
支持批量生成,灵活配置步长 需处理 Redis 集群的主从同步延迟问题
面试常问

Q:Redis 生成 ID 时,持久化导致的数据丢失会引发什么问题?如何缓解?

A:Redis 宕机重启后,若持久化数据未同步,incr 命令会从初始值重新开始,导致 ID 重复。缓解方案:①开启 RDB+AOF 混合持久化,降低数据丢失概率;②结合本地缓存,批量获取 ID 并缓存,减少 Redis 访问;③使用 Redis Cluster 的主从复制 + 哨兵模式,提高可用性。

三、美团 Leaf 分布式 ID 框架

Leaf 是美团开源的分布式 ID 生成框架,结合数据库号段雪花算法 实现,解决了传统方案的痛点,是面试中企业实战场景的高频考点。

3.1 Leaf 核心设计理念

名称源自莱布尼茨名言:There are no two identical leaves in the world (世界上没有两片相同的树叶),核心目标:高性能、高可用、可扩展、适配多业务场景

3.2 Leaf-segment(数据库号段方案)

对传统数据库方案的核心优化:批量获取 ID,减少数据库访问

核心原理
数据库表设计
字段名 类型 长度 说明
biz_tag varchar 128 业务标识,隔离不同业务的 ID 生成
max_id bigint - 该业务当前分配的号段最大值
step int - 每次分配的号段长度
description varchar 256 业务描述
update_time timestamp - 号段更新时间
号段获取流程
  1. 业务服务向 Leaf 服务请求 ID
  2. Leaf 服务从本地号段缓存中分配 ID
  3. 当号段消耗完时,执行 SQL 更新号段:UPDATE table SET max_id=max_id+step WHERE biz_tag=xxx
  4. 重新查询新号段并加载到本地缓存
双 Buffer 优化(解决 TP999 波动问题)
  • 核心思路:当当前号段消耗至10% 时,异步加载下一个号段到备用 Buffer
  • 解决问题:避免号段用尽时同步访问数据库导致的线程阻塞,降低 TP999 延迟
  • 推荐配置:号段长度 = 服务高峰期 QPS×600(支撑 10 分钟无数据库访问)
高可用容灾
  • 数据库部署:一主两从 + 分机房部署,主从半同步复制
  • 中间件:使用 Atlas/DBProxy 实现主从自动切换
  • 强一致方案:MySQL Group Replication(类 Paxos 算法),运维成本较高
优缺点
优点 缺点
线性扩展,性能支撑大多数业务 ID 号段连续,存在信息泄露风险,不适用于订单 ID
本地缓存号段,DB 宕机仍能短时间提供服务 TP999 仍存在微小波动(号段切换时)
支持自定义 max_id,便于业务迁移 依赖数据库,数据库完全宕机后无法获取新号段
按 biz_tag 隔离业务,无相互影响 -

3.3 Leaf-snowflake(雪花算法方案)

基于原生雪花算法优化,解决WorkerID 自动分配时钟回拨 问题,适用于ID 无规则的场景(如订单 ID)。

核心优化点
  1. WorkerID 自动分配:基于 Zookeeper 持久顺序节点,节点启动时自动注册并获取唯一 WorkerID
  2. 弱依赖 Zookeeper:本机文件系统缓存 WorkerID,ZK 宕机时仍能启动服务
  3. 时钟回拨解决:启动时校验集群时间、运行时检测时钟偏移、小偏移等待、大偏移返回异常并报警
  4. 集群时间同步:节点定期上报时间到 ZK,运行时对比集群平均时间
启动流程

3.4 Leaf 性能指标

  • 机器配置:4C8G
  • 压测 QPS:近 5 万 /s
  • TP999 延迟:1ms
  • 日常调用:每天亿数量级,支撑美团金融、支付、外卖、猫眼等核心业务

四、电商项目实战:tulingmall-unqid(基于 Leaf 改造)

4.1 改造背景

电商系统无 Zookeeper 部署,且无需考虑 ID 信息安全,因此移除 Leaf-snowflake 模块,仅保留 Leaf-segment 核心功能。

4.2 核心改造点:批量获取 ID

业务痛点

订单生成时,需同时生成订单主表 ID订单详情表多个 ID,若单次获取一个 ID,会增加网络请求,影响性能。

解决方案

新增批量获取 ID 接口,限定最大批量数为 5000,满足订单详情等批量插入场景。

核心接口(Java/SpringMVC)
复制代码
// 原接口:单次获取一个ID
@RequestMapping(value = "/api/segment/get/{key}")
public String getSegmentId(@PathVariable("key") String key) {
    return get(key, segmentService.getId(key));
}

// 改造后新增:批量获取ID
@RequestMapping(value = "/api/segment/getlist/{key}")
public List<String> getSegmentIdList(@PathVariable("key") String key, @RequestParam int keyNumber) {
    if(keyNumber==0 || keyNumber>5000) keyNumber=5000; // 限定最大5000
    return getList(key,segmentService.getIds(key,keyNumber));
}

4.3 服务特性

  • 无状态服务,支持集群部署,水平扩容无瓶颈
  • 基于 Leaf-segment 双 Buffer 优化,保证高性能
  • 按业务 biz_tag 隔离,支持商品、订单、用户等多业务 ID 生成
相关推荐
七夜zippoe2 小时前
分布式系统弹性设计实战:Hystrix与Sentinel熔断降级深度解析
java·hystrix·sentinel·aws·分布式系统
廋到被风吹走2 小时前
设计原则深度解析:高内聚低耦合、模块边界与服务拆分
java
识君啊2 小时前
Java 滑动窗口 - 附LeetCode经典题解
java·算法·leetcode·滑动窗口
Zachery Pole2 小时前
JAVA_08_封装、继承和多态
java·开发语言
白露与泡影2 小时前
Java 春招高级面试指南( Java 面试者必备)
java·开发语言·面试
追随者永远是胜利者2 小时前
(LeetCode-Hot100)3. 无重复字符的最长子串
java·算法·leetcode·职场和发展·go
Lenyiin2 小时前
《LeetCode 顺序刷题》11 -20
java·c++·python·算法·leetcode·lenyiin
wuqingshun3141592 小时前
说一下java的四种引用
java·开发语言