分库分表后,主键 ID 如何优雅生成?

前言

在分布式系统中,分库分表是解决"数据量过大、单库单表性能瓶颈"的核心方案。但分库分表后,原本单表依赖的"自增主键(AUTO_INCREMENT)"彻底失效------多个分表若各自自增,会出现主键重复;跨库跨表查询时,重复主键会导致数据混乱。因此,"生成全局唯一、有序且高性能的主键 ID",成为分库分表落地的关键问题。

本文将拆解 5 种主流的主键生成方案,结合代码示例分析其优缺点,帮你找到适合业务的"优雅方案"。

一、为什么不能用"自增主键"?

先明确痛点:单库单表时,AUTO_INCREMENT 能保证主键唯一且有序,但分库分表后完全不适用:

• 主键重复风险:比如分表 2 张(order_1order_2),若两张表都从 1 开始自增,会同时生成 ID=1 的订单记录,跨表查询时无法区分;

• 无法全局排序:自增主键仅在单表内有序,跨表时 ID 顺序混乱(比如 order_1 最大 ID=100,order_2 最大 ID=90),无法通过 ID 判断数据插入时间。

因此,分库分表需要的主键 ID,必须满足 3 个核心要求:全局唯一、趋势有序(可选,便于索引优化)、高性能(生成速度快,无单点瓶颈)

二、5 种主键生成方案:代码示例 + 优缺点分析

方案 1:UUID/GUID(最简单但不推荐)

原理:基于 UUID(Universally Unique Identifier)标准,通过 MAC 地址、时间戳、随机数等生成 128 位的全局唯一字符串(如 550e8400-e29b-41d4-a716-446655440000)。 Java 中可直接通过 java.util.UUID 类生成,无需额外依赖。

代码示例

java 复制代码
import java.util.UUID;

public class UuidGenerator {
    public static String generateId() {
        // 生成标准 UUID(带横杠),可通过 replace("-", "") 去除横杠
        UUID uuid = UUID.randomUUID();
        return uuid.toString().replace("-", ""); // 输出示例:550e8400e29b41d4a716446655440000
    }

    public static void main(String[] args) {
        System.out.println("生成的UUID主键:" + generateId());
    }
}

优缺点

优点:实现简单(一行代码)、无网络开销、无单点风险;

缺点

1.无序:UUID 是随机字符串,无法通过 ID 判断插入时间,会导致数据库索引(如 MySQL 聚簇索引)频繁分裂,性能下降;

2.占用空间大:128 位字符串比 64 位整数(Long 类型)占用更多存储,且查询效率低。

适用场景:对 ID 有序性无要求、数据量小的非核心业务(如日志表、临时表)。

方案 2:数据库自增表(简单但有瓶颈)

原理 :单独创建一张"主键生成表",通过单库单表的自增主键,为所有分库分表提供全局唯一 ID。例如创建 id_generator 表,每次需要 ID 时,插入一条空记录并返回自增 ID。

代码示例(MySQL 表 + Java 实现)

1.先创建主键生成表:

sql 复制代码
CREATE TABLE `id_generator` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '全局唯一ID',
  `biz_type` varchar(50) NOT NULL COMMENT '业务类型(如order、user)',
  `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_biz_type` (`biz_type`) COMMENT '避免同一业务重复插入'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT '全局主键生成表';

2.Java 代码调用(通过 MyBatis 操作):

java 复制代码
// Mapper 接口
public interface IdGeneratorMapper {
    // 插入一条业务记录,获取自增ID
    @Insert("INSERT INTO id_generator (biz_type) VALUES (#{bizType}) ON DUPLICATE KEY UPDATE id = id + 1")
    @SelectKey(statement = "SELECT 1608051", keyProperty = "id", resultType = Long.class, before = false)
    void generateId(@Param("bizType") String bizType, @Param("id") Long id);
}

// 服务层实现
@Service
public class IdGeneratorService {
    @Autowired
    private IdGeneratorMapper idGeneratorMapper;

    // 生成指定业务的全局ID
    public Long getGlobalId(String bizType) {
        Map<String, Object> param = new HashMap<>();
        param.put("bizType", bizType);
        param.put("id", 0L); // 初始值,会被覆盖
        
        idGeneratorMapper.generateId(param);
        return (Long) param.get("id");
    }

    // 测试:生成订单业务的全局ID
    public static void main(String[] args) {
        IdGeneratorService service = new IdGeneratorService();
        Long orderId = service.getGlobalId("order");
        System.out.println("订单全局ID:" + orderId); // 输出示例:100001
    }
}

优缺点

优点:实现简单、ID 有序(便于索引优化)、全局唯一;

缺点

1.单点瓶颈:所有业务的 ID 生成都依赖这张表,高并发场景下(如秒杀订单)会导致数据库压力过大,甚至宕机;

2.扩展性差:若主键生成表所在数据库挂了,整个系统无法生成 ID,可用性低。

适用场景:中小规模业务、并发量不高(如日均订单 10 万以下)的场景。

方案 3:号段模式(缓解数据库压力)

原理:在"数据库自增表"基础上优化,一次性从数据库获取一段 ID(如 1000 个),缓存在本地服务中,后续生成 ID 直接从缓存中取,用完再向数据库申请新号段。例如:第一次申请 1-1000,用完后申请 1001-2000。

代码示例(基于 Redis 缓存号段)

1.数据库表结构不变(复用方案 2 的 id_generator 表);

2.Java 代码实现(结合 Redis 缓存号段):

java 复制代码
@Service
public class SegmentIdGeneratorService {
    @Autowired
    private IdGeneratorMapper idGeneratorMapper;
    @Autowired
    private RedisTemplate<String, String> redisTemplate;

    // 号段长度(每次从数据库申请1000个ID)
    private static final int SEGMENT_SIZE = 1000;
    // Redis键前缀(存储当前业务的号段范围,如 order:current=1000,order:max=2000)
    private static final String REDIS_KEY_CURRENT = "%s:segment:current";
    private static final String REDIS_KEY_MAX = "%s:segment:max";

    public Long getGlobalId(String bizType) {
        // 1. 先从Redis获取当前号段
        String currentKey = String.format(REDIS_KEY_CURRENT, bizType);
        String maxKey = String.format(REDIS_KEY_MAX, bizType);
        
        Long current = redisTemplate.opsForValue().increment(currentKey, 1);
        Long max = Long.valueOf(redisTemplate.opsForValue().get(maxKey));

        // 2. 若当前号段已用完(current > max),向数据库申请新号段
        if (current == null || max == null || current > max) {
            synchronized (bizType.intern()) { // 防止并发重复申请号段
                // 二次检查(避免锁等待期间已被其他线程申请)
                current = redisTemplate.opsForValue().increment(currentKey, 1);
                max = Long.valueOf(redisTemplate.opsForValue().get(maxKey));
                if (current == null || max == null || current > max) {
                    // 从数据库申请新号段(每次加SEGMENT_SIZE)
                    Long dbMaxId = getDbMaxId(bizType);
                    Long newMax = dbMaxId + SEGMENT_SIZE;
                    // 更新Redis中的号段
                    redisTemplate.opsForValue().set(currentKey, "1"); // 重置current为1
                    redisTemplate.opsForValue().set(maxKey, newMax.toString());
                    // 重新获取当前ID
                    current = redisTemplate.opsForValue().increment(currentKey, 1);
                    max = newMax;
                }
            }
        }

        // 3. 生成最终ID(数据库当前最大ID + 缓存中的current)
        Long dbMaxId = getDbMaxId(bizType);
        return dbMaxId + current - 1;
    }

    // 从数据库获取当前业务的最大ID
    private Long getDbMaxId(String bizType) {
        Map<String, Object> param = new HashMap<>();
        param.put("bizType", bizType);
        param.put("id", 0L);
        idGeneratorMapper.generateId(param);
        return (Long) param.get("id");
    }

    // 测试
    public static void main(String[] args) {
        SegmentIdGeneratorService service = new SegmentIdGeneratorService();
        Long orderId = service.getGlobalId("order");
        System.out.println("订单全局ID:" + orderId); // 输出示例:100001(若数据库当前最大ID=100000,current=1)
    }
}

优缺点

•优点:大幅降低数据库压力(1 次数据库请求对应 1000 次 ID 生成)、ID 有序、无重复;

•缺点

1.服务重启丢失号段:若服务宕机,缓存中的未使用号段会丢失(如申请了 1001-2000,只用了 1001-1500,重启后会从 2001 开始,1501-2000 永久丢失),导致 ID 不连续;

2.仍依赖数据库:数据库挂了,无法申请新号段,仍有可用性风险。

适用场景:中高并发场景(如日均订单 10 万-100 万),对 ID 连续性要求不高的业务。

方案 4:雪花算法(Snowflake,分布式首选)

原理:由 Twitter 开源,基于"时间戳 + 机器 ID + 序列号"生成 64 位 Long 类型 ID,结构如下:

•1 位符号位:固定为 0(确保 ID 为正数);

•41 位时间戳:记录毫秒级时间(从指定起始时间开始,可使用约 69 年);

•10 位机器 ID:区分不同服务器(支持 2^10=1024 台机器);

•12 位序列号:同一毫秒内,同一机器可生成 2^12=4096 个 ID(解决同一毫秒内的并发问题)。

代码示例(Java 实现雪花算法)

java 复制代码
public class SnowflakeIdGenerator {
    // 1. 固定参数
    private static final long SIGN_BIT = 1L << 63; // 符号位(固定0,无需使用)
    private static final long TIMESTAMP_BITS = 41L; // 时间戳位数
    private static final long WORKER_ID_BITS = 10L; // 机器ID位数
    private static final long SEQUENCE_BITS = 12L; // 序列号位数

    // 2. 位移参数(计算各部分的偏移量)
    private static final long WORKER_ID_SHIFT = SEQUENCE_BITS; // 机器ID左移12位
    private static final long TIMESTAMP_SHIFT = SEQUENCE_BITS + WORKER_ID_BITS; // 时间戳左移22位

    // 3. 最大值(防止溢出)
    private static final long MAX_WORKER_ID = (1L << WORKER_ID_BITS) - 1; // 机器ID最大为1023
    private static final long MAX_SEQUENCE = (1L << SEQUENCE_BITS) - 1; // 序列号最大为4095

    // 4. 自定义起始时间(2024-01-01 00:00:00,减少时间戳位数占用)
    private static final long START_TIMESTAMP = 1704067200000L;

    // 5. 本地变量(线程安全)
    private final long workerId; // 当前机器ID
    private long lastTimestamp = -1L; // 上一次生成ID的时间戳
    private long sequence = 0L; // 当前序列号

    // 构造器:传入机器ID(需确保全局唯一,可从配置中心获取)
    public SnowflakeIdGenerator(long workerId) {
        if (workerId < 0 || workerId > MAX_WORKER_ID) {
            throw new IllegalArgumentException("机器ID超出范围(0-1023):" + workerId);
        }
        this.workerId = workerId;
    }

    // 生成全局唯一ID(线程安全,加synchronized)
    public synchronized long generateId() {
        long currentTimestamp = System.currentTimeMillis();

        // 1. 防止时间回拨(若当前时间 < 上一次时间,说明时钟回拨,抛出异常)
        if (currentTimestamp < lastTimestamp) {
            throw new RuntimeException("时钟回拨,无法生成ID:" + (lastTimestamp - currentTimestamp) + "ms");
        }

        // 2. 同一毫秒内,序列号自增
        if (currentTimestamp == lastTimestamp) {
            sequence = (sequence + 1) & MAX_SEQUENCE;
            // 3. 序列号用完(同一毫秒生成4096个ID后,阻塞到下一毫秒)
            if (sequence == 0) {
                currentTimestamp = waitNextMillis(lastTimestamp);
            }
        } else {
            // 4. 新的毫秒,重置序列号为0
            sequence = 0L;
        }

        // 5. 更新上一次时间戳
        lastTimestamp = currentTimestamp;

        // 6. 拼接ID:时间戳 + 机器ID + 序列号
        return ((currentTimestamp - START_TIMESTAMP) << TIMESTAMP_SHIFT) 
                | (workerId << WORKER_ID_SHIFT) 
                | sequence;
    }

    // 阻塞到下一毫秒,获取新的时间戳
    private long waitNextMillis(long lastTimestamp) {
        long timestamp = System.currentTimeMillis();
        while (timestamp <= lastTimestamp) {
            timestamp = System.currentTimeMillis();
        }
        return timestamp;
    }

    // 测试(机器ID设为1)
    public static void main(String[] args) {
        SnowflakeIdGenerator generator = new SnowflakeIdGenerator(1);
        for (int i = 0; i < 5; i++) {
            System.out.println("雪花算法生成ID:" + generator.generateId());
        }
        // 输出示例:15468960768001, 15468960768002, 15468960768003...
    }
}

优缺点

•优点

1.高性能:本地生成,无网络开销,单机每秒可生成百万级 ID;

2.全局唯一:通过机器 ID 区分服务器,通过序列号解决同一毫秒并发;

3.趋势有序:时间戳在前,ID 整体按时间递增,便于数据库索引优化;

4.无依赖:不依赖数据库或 Redis,可用性高。

•缺点

1.机器 ID 需全局唯一:需通过配置中心(如 Nacos、ZooKeeper)分配机器 ID,避免重复;

2.时钟回拨风险:若服务器时钟回拨(如手动调整时间),会导致 ID 重复,需通过监控或算法规避(如示例中抛出异常)。

适用场景:高并发分布式系统(如秒杀、电商订单),是目前分库分表场景下的"首选方案"。

方案 5:第三方工具(省心省力)

原理:直接使用成熟的分布式 ID 生成工具,无需重复造轮子,常见工具包括:

•美团 Leaf:基于"号段模式 + 雪花算法",支持两种模式切换,提供可视化管理界面;

•百度 UID Generator:优化雪花算法,支持自定义时间戳、机器 ID 位数,解决雪花算法的机器 ID 上限问题;

•Redis 自增:利用 INCR 命令的原子性生成 ID(如 INCR order:id),适合中小并发场景。

好的,先给大家分享这些吧,觉得有帮助的友友们可以点个一键三连,多多关注我,后续持续分享内容哦

相关推荐
焯7593 小时前
若依微服务遇到的配置问题
java·mybatis·ruoyi
wuxuanok3 小时前
Spring Boot 全局异常处理问题分析与解决方案
java·spring boot·后端
SunnyDays10113 小时前
Java 攻克 PDF 表格数据提取:从棘手挑战到自动化实践
java·提取pdf表格·读取pdf表格数据·pdf表格转csv·导出pdf表格为csv
初学小白...3 小时前
泛型-泛型方法
java·开发语言
LQ深蹲不写BUG3 小时前
深挖三色标记算法的底层原理
java·算法
上官浩仁3 小时前
springboot knife4j 接口文档入门与实战
java·spring boot·spring
bobz9653 小时前
spine leaf 组网架构:leaf 和 spine 之间的链路 mtu 普遍都是 9000
后端
bobz9653 小时前
arp 广播带 vlan id 么?
后端
optimistic_chen4 小时前
【Java EE进阶 --- SpringBoot】Spring IoC
spring boot·后端·spring·java-ee·mvc·loc