Mysql

关注公众号 jb51net

关闭
首页 > 数据库 > Mysql > MySQL主键ID生成方案

MySQL分库分表后主键ID生成的八种方案

作者:墨夶

当你的MySQL数据库因数据量或并发压力进行分库分表后,主键ID重复会成为系统崩溃的导火索,本文将从底层原理出发,结合真实业务代码,深度解析8种主流主键ID生成方案,助你构建稳定可靠的分库分表系统,需要的朋友可以参考下

分库分表后的主键ID冲突陷阱

当你的MySQL数据库因数据量或并发压力进行分库分表后,主键ID重复会成为系统崩溃的导火索。假设你将订单表拆分为10个分表,每个分表都从1开始自增,最终会出现以下灾难性问题:

本文将从底层原理出发,结合真实业务代码,深度解析8种主流主键ID生成方案,助你构建稳定可靠的分库分表系统。

一、方案一:数据库自增ID + 分段设置

核心思想

通过为每个分表配置不同的起始值步长,确保ID全局唯一。

实现代码

-- 分表1(t_order_0)配置
ALTER TABLE t_order_0 AUTO_INCREMENT = 1; -- 起始值1,步长10

-- 分表2(t_order_1)配置
ALTER TABLE t_order_1 AUTO_INCREMENT = 11; -- 起始值11,步长10

-- 分表3(t_order_2)配置
ALTER TABLE t_order_2 AUTO_INCREMENT = 21; -- 起始值21,步长10

代码注解

Java调用示例

// 根据订单ID计算分表索引
int getTableIndex(Long orderId) {
    return (int)(orderId % 10); // 假设分10个表
}

// 插入订单
void insertOrder(Order order) {
    int tableIndex = getTableIndex(order.getId());
    String tableName = "t_order_" + tableIndex;
    
    // 动态拼接SQL
    String sql = "INSERT INTO " + tableName + "(id, order_no) VALUES (?, ?)";
    jdbcTemplate.update(sql, order.getId(), order.getOrderNo());
}

优缺点分析

优点缺点
实现简单,无需额外组件分表数量固定,扩容困难
性能优秀,直接利用数据库特性高并发下单表自增ID可能耗尽
存储效率高,ID紧凑需手动维护分表配置

二、方案二:UUID全局唯一标识符

核心思想

利用UUID的128位随机性保证全局唯一性。

实现代码

-- 创建订单表(主键字段为UUID)
CREATE TABLE t_order (
    id CHAR(36) PRIMARY KEY, -- UUID长度36位
    order_no VARCHAR(50)
);

Java生成UUID

import java.util.UUID;

public class Order {
    private String id = UUID.randomUUID().toString(); // 生成UUID
    private String orderNo;

    // Getter & Setter
}

代码注解

性能优化示例

// 压缩UUID为16字节
public byte[] compressUuid(String uuid) {
    return UUID.fromString(uuid).toString().getBytes(StandardCharsets.UTF_8);
}

优缺点分析

优点缺点
完全随机,无依赖数据库存储空间大(36字节)
适用于分布式系统B+树索引性能差(随机写入)
无需协调生成ID无法直接定位分表

三、方案三:Snowflake算法(Twitter开源)

核心思想

通过时间戳+机器ID+序列号生成64位有序ID。

Java实现代码

public class SnowflakeIdGenerator {
    private final long twepoch = 1288834974657L; // 起始时间戳(2010-11-04)
    private final long workerIdBits = 5L; // 机器ID位数
    private final long datacenterIdBits = 5L; // 数据中心ID位数
    private final long sequenceBits = 12L; // 序列号位数
    
    private final long maxWorkerId = ~(-1L << workerIdBits);
    private final long maxDatacenterId = ~(-1L << datacenterIdBits);
    private final long sequenceMask = ~(-1L << sequenceBits);
    
    private long workerId;
    private long datacenterId;
    private long sequence = 0L;
    private long lastTimestamp = -1L;

    public SnowflakeIdGenerator(long workerId, long datacenterId) {
        if (workerId > maxWorkerId || workerId < 0) {
            throw new IllegalArgumentException(String.format("worker Id can't be greater than %d or less than 0", maxWorkerId));
        }
        if (datacenterId > maxDatacenterId || datacenterId < 0) {
            throw new IllegalArgumentException(String.format("datacenter Id can't be greater than %d or less than 0", maxDatacenterId));
        }
        this.workerId = workerId;
        this.datacenterId = datacenterId;
    }

    public synchronized long nextId() {
        long timestamp = timeGen();
        
        if (timestamp < lastTimestamp) {
            throw new RuntimeException(String.format("时钟回拨: %d ms", lastTimestamp - timestamp));
        }

        if (lastTimestamp == timestamp) {
            sequence = (sequence + 1) & sequenceMask;
            if (sequence == 0) {
                timestamp = tilNextMillis(lastTimestamp);
            }
        } else {
            sequence = 0;
        }

        lastTimestamp = timestamp;
        return (timestamp - twepoch) << (workerIdBits + datacenterIdBits + sequenceBits)
                | (datacenterId << (workerIdBits + sequenceBits))
                | (workerId << sequenceBits)
                | sequence;
    }

    protected long tilNextMillis(long lastTimestamp) {
        long timestamp = timeGen();
        while (timestamp <= lastTimestamp) {
            timestamp = timeGen();
        }
        return timestamp;
    }

    protected long timeGen() {
        return System.currentTimeMillis();
    }
}

代码注解

调用示例

SnowflakeIdGenerator idGenerator = new SnowflakeIdGenerator(1, 1);
long orderId = idGenerator.nextId();
System.out.println("生成的ID: " + orderId);

优缺点分析

优点缺点
全局唯一且有序依赖系统时间(时钟回拨问题)
支持分布式场景需要预先分配机器ID
存储效率高(8字节)最大时间戳限制为69年

四、方案四:Redis原子自增

核心思想

利用Redis的INCR命令生成全局唯一ID。

Redis配置

# 启动Redis
redis-server

# 设置初始值
SET order_id 1000

Java调用示例

import redis.clients.jedis.Jedis;

public class RedisIdGenerator {
    private Jedis jedis = new Jedis("localhost");

    public long generateId() {
        return jedis.incr("order_id"); // 原子自增
    }
}

代码注解

集群模式优化

// Redis Cluster客户端配置
JedisCluster jedisCluster = new JedisCluster(new HostAndPort("192.168.1.101", 6379),
                                             new HostAndPort("192.168.1.102", 6379));

优缺点分析

优点缺点
高性能(每秒百万级QPS)依赖Redis服务稳定性
支持分布式场景单点故障风险(需集群)
简单易用需处理网络延迟

五、方案五:第三方ID生成服务

核心思想

使用独立服务(如滴滴TinyID)生成ID。

TinyID集成示例

// 1. 添加Maven依赖
<dependency>
    <groupId>com.didi</groupId>
    <artifactId>tinyid-client</artifactId>
    <version>1.0.0</version>
</dependency>

// 2. 配置TinyID
TinyIdClient client = new TinyIdClient("http://tinyid-service.com");

// 3. 生成ID
String businessType = "order";
Long id = client.getId(businessType);

代码注解

TinyID服务端配置

CREATE TABLE tinyid (
    id BIGINT PRIMARY KEY,
    business_type VARCHAR(50),
    max_id BIGINT,
    step INT,
    version INT
);

优缺点分析

优点缺点
支持多业务类型需维护独立服务
高可用性(支持集群)依赖网络通信
灵活配置步长学习成本较高

六、方案六:COMB ID(组合ID)

核心思想

时间戳随机数组合,生成有序UUID。

Java实现代码

import java.time.Instant;
import java.util.Random;

public class CombIdGenerator {
    private final Random random = new Random();

    public String generateId() {
        byte[] uuid = new byte[16];
        
        // 前6字节为时间戳
        long timestamp = Instant.now().toEpochMilli();
        for (int i = 0; i < 6; i++) {
            uuid[i] = (byte)(timestamp >> (8 * (5 - i)));
        }
        
        // 后10字节为随机数
        random.nextBytes(uuid);
        
        return bytesToHex(uuid);
    }

    private String bytesToHex(byte[] bytes) {
        StringBuilder sb = new StringBuilder();
        for (byte b : bytes) {
            sb.append(String.format("%02x", b));
        }
        return sb.toString();
    }
}

代码注解

优缺点分析

优点缺点
兼具有序性和随机性实现复杂度高
降低B+树索引碎片依赖时间同步

七、方案七:数据库中间件内置策略

ShardingSphere示例

// 1. 配置ShardingSphere
spring.shardingsphere.sharding.tables.t_order.key-generator.column=order_id
spring.shardingsphere.sharding.tables.t_order.key-generator.type=SNOWFLAKE
spring.shardingsphere.sharding.tables.t_order.key-generator.props.worker.id=123

代码注解

自定义生成器示例

public class CustomKeyGenerator implements KeyGenerator {
    @Override
    public Comparable<?> generateKey() {
        return new SnowflakeIdGenerator(1, 1).nextId();
    }
}

优缺点分析

优点缺点
无缝集成ShardingSphere依赖中间件版本
支持多种算法配置复杂度高

八、方案八:基因法 + Hash分片

核心思想

在ID中嵌入分片基因,直接定位分表。

Java实现代码

public class ShardIdGenerator {
    private static final int SHARD_COUNT = 8; // 8个分表
    private static final int GEN_BITS = 3; // 3位基因(2^3=8)

    public long generateShardId(long baseId, int shardIndex) {
        // 基因掩码:0b00000111
        long mask = (1 << GEN_BITS) - 1;
        
        // 清除低3位基因
        long idWithoutGene = baseId & ~mask;
        
        // 设置新的基因
        return idWithoutGene | (shardIndex & mask);
    }
}

代码注解

分表查询示例

long shardId = 1234567890123456789L;
int shardIndex = (int)(shardId & ((1 << 3) - 1)); // 提取低3位
String tableName = "t_order_" + shardIndex;

优缺点分析

优点缺点
直接定位分表ID修改后需重新计算
无需额外组件分片规则强依赖基因位

如何选择最适合你的方案?

方案适用场景推荐指数
数据库自增+步长小规模分表⭐⭐⭐
UUID分布式系统⭐⭐⭐⭐
Snowflake高并发系统⭐⭐⭐⭐⭐
Redis高性能要求⭐⭐⭐⭐
TinyID多业务场景⭐⭐⭐⭐
COMB ID有序性优先⭐⭐⭐
ShardingSphere中间件集成⭐⭐⭐⭐
基因法Hash分片⭐⭐⭐⭐

最后提醒:选择主键生成方案时,需综合考虑业务规模性能需求运维成本。记住:没有银弹方案,只有最合适的解决方案

代码包结构

MainKeyGenerator/
├── config/
│   └── shard.properties       # 分片配置
├── service/
│   ├── RedisIdService.java    # Redis生成器
│   ├── SnowflakeService.java  # Snowflake实现
│   └── TinyIdService.java     # TinyID客户端
├── model/
│   └── Order.java             # 订单模型
└── util/
    └── ShardUtil.java         # 分片工具类

部署建议

“主键ID是分库分表系统的命脉,选对方案,你的系统才能稳如泰山!”

以上就是MySQL分库分表后主键ID生成的八种方案的详细内容,更多关于MySQL主键ID生成方案的资料请关注脚本之家其它相关文章!

您可能感兴趣的文章:
阅读全文