Files
interview/questions/distributed-id.md
yasinshaw 71e3497bfd feat: add comprehensive system design interview questions
- design-seckill.md: 秒杀系统设计
- design-shorturl.md: 短链接系统设计
- design-lbs.md: LBS附近的人系统设计
- design-im.md: 即时通讯系统设计
- design-feed.md: 社交信息流系统设计

Each document includes:
- Requirements analysis and data volume assessment
- Technical challenges
- System architecture design
- Database design
- Caching strategies
- Scalability considerations
- Practical project experience
- Alibaba P7 level additional points

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-28 23:43:36 +08:00

18 KiB
Raw Blame History

分布式 ID 生成方案

问题

  1. 为什么需要分布式 ID分布式 ID 有哪些要求?
  2. 常见的分布式 ID 生成方案有哪些?各自的优缺点是什么?
  3. Snowflake 算法的原理是什么?有什么坑?
  4. 数据库自增 ID 如何实现分布式?如何优化性能?
  5. Redis 如何生成分布式 ID有哪些优缺点
  6. 在实际项目中,你是如何设计分布式 ID 的?

标准答案

1. 分布式 ID 的要求和特点

为什么需要分布式 ID

在分布式系统中,需要保证 ID 的全局唯一性

  • 订单号、用户 ID、支付流水号
  • 分库分表后的主键冲突问题
  • 微服务间的数据关联

单机 ID 的问题

单机数据库自增 ID
- 实例 11, 2, 3, 4, 5
- 实例 21, 2, 3, 4, 5  ← 冲突!

分布式 ID 的核心要求

要求 说明 示例
全局唯一性 不能重复 订单号不能重复
有序性 趋势递增(可选) 按时间排序的订单号
高性能 生成速度快 支持 10 万+/秒
高可用 服务不中断 宕机后仍可生成
信息安全 不暴露业务信息 不暴露订单总量

2. 常见分布式 ID 方案对比

方案 唯一性 有序性 性能 复杂度 适用场景
UUID 非主键、内部 ID
数据库自增 小规模、并发低
Redis INCR 中小规模
Snowflake 大规模、高并发
号段模式 大规模、高性能
美团 Leaf 金融级、高可用

3. UUID

原理

UUIDUniversally Unique Identifier是 128 位的唯一标识符。

格式

xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
550e8400-e29b-41d4-a716-446655440000

Java 示例

import java.util.UUID;

String uuid = UUID.randomUUID().toString();
// 输出550e8400-e29b-41d4-a716-446655440000

优缺点

优点

  • 性能高:本地生成,无网络开销
  • 简单JDK 自带,无需额外组件

缺点

  • 无序:无法按时间排序
  • 过长36 字符,存储空间大
  • 不安全:暴露 MAC 地址UUID v1
  • 索引性能差:无序 ID 导致 B+ 树频繁分裂

B+ 树分裂问题

有序 ID
1 → 100 → 1000 → 10000
└─ 顺序插入B+ 树叶子节点顺序填充

无序 IDUUID
abc → xyz → 123 → 999
└─ 随机插入B+ 树频繁分裂,性能差

适用场景

  • 非数据库主键(如请求 ID
  • 临时标识、会话 ID
  • 内部系统、不需要排序
  • 订单号、用户 ID需要有序

4. 数据库自增 ID

方案 1单机自增

CREATE TABLE orders (
  id BIGINT AUTO_INCREMENT PRIMARY KEY,
  order_no VARCHAR(32),
  created_at DATETIME
);

问题:单机性能瓶颈,无法扩展。


方案 2多机步长模式Flickr 方案)

原理:不同数据库实例设置不同的起始值和步长。

实例 1起始 1步长 2 → 1, 3, 5, 7, 9
实例 2起始 2步长 2 → 2, 4, 6, 8, 10

配置

-- 实例 1
SET auto_increment_increment = 2;
SET auto_increment_offset = 1;

-- 实例 2
SET auto_increment_increment = 2;
SET auto_increment_offset = 2;

优点

  • 实现简单
  • ID 有序

缺点

  • 扩容困难:需要重新计算步长
  • 性能瓶颈:数据库写入限制

方案 3号段模式批量获取

原理:一次从数据库获取一批 ID号段缓存在本地。

表结构

CREATE TABLE id_segment (
  biz_type VARCHAR(32) PRIMARY KEY,  -- 业务类型
  max_id BIGINT,                     -- 当前最大 ID
  step INT,                          -- 步长(批量大小)
  version INT,                       -- 版本号(乐观锁)
  updated_at DATETIME
);

-- 初始化数据
INSERT INTO id_segment (biz_type, max_id, step, version)
VALUES ('order', 0, 1000, 1);

获取 ID 逻辑

@Service
public class IdSegmentService {

    @Autowired
    private IdSegmentMapper segmentMapper;

    private final Map<String, IdSegment> localCache = new ConcurrentHashMap<>();

    @Transactional
    public synchronized Long nextId(String bizType) {
        IdSegment segment = localCache.get(bizType);

        // 本地号段用完,从数据库获取
        if (segment == null || segment.getCurrentId() >= segment.getMaxId()) {
            segment = fetchSegmentFromDb(bizType);
        }

        // 返回下一个 ID
        return segment.getNextId();
    }

    private IdSegment fetchSegmentFromDb(String bizType) {
        // 使用 CAS 更新数据库
        IdSegment dbSegment = segmentMapper.selectByType(bizType);

        // 更新 max_id = max_id + step
        int updated = segmentMapper.updateMaxId(
            bizType,
            dbSegment.getMaxId() + dbSegment.getStep(),
            dbSegment.getVersion()
        );

        if (updated == 0) {
            throw new RuntimeException("并发冲突,请重试");
        }

        // 缓存到本地
        IdSegment newSegment = new IdSegment();
        newSegment.setMaxId(dbSegment.getMaxId() + dbSegment.getStep());
        newSegment.setCurrentId(dbSegment.getMaxId());

        localCache.put(bizType, newSegment);
        return newSegment;
    }
}

Mapper

@Update("UPDATE id_segment SET max_id = #{maxId}, version = version + 1 " +
        "WHERE biz_type = #{bizType} AND version = #{version}")
int updateMaxId(@Param("bizType") String bizType,
                @Param("maxId") Long maxId,
                @Param("version") Integer version);

优点

  • 性能高:本地缓存,减少数据库访问
  • 有序ID 趋势递增

缺点

  • 宕机丢 ID本地缓存的 ID 未使用完就丢失
  • 实现复杂

优化使用双缓冲Double Buffer机制预加载号段。


5. Redis INCR

原理

使用 Redis 的 INCRINCRBY 命令生成全局唯一 ID。

示例

# 初始化
SET order:id 1

# 获取下一个 ID
INCR order:id
# 返回2

# 批量获取(步长 1000
INCRBY order:id 1000
# 返回1001

Java 实现

@Service
public class RedisIdGenerator {

    @Autowired
    private StringRedisTemplate redisTemplate;

    public Long nextId(String key) {
        // 使用 redisTemplate 的 opsForValue
        Long id = redisTemplate.opsForValue().increment(key);

        if (id == null) {
            throw new RuntimeException("生成 ID 失败");
        }

        return id;
    }

    // 批量获取(优化性能)
    public Long[] batchNextId(String key, int count) {
        Long startId = redisTemplate.opsForValue().increment(key, count);

        Long[] ids = new Long[count];
        for (int i = 0; i < count; i++) {
            ids[i] = startId - count + i + 1;
        }

        return ids;
    }
}

优缺点

优点

  • 性能高Redis 内存操作
  • 有序ID 趋势递增
  • 实现简单

缺点

  • 依赖 Redis需要维护 Redis 集群
  • 宕机丢数据:未持久化会丢失

持久化配置

# 开启 AOF 持久化
appendonly yes
appendfsync everysec

适用场景

  • 中小规模、性能要求高
  • 已有 Redis 集群
  • 可容忍短期 ID 丢失

6. Snowflake 算法

原理

Snowflake 是 Twitter 开源的分布式 ID 算法,生成 64 位的 Long 型 ID。

结构64 位):

0 - 0000000000 0000000000 0000000000 0000000000 0 - 00000 - 00000 - 000000000000
↑   ↑                                                    ↑       ↑        ↑
│   │                                                    │       │        │
│   └─ 41 位时间戳(毫秒)                                │       │        │
│                                                        │       │        │
│                                                        └─ 5 位数据中心 ID        │
│                                                                └─ 12 位序列号
1 位符号位(始终为 0

组成部分

  • 1 位符号位:始终为 0正数
  • 41 位时间戳:毫秒级,可用 69 年(2^41 / 1000 / 60 / 60 / 24 / 365 ≈ 69
  • 5 位数据中心 ID:支持 32 个数据中心
  • 5 位机器 ID:每个数据中心 32 台机器
  • 12 位序列号:每毫秒可生成 4096 个 ID

理论 QPS

单机4096 / 毫秒 = 409.6 万/秒

Java 实现

public class SnowflakeIdGenerator {

    // 起始时间戳2024-01-01 00:00:00
    private final long twepoch = 1704067200000L;

    // 各部分位数
    private final long workerIdBits = 5L;
    private final long datacenterIdBits = 5L;
    private final long sequenceBits = 12L;

    // 最大值
    private final long maxWorkerId = -1L ^ (-1L << workerIdBits);        // 31
    private final long maxDatacenterId = -1L ^ (-1L << datacenterIdBits); // 31
    private final long maxSequence = -1L ^ (-1L << sequenceBits);        // 4095

    // 位移
    private final long workerIdShift = sequenceBits;                      // 12
    private final long datacenterIdShift = sequenceBits + workerIdBits;   // 17
    private final long timestampShift = sequenceBits + workerIdBits + datacenterIdBits; // 22

    private final long workerId;
    private final long datacenterId;
    private long sequence = 0L;
    private long lastTimestamp = -1L;

    public SnowflakeIdGenerator(long workerId, long datacenterId) {
        if (workerId > maxWorkerId || workerId < 0) {
            throw new IllegalArgumentException("workerId 无效");
        }
        if (datacenterId > maxDatacenterId || datacenterId < 0) {
            throw new IllegalArgumentException("datacenterId 无效");
        }
        this.workerId = workerId;
        this.datacenterId = datacenterId;
    }

    public synchronized long nextId() {
        long timestamp = System.currentTimeMillis();

        // 时钟回拨检查
        if (timestamp < lastTimestamp) {
            throw new RuntimeException("时钟回拨,拒绝生成 ID");
        }

        // 同一毫秒内,序列号自增
        if (timestamp == lastTimestamp) {
            sequence = (sequence + 1) & maxSequence;
            // 序列号溢出,等待下一毫秒
            if (sequence == 0) {
                timestamp = tilNextMillis(lastTimestamp);
            }
        } else {
            // 新毫秒,序列号重置
            sequence = 0L;
        }

        lastTimestamp = timestamp;

        // 组装 ID
        return ((timestamp - twepoch) << timestampShift)
                | (datacenterId << datacenterIdShift)
                | (workerId << workerIdShift)
                | sequence;
    }

    // 等待下一毫秒
    private long tilNextMillis(long lastTimestamp) {
        long timestamp = System.currentTimeMillis();
        while (timestamp <= lastTimestamp) {
            timestamp = System.currentTimeMillis();
        }
        return timestamp;
    }

    // 解析 ID用于调试
    public static void parseId(long id) {
        long timestamp = (id >> 22) + 1704067200000L;
        long datacenterId = (id >> 17) & 0x1F;
        long workerId = (id >> 12) & 0x1F;
        long sequence = id & 0xFFF;

        System.out.println("ID: " + id);
        System.out.println("时间戳: " + new Date(timestamp));
        System.out.println("数据中心 ID: " + datacenterId);
        System.out.println("机器 ID: " + workerId);
        System.out.println("序列号: " + sequence);
    }
}

使用示例

// 初始化workerId=1, datacenterId=1
SnowflakeIdGenerator idGenerator = new SnowflakeIdGenerator(1, 1);

// 生成 ID
long id = idGenerator.nextId();
System.out.println("生成的 ID: " + id);

// 解析 ID
SnowflakeIdGenerator.parseId(id);

Snowflake 的坑

问题 1时钟回拨

原因

  • 系统时钟不准确NTP 同步)
  • 人工修改系统时间

后果

时间10:00:00.100,生成 ID时间戳=T1
时钟回拨:时间 → 09:59:59.900
时间09:59:59.950,生成 ID时间戳=T2 < T1← 冲突!

解决方案

  1. 拒绝服务(简单):
if (timestamp < lastTimestamp) {
    throw new RuntimeException("时钟回拨,拒绝生成 ID");
}
  1. 等待时钟追上(推荐):
if (timestamp < lastTimestamp) {
    long offset = lastTimestamp - timestamp;
    if (offset <= 5) {
        // 等待 5 毫秒
        try {
            Thread.sleep(offset << 1);
            timestamp = System.currentTimeMillis();
        } catch (InterruptedException e) {
            throw new RuntimeException("时钟回拨,等待失败");
        }
    } else {
        throw new RuntimeException("时钟回拨过多,拒绝生成 ID");
    }
}
  1. 使用备用 workerId(美团 Leaf 方案):
if (timestamp < lastTimestamp) {
    // 切换到备用 workerId
    workerId = backupWorkerId;
}

问题 2机器 ID 分配

问题:如何保证 workerId 全局唯一?

解决方案

  1. 配置文件(简单):
application.yml:
  snowflake:
    worker-id: 1
    datacenter-id: 1
  1. 数据库配置
CREATE TABLE worker_config (
  id INT PRIMARY KEY,
  worker_id INT,
  datacenter_id INT,
  ip VARCHAR(32),
  used BOOLEAN
);

-- 启动时申请 workerId
INSERT INTO worker_config (worker_id, datacenter_id, ip, used)
VALUES (1, 1, '192.168.1.10', TRUE);
  1. Zookeeper 顺序节点(动态):
// 在 Zookeeper 中创建临时顺序节点
String path = zk.create("/snowflake/worker-", null, ZooDefs.Ids.EPHEMERAL_SEQUENTIAL);
// 获取序号作为 workerId
int workerId = Integer.parseInt(path.split("-")[1]);
  1. Redis INCR(动态):
Long workerId = redisTemplate.opsForValue().increment("snowflake:worker:id");

问题 3序列号溢出

场景高并发下1 毫秒内请求超过 4096 个。

解决

// 序列号溢出,等待下一毫秒
if (sequence == 0) {
    timestamp = tilNextMillis(lastTimestamp);
}

优化:使用 13 位序列号(每毫秒 8192 个 ID


优缺点

优点

  • 性能极高409 万 QPS/单机
  • 有序:趋势递增(按时间排序)
  • 不依赖数据库、Redis

缺点

  • 时钟回拨问题
  • 机器 ID 分配复杂
  • ID 较长18 位数字)

适用场景

  • 大规模、高并发场景
  • 需要有序 ID
  • 可容忍时钟问题(或已有解决方案)

实际应用

  • 百度 UidGenerator
  • 美团 LeafSnowflake 模式)
  • Etsy

7. 美团 Leaf

Leaf-segment号段模式

原理:优化版号段模式,使用双缓冲机制。

架构

Leaf Server
├─ Buffer 1当前使用号段 [1000, 2000)
└─ Buffer 2预加载号段 [2000, 3000)(后台异步加载)

当 Buffer 1 用完:
└─ 切换到 Buffer 2
└─ 异步加载 Buffer 1 的下一个号段

优点

  • 无停顿:双缓冲无缝切换
  • 高性能:本地缓存

缺点

  • 宕机丢 ID未使用的号段丢失

Leaf-snowflake优化版 Snowflake

优化点

  1. Zookeeper 生成 workerId:动态分配,无需配置
  2. 时钟回拨优化
    • 回拨 5ms 内:等待时钟追上
    • 回拨 5ms 外:告警并拒绝服务

架构

Leaf Server 1workerId=1
Leaf Server 2workerId=2
Leaf Server 3workerId=3
       ↓
  Zookeeper协调

GitHubhttps://github.com/Meituan-Dianping/Leaf


8. 百度 UidGenerator

特点

  • 基于 Snowflake 优化
  • 使用 22 位序列号(每秒 400 万 ID
  • 支持跨毫秒分配序列号

GitHubhttps://github.com/baidu/uid-generator


9. 实际项目选型建议

决策树

是否需要有序?
├─ 否 → UUID最简单
└─ 是 → 继续判断
    │
    ├─ QPS < 1000
    │   ├─ 是 → Redis INCR简单
    │   └─ 否 → 继续判断
    │
    ├─ 已有 Redis
    │   ├─ 是 → 号段模式(高性能)
    │   └─ 否 → 继续判断
    │
    ├─ 可容忍时钟回拨问题?
    │   ├─ 是 → Snowflake性能最高
    │   └─ 否 → 美团 Leaf-snowflake
    │
    └─ 金融级可靠性?
        └─ 美团 Leaf-segment + 监控

性能对比

方案 单机 QPS 延迟 依赖
UUID 1000 万+ 0.001ms
数据库自增 1000 10ms 数据库
Redis INCR 10 万 1ms Redis
号段模式 100 万 0.1ms 数据库
Snowflake 400 万 0.01ms

10. 阿里 P7 加分项

深度理解

  • 理解 Snowflake 的时间戳回拨问题的根本原因
  • 理解号段模式的双缓冲机制和 CAS 原理

实战经验

  • 有处理 Snowflake 时钟回拨的线上故障经验
  • 有号段模式宕机丢 ID 的解决方案
  • 有分布式 ID 迁移经验(如从数据库自增迁移到 Snowflake

架构能力

  • 能设计支持多业务类型的分布式 ID 系统
  • 能设计分布式 ID 的监控和告警体系
  • 有分布式 ID 容灾方案(多机房容灾)

技术选型

  • 能根据业务特点选择合适的方案
  • 了解美团 Leaf、百度 UidGenerator 等开源方案
  • 有自研分布式 ID 生成器的经验