MySQL 分布式 ID 方案全解析

分布式 ID 是解决「分库分表 ID 冲突」「自增 ID 上限」「跨节点 ID 唯一」的核心方案,本质是生成全局唯一、有序(可选)、高性能 的 ID。下面从「主流方案对比」「落地实现」「选型建议」三个维度,给出可直接落地的解决方案。


一、主流分布式 ID 方案对比(核心)

先通过表格快速掌握各方案的优缺点和适用场景,避免踩坑:

方案类型 核心原理 优点 缺点 适用场景
雪花算法(Snowflake) 64位长整型:时间戳(41)+机器ID(10)+序列号(12) 高性能(本地生成)、有序、无依赖、容量大 依赖服务器时钟,时钟回拨会导致ID重复 绝大多数业务(推荐):订单、用户、交易表
数据库分段 ID 按业务/机器分配 ID 区间(如1-1000万) 实现简单、有序、可追溯 依赖数据库,性能受限于DB,扩容复杂 中小规模业务、对性能要求不高的场景
UUID/GUID 基于网卡MAC+时间戳+随机数生成36位字符串 完全去中心化、实现极简单、无上限 字符串索引性能差、无序、占用空间大 非核心业务:日志、临时表、关联表
数据库自增 ID 改造 单库单表生成自增 ID,分库分表时设置步长 兼容原有自增逻辑、有序、易理解 性能瓶颈在主库、扩容麻烦、有单点风险 从单库迁移到分库分表的过渡场景
号段模式(美团Leaf) 预生成 ID 号段缓存到本地,用完再申请 性能高(缓存)、有序、可扩容、无时钟依赖 实现稍复杂、依赖中间件(如Redis/DB) 高并发核心业务:电商订单、支付流水
Redis 自增 ID 利用 Redis 的 INCR/INCRBY 原子性生成 高性能、原子性、无锁 依赖Redis、断电易丢失(需持久化)、无序 高频写场景:秒杀、库存、计数器

二、核心方案落地实现(可直接用)

方案 1:雪花算法(Snowflake)—— 推荐首选

1. 核心结构(64位长整型)

雪花算法生成的 ID 是一个 BIGINT 类型的数字,结构如下(可自定义调整位数):

0(符号位) + 41位时间戳(毫秒) + 10位机器ID + 12位序列号
  • 符号位:固定 0,保证 ID 为正数;
  • 时间戳:41位可表示约 69 年(2^41-1 毫秒),足够覆盖业务周期;
  • 机器ID:10位可部署 1024 个节点(分布式场景不冲突);
  • 序列号:12位每毫秒可生成 4096 个 ID,满足高并发。

2. 落地实现(Java 示例,MySQL 可直接存储)

雪花算法无需依赖 MySQL,由应用层生成后插入数据库,示例代码(简化版):

public class SnowflakeIdGenerator {
    // 起始时间戳(2024-01-01 00:00:00),可自定义
    private final static long START_TIMESTAMP = 1704067200000L;
    // 机器ID位数(10位)
    private final static long MACHINE_BIT = 10;
    // 序列号位数(12位)
    private final static long SEQUENCE_BIT = 12;

    // 机器ID最大值(1023)
    private final static long MAX_MACHINE_NUM = ~(-1L << MACHINE_BIT);
    // 序列号最大值(4095)
    private final static long MAX_SEQUENCE = ~(-1L << SEQUENCE_BIT);

    // 位移:时间戳左移22位,机器ID左移12位
    private final static long MACHINE_LEFT = SEQUENCE_BIT;
    private final static long TIMESTAMP_LEFT = SEQUENCE_BIT + MACHINE_BIT;

    private long machineId; // 机器ID
    private long sequence = 0L; // 序列号
    private long lastTimestamp = -1L; // 上一次生成ID的时间戳

    // 构造函数:传入机器ID(0-1023)
    public SnowflakeIdGenerator(long machineId) {
        if (machineId > MAX_MACHINE_NUM || machineId < 0) {
            throw new IllegalArgumentException("机器ID超出范围");
        }
        this.machineId = machineId;
    }

    // 生成下一个ID
    public synchronized long nextId() {
        long currTimestamp = System.currentTimeMillis();
        // 时钟回拨检查
        if (currTimestamp < lastTimestamp) {
            throw new RuntimeException("时钟回拨,拒绝生成ID");
        }
        // 同一毫秒,序列号自增
        if (currTimestamp == lastTimestamp) {
            sequence = (sequence + 1) & MAX_SEQUENCE;
            // 同一毫秒序列号用完,等待下一毫秒
            if (sequence == 0L) {
                currTimestamp = getNextMill();
            }
        } else {
            // 不同毫秒,序列号重置为0
            sequence = 0L;
        }
        lastTimestamp = currTimestamp;
        // 拼接ID
        return (currTimestamp - START_TIMESTAMP) << TIMESTAMP_LEFT // 时间戳部分
                | machineId << MACHINE_LEFT // 机器ID部分
                | sequence; // 序列号部分
    }

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

    // 测试
    public static void main(String[] args) {
        SnowflakeIdGenerator generator = new SnowflakeIdGenerator(1); // 机器ID=1
        for (int i = 0; i < 10; i++) {
            long id = generator.nextId();
            System.out.println("生成的分布式ID:" + id);
            // 插入MySQL:直接将id作为主键插入BIGINT类型字段
            // INSERT INTO order (id, order_no) VALUES (id, 'xxx');
        }
    }
}

3. MySQL 适配

  • 表字段类型设置为 BIGINT UNSIGNED(避免溢出);
  • 主键索引直接建在该字段上,性能与自增 ID 一致。

方案 2:数据库分段 ID(简单易落地)

1. 核心思路

创建一个「ID 分配表」,按业务分配 ID 区间(如订单表分配 1-1000 万,用户表分配 1001 万-2000 万),应用层获取区间后本地生成 ID,用完再申请新区间。

2. 落地实现

-- 1. 创建ID分配表
CREATE TABLE id_generator (
    id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
    biz_type VARCHAR(32) NOT NULL COMMENT '业务类型(如order/user)',
    max_id BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '当前最大ID',
    step INT NOT NULL DEFAULT 10000 COMMENT '步长(每次申请1万个ID)',
    create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    PRIMARY KEY (id),
    UNIQUE KEY uk_biz_type (biz_type)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

-- 2. 初始化业务ID(订单表)
INSERT INTO id_generator (biz_type, max_id, step) VALUES ('order', 0, 10000);

-- 3. 申请ID区间的存储过程(原子操作,避免并发冲突)
DELIMITER //
CREATE PROCEDURE apply_id(IN biz_type VARCHAR(32), OUT start_id BIGINT, OUT end_id BIGINT)
BEGIN
    -- 行锁:避免并发申请同一业务的ID
    UPDATE id_generator 
    SET max_id = max_id + step 
    WHERE biz_type = biz_type;
    
    -- 查询申请到的区间
    SELECT max_id - step + 1, max_id INTO start_id, end_id 
    FROM id_generator 
    WHERE biz_type = biz_type;
END //
DELIMITER ;

-- 4. 测试:申请订单表的ID区间
CALL apply_id('order', @start_id, @end_id);
SELECT @start_id AS 起始ID, @end_id AS 结束ID;
-- 输出:起始ID=1,结束ID=10000(下次申请为10001-20000)

3. 应用层逻辑

  • 调用存储过程获取 ID 区间(如 1-10000);
  • 本地维护当前 ID(从 1 到 10000 自增);
  • 用完 10000 后,再次调用存储过程申请新区间。

方案 3:UUID 简化版(最快落地)

如果对性能要求不高,可直接用 MySQL 内置函数生成数字型 UUID,避免字符串索引性能问题:

-- 1. 生成64位数字UUID(UUID_SHORT())
SELECT UUID_SHORT(); -- 输出示例:923957845781234567

-- 2. 插入数据时直接使用
INSERT INTO log (id, content) VALUES (UUID_SHORT(), '操作日志');

-- 3. 表字段设置
CREATE TABLE log (
    id BIGINT UNSIGNED NOT NULL DEFAULT UUID_SHORT(),
    content VARCHAR(255) NOT NULL,
    create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    PRIMARY KEY (id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

⚠️ 注意:UUID_SHORT() 生成的 ID 无序,大批量插入会导致索引碎片,不建议用于核心业务表。

方案 4:Redis 自增 ID(高并发场景)

利用 Redis 的 INCR 原子性生成 ID,适合秒杀、库存等高频写场景:

# 1. Redis 命令(原子自增)
127.0.0.1:6379> INCR order_id # 每次调用+1,返回新ID
(integer) 1

# 2. 设置初始值(可选)
127.0.0.1:6379> SET order_id 10000 # 从10000开始自增
OK
// Java 示例(Jedis)
public class RedisIdGenerator {
    private Jedis jedis;
    private String key;

    public RedisIdGenerator(Jedis jedis, String key) {
        this.jedis = jedis;
        this.key = key;
    }

    public long nextId() {
        return jedis.incr(key);
    }

    // 测试
    public static void main(String[] args) {
        Jedis jedis = new Jedis("127.0.0.1", 6379);
        RedisIdGenerator generator = new RedisIdGenerator(jedis, "order_id");
        long id = generator.nextId();
        System.out.println("Redis生成的ID:" + id);
    }
}

三、选型建议(避坑指南)

1. 优先选雪花算法的场景

  • 核心业务(订单、用户、交易);
  • 分库分表场景;
  • 要求 ID 有序(便于排序/分页);
  • 高并发(每秒万级以上)。

2. 选数据库分段 ID 的场景

  • 中小规模业务;
  • 团队技术栈以 MySQL 为主,不想引入额外组件;
  • 要求 ID 可追溯、可人工调整。

3. 选 UUID 的场景

  • 非核心业务(日志、临时表);
  • 完全去中心化,不想依赖任何中间件;
  • 对 ID 有序性、性能无要求。

4. 选 Redis 自增 ID 的场景

  • 高频写场景(秒杀、库存);
  • 临时计数器(如活动参与人数);
  • 允许 ID 无序。

四、避坑要点

  1. 雪花算法时钟回拨:可通过「缓存最近生成的 ID」「时钟同步」「降级方案」解决;
  2. 数据库分段 ID 并发冲突:必须用行锁(UPDATESELECT)或乐观锁;
  3. ID 类型统一:MySQL 中统一用 BIGINT UNSIGNED,避免溢出;
  4. 避免过度设计:中小业务优先用雪花算法或数据库分段 ID,无需引入复杂中间件。

总结

  1. 首选方案:雪花算法(本地生成、高性能、有序,适配绝大多数业务);
  2. 简易方案:数据库分段 ID(依赖 MySQL,实现简单)或 UUID(零依赖,非核心业务);
  3. 高并发场景:Redis 自增 ID 或美团 Leaf 号段模式;
  4. 核心原则:优先保证 ID 全局唯一,其次考虑性能、有序性、可维护性。
posted @ 2026-03-12 16:34  七星6609  阅读(1)  评论(0)    收藏  举报