分布式ID原理与使用详解

分布式 ID 原理与使用(一份能直接落地的工程指南)

目标:在高并发、多实例、多机房 场景下,生成全局唯一、趋势递增、高性能 的 ID。

本文从原理 → 方案对比 → 代码实现 → 工程坑位,一步到位。


1. 为什么需要分布式 ID?

在单机时代:

  • MySQL AUTO_INCREMENT 足够好用

但一旦进入分布式系统:

  • 多库多表
  • 多实例并发写
  • 异步消息 / 跨系统调用

你会立刻遇到问题:

  • ❌ ID 冲突
  • ❌ 无法保证顺序
  • ❌ 数据迁移/合库困难
  • ❌ 数据库成为性能瓶颈

所以:ID 必须从"数据库附属品"升级为"基础设施"


2. 一个"好 ID"通常要满足什么?

不是所有系统都需要全部,但你要知道你在取舍什么。

常见诉求:

  • 全局唯一
  • 高性能 / 高并发
  • 趋势递增(对数据库、索引、分页友好)
  • 长度可控(最好 64bit long)
  • 不依赖单点
  • 可跨机房

⚠️ 注意:

绝对递增 vs 高可用 通常是冲突的,只能选一个"更重要"。


3. 主流分布式 ID 方案总览

方案 是否唯一 趋势递增 性能 依赖 典型问题
UUID 太长、无序
数据库号段 DB DB 压力
Redis INCR Redis 回退风险
Snowflake 极高 本地 时钟回拨
Leaf / Uid 极高 DB 架构复杂

4. UUID:能用,但很多时候你不该用

java 复制代码
UUID.randomUUID().toString();

问题:

  • 长度大(36 字符)
  • 无序,索引性能差

建议:

  • ❌ 不做数据库主键
  • ✅ 适合幂等键、业务唯一标识

5. 数据库号段(Segment)------最稳妥的方案

原理

  • DB 只负责分配区间
  • 服务内存自增

表结构

sql 复制代码
CREATE TABLE id_segment (
  biz_type VARCHAR(50) PRIMARY KEY,
  max_id BIGINT NOT NULL,
  step INT NOT NULL
);

简化实现

java 复制代码
class SegmentIdGenerator {
    private long current;
    private long max;

    synchronized long nextId() {
        if (current > max) {
            loadFromDb();
        }
        return current++;
    }
}

优点:

  • 稳定
  • 趋势递增

缺点:

  • 实现稍复杂

6. Redis INCR:简单但要清楚风险

shell 复制代码
INCR order:id

优点:

  • 实现简单
  • 性能高

风险:

  • Redis 宕机
  • AOF 回放导致 ID 回退

适合:

  • 中小系统
  • 非强一致 ID

7. Snowflake(雪花算法)

64 位结构

复制代码
时间戳 | 机器号 | 序列号

Java 示例

java 复制代码
public synchronized long nextId() {
    long ts = System.currentTimeMillis();
    if (ts == lastTs) {
        seq = (seq + 1) & 4095;
    } else {
        seq = 0;
    }
    lastTs = ts;
    return (ts << 22) | seq;
}

⚠️ 一定要处理 时钟回拨


8. 选型建议

  • 订单 / 支付:数据库号段 or Leaf
  • 高并发日志:Snowflake
  • 内部系统:Redis INCR
  • 非 DB ID:UUID

9. 面试 30 秒总结

分布式 ID 的目标是全局唯一、高性能、趋势递增。UUID 简单但无序;数据库号段通过区间分配最稳定;Snowflake 性能高但要处理时钟回拨;Redis INCR 简单但存在回退风险。


10. 高频坑

  1. UUID 当主键
  2. Snowflake 无回拨保护
  3. Redis 单点
  4. 后期改 ID 方案

11.Segment实现分布式ID示例

1)表结构(MySQL)
sql 复制代码
CREATE TABLE id_segment (
  biz_type   VARCHAR(50)  NOT NULL PRIMARY KEY COMMENT '业务标识,比如 order/pay/refund',
  max_id     BIGINT       NOT NULL COMMENT '当前已分配到的最大ID',
  step       INT          NOT NULL COMMENT '每次分配的号段步长',
  version    BIGINT       NOT NULL DEFAULT 0 COMMENT '乐观锁版本号',
  update_time TIMESTAMP   NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB;

-- 初始化:order 每次取 1000 个
INSERT INTO id_segment(biz_type, max_id, step, version)
VALUES ('order', 0, 1000, 0);

解释:

max_id:DB 里记录"已经发出去的最大值"

每次取段:把 max_id 增加 step,返回区间 (oldMax+1 ~ newMax)

version:CAS 更新,防并发发重段

2)Mapper(MyBatis / MyBatis-Plus 都行)

SQL(核心是"先查后 CAS 更新")

sql 复制代码
-- 1. 查当前段信息
SELECT biz_type, max_id, step, version
FROM id_segment
WHERE biz_type = #{bizType};

-- 2. CAS 更新(乐观锁)
UPDATE id_segment
SET max_id = max_id + step,
    version = version + 1
WHERE biz_type = #{bizType}
  AND version = #{version};

Mapper 接口示例

java 复制代码
public interface IdSegmentMapper {
    IdSegmentDO selectForUpdate(String bizType); // 实际不需要for update,靠 version CAS
    int updateMaxIdCas(String bizType, long version);
}

不用 SELECT ... FOR UPDATE,因为那会把并发打回串行;Segment 的优势就是让 DB 轻松。

3)核心模型:SegmentBuffer(双 Buffer + 预加载)

目标:用号段 A 的时候,后台预取号段 B,A 快用完时无缝切到 B。

java 复制代码
import java.util.concurrent.atomic.AtomicLong;

public class Segment {
    volatile long start; // inclusive
    volatile long end;   // inclusive
    final AtomicLong cur = new AtomicLong(0);

    void reset(long start, long end) {
        this.start = start;
        this.end = end;
        this.cur.set(start);
    }

    long next() {
        long v = cur.getAndIncrement();
        return v <= end ? v : -1;
    }

    long remain() {
        return end - cur.get();
    }

    long size() {
        return end - start + 1;
    }
}

public class SegmentBuffer {
    final String bizType;
    final Segment[] segments = {new Segment(), new Segment()};
    volatile int currentIndex = 0;

    // 是否正在加载 next 段
    volatile boolean loadingNext = false;

    SegmentBuffer(String bizType) { this.bizType = bizType; }
}
4)发号逻辑(核心服务)
4.1 DB 拉号段(CAS 重试)
java 复制代码
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
public class SegmentAllocator {

    private final IdSegmentMapper mapper;

    public SegmentAllocator(IdSegmentMapper mapper) {
        this.mapper = mapper;
    }

    @Transactional
    public IdRange allocate(String bizType) {
        for (int i = 0; i < 10; i++) { // CAS 重试
            IdSegmentDO row = mapper.selectByBizType(bizType);
            if (row == null) throw new IllegalArgumentException("bizType not found: " + bizType);

            int updated = mapper.updateMaxIdCas(bizType, row.getVersion());
            if (updated == 1) {
                long oldMax = row.getMaxId();
                long newMax = oldMax + row.getStep();
                return new IdRange(oldMax + 1, newMax);
            }
        }
        throw new IllegalStateException("allocate segment failed after retries, bizType=" + bizType);
    }

    public record IdRange(long start, long end) {}
}
4.2 ID 生成器(内存自增 + 预加载 + 切段)
java 复制代码
import java.util.concurrent.*;
import java.util.concurrent.ConcurrentHashMap;

public class SegmentIdGenerator {

    private final SegmentAllocator allocator;
    private final ConcurrentHashMap<String, SegmentBuffer> cache = new ConcurrentHashMap<>();
    private final ExecutorService preloadPool =
            Executors.newFixedThreadPool(Math.max(2, Runtime.getRuntime().availableProcessors() / 2));

    // 触发预加载阈值:当前段剩余 < 10%
    private final double preloadFactor = 0.10;

    public SegmentIdGenerator(SegmentAllocator allocator) {
        this.allocator = allocator;
    }

    public long nextId(String bizType) {
        SegmentBuffer buf = cache.computeIfAbsent(bizType, this::initBuffer);

        while (true) {
            Segment curSeg = buf.segments[buf.currentIndex];
            long id = curSeg.next();
            if (id != -1) {
                maybePreloadNext(buf, curSeg);
                return id;
            }
            // 当前段用完,切换到 next 段
            switchSegment(buf);
        }
    }

    private SegmentBuffer initBuffer(String bizType) {
        SegmentBuffer buf = new SegmentBuffer(bizType);
        // 同步加载两段,启动即热
        fillSegment(buf.segments[0], bizType);
        fillSegment(buf.segments[1], bizType);
        buf.currentIndex = 0;
        return buf;
    }

    private void fillSegment(Segment seg, String bizType) {
        SegmentAllocator.IdRange r = allocator.allocate(bizType);
        seg.reset(r.start(), r.end());
    }

    private void maybePreloadNext(SegmentBuffer buf, Segment curSeg) {
        long remain = curSeg.remain();
        if (remain <= 0) return;

        if (remain < curSeg.size() * preloadFactor && !buf.loadingNext) {
            synchronized (buf) {
                if (buf.loadingNext) return;
                buf.loadingNext = true;
                int nextIdx = 1 - buf.currentIndex;

                preloadPool.submit(() -> {
                    try {
                        fillSegment(buf.segments[nextIdx], buf.bizType);
                    } finally {
                        buf.loadingNext = false;
                    }
                });
            }
        }
    }

    private void switchSegment(SegmentBuffer buf) {
        synchronized (buf) {
            int nextIdx = 1 - buf.currentIndex;

            // 如果 next 段还没准备好,就同步加载(兜底)
            Segment nextSeg = buf.segments[nextIdx];
            long probe = nextSeg.cur.get();
            if (probe == 0 || probe > nextSeg.end) { // 粗略判断是否可用
                fillSegment(nextSeg, buf.bizType);
            }

            buf.currentIndex = nextIdx;
        }
    }
}

特点:

绝大多数请求:只是在内存里 AtomicLong++

DB 调用:每 step 次请求才触发一次(比如 step=1000)

预加载:快用完时后台拉下一段,避免"用完瞬间卡一下"

5)怎么在 Spring Boot 里注入使用
java 复制代码
@Configuration
public class IdConfig {

    @Bean
    public SegmentIdGenerator segmentIdGenerator(SegmentAllocator allocator) {
        return new SegmentIdGenerator(allocator);
    }
}

业务里直接用:

java 复制代码
long orderId = segmentIdGenerator.nextId("order");
6)工程级建议(你一定会用到)

step 怎么设?

经验:step = 峰值QPS * 1~5秒

比如订单峰值 2000 QPS → step 2000~10000 都行

step 太小:DB 压力上升

step 太大:重启可能浪费一段(但一般可接受)

多实例会不会冲突?

不会。因为每次分段是 DB CAS 更新 version,天然全局唯一。

重启会不会丢号?

会"浪费"一段,但不会重复。大多数系统都接受(ID 不需要连续)。

监控你该看什么?

分配号段次数/分钟(DB 压力指标)

预加载耗时

同步兜底加载次数(说明预加载不够快/step太小)

相关推荐
源代码•宸9 天前
goframe框架签到系统项目开发(分布式 ID 生成器、雪花算法、抽离业务逻辑到service层)
经验分享·分布式·mysql·算法·golang·雪花算法·goframe
无心水1 个月前
【分布式利器:分布式ID】7、分布式数据库方案:TiDB/OceanBase全局ID实战
数据库·分布式·tidb·oceanbase·分库分表·分布式id·分布式利器
无心水1 个月前
【分布式利器:分布式ID】6、中间件方案:Redis/ZooKeeper分布式ID实现
redis·分布式·zookeeper·中间件·分库分表·分布式id·分布式利器
无心水1 个月前
【分布式利器:分布式ID】5、UUID/GUID方案:无依赖实现,优缺点与场景选型
分布式·分库分表·uuid·分布式id·水平分库·分布式利器·guid
wáng bēn2 个月前
Pig4Cloud微服务分布式ID生成:Snowflake算法深度集成指南
微服务·雪花算法·twitter·snowflake·pig4cloud
不见长安在2 个月前
分布式ID
java·分布式·分布式id
Java爱好狂.2 个月前
分布式ID|从源码角度深度解析美团Leaf双Buffer优化方案
java·数据库·分布式·分布式id·es·java面试·java程序员
卷心菜不卷Iris5 个月前
第4章唯一ID生成器——4.5 美团点评开源方案Leaf
雪花算法·美团·分布式系统·leaf·分布式唯一id·点评
啾啾Fun7 个月前
Java面试题:分布式ID时钟回拨怎么处理?序列号耗尽了怎么办?
java·分布式·分布式id·八股