本文基于 MyBatis-Plus 最新稳定版本 3.5.16,全面解析主键生成策略的使用方法、适用场景及最佳实践。

一、主键生成策略概览

MyBatis-Plus 提供了灵活且强大的主键生成策略,通过 IdType 枚举类进行配置。合理的选型不仅能保证数据唯一性,还能显著提升系统性能和可扩展性。

1.1 核心枚举值一览

策略类型 适用场景 主键类型 数据库要求 优缺点分析
AUTO 单库单表、简单业务 Long/Integer 需支持自增 ✅ 简单高效;❌ 分布式场景受限
ASSIGN_ID 分布式系统、高并发 Long/String 无特殊要求 ✅ 全局唯一有序;❌ 依赖系统时钟
ASSIGN_UUID 跨系统唯一标识 String 无特殊要求 ✅ 无需依赖数据库;❌ 无序、占用空间大
INPUT Oracle序列/业务自定义ID 任意类型 需序列支持 ✅ 灵活可控;❌ 需手动维护唯一性
NONE 跟随全局配置 任意类型 无特殊要求 ✅ 配置灵活;❌ 未配置时需手动赋值

二、默认策略与优先级规则

2.1 默认主键策略

MyBatis-Plus 3.5.16 的默认主键生成策略为 IdType.ASSIGN_ID(雪花算法)。

生效条件:

  • 实体类主键字段为 LongString 类型
  • 未显式配置 @TableId(type = ...) 注解

实现原理:
使用 DefaultIdentifierGenerator 生成 64 位 Snowflake ID:

  • 41 位时间戳(毫秒级,可用约 69 年)
  • 10 位机器码(支持 1024 台机器)
  • 12 位序列号(每毫秒内支持 4096 个 ID)

数据库字段要求:

  • BIGINT(对应 Long)
  • VARCHAR(19)(对应 String)
@Data
@TableName("user")
public class User {
    // 默认使用 ASSIGN_ID(雪花算法),无需显式配置
    private Long id;
    private String name;
}

2.2 策略优先级

配置优先级遵循以下规则(从高到低):

局部注解(@TableId) > 全局配置 > 默认策略(ASSIGN_ID)

局部覆盖示例:

@TableId(type = IdType.AUTO)  // 优先级最高,覆盖全局配置
private Long id;

全局配置示例:

# application.yml
mybatis-plus:
  global-config:
    db-config:
      id-type: assign_uuid  # 全局改为UUID策略

三、各策略详解与实战

3.1 AUTO:数据库自增策略

适用场景

  • 单库单表的简单业务
  • 对主键连续性有要求的场景
  • 数据库支持自增功能(MySQL、SQLServer 等)

配置步骤

数据库表设计:

CREATE TABLE user (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,
    username VARCHAR(50),
    age INT
);

实体类配置:

@Data
@TableName("user")
public class User {
    @TableId(type = IdType.AUTO)
    private Long id;
    private String username;
    private Integer age;
}

使用示例

// 插入数据时无需设置id,数据库自动生成
User user = new User();
user.setUsername("test_user");
user.setAge(25);
userMapper.insert(user);

// 插入后可直接获取数据库生成的id
Long generatedId = user.getId();
System.out.println("自增主键:" + generatedId);

注意事项

  • 数据库必须支持自增(如 MySQL 的 AUTO_INCREMENT
  • 主键类型需为 LongInteger
  • 分布式场景下多库实例无法保证全局唯一
  • 插入前无法获取主键值,需插入后通过查询获取

3.2 ASSIGN_ID:雪花算法(推荐)

适用场景

  • 分布式系统、微服务架构
  • 分库分表场景
  • 需要插入前获取主键值的业务
  • 高并发、大数据量场景

核心原理

雪花算法生成的 64 位 Long 型 ID 结构:

0 | 0000000000 0000000000 0000000000 0000000000 0 | 0000000000 | 000000000000
↑                41位时间戳                      ↑    10位机器码   ↑    12位序列号
符号位

配置方式

方式一:实体类注解(推荐)

@Data
@TableName("order")
public class Order {
    // 显式声明雪花算法(也可省略,因为这是默认策略)
    @TableId(type = IdType.ASSIGN_ID)
    private Long id;
    private Long userId;
    private String orderNo;
}

方式二:全局配置

# application.yml
mybatis-plus:
  global-config:
    db-config:
      id-type: assign_id

使用示例

Order order = new Order();
order.setUserId(1001L);
order.setOrderNo("ORD20260210001");

// 插入时无需设置id,MyBatis-Plus自动生成雪花ID
orderMapper.insert(order);

// 插入前主键已生成,可直接获取
System.out.println("雪花算法生成的ID:" + order.getId());
// 输出示例:1765000000000000001

自定义机器码(可选)

在分布式环境中,如果节点较多(超过 1024 个)或需手动指定机器码:

@Configuration
public class MyBatisPlusConfig {
    
    @Bean
    public IdentifierGenerator identifierGenerator() {
        return new CustomIdentifierGenerator();
    }
    
    static class CustomIdentifierGenerator implements IdentifierGenerator {
        @Override
        public Number nextId(Object entity) {
            // 自定义机器码:数据中心ID + 机器ID
            long dataCenterId = 1L;  // 数据中心ID(0-31)
            long workerId = 1L;       // 机器ID(0-31)
            
            // 这里简化实现,实际应使用完整的雪花算法
            long timestamp = System.currentTimeMillis();
            long sequence = 0L;
            
            return ((timestamp - 1288834974657L) << 22) 
                | (dataCenterId << 17) 
                | (workerId << 12) 
                | sequence;
        }
    }
}

注意事项

  • 主键类型需为 LongString(存储为字符串形式的 Long 值)
  • 依赖系统时钟,若时钟回拨可能导致 ID 冲突(生产环境需保证服务器时钟同步)
  • 生成的 ID 趋势递增,索引效率高
  • 插入前可获取主键值,便于后续业务逻辑处理

3.3 ASSIGN_UUID:UUID 策略

适用场景

  • 需要字符串类型主键的场景
  • 对有序性无要求的业务
  • 跨系统数据交换
  • 不依赖数据库特性的场景

配置方式

@Data
@TableName("product")
public class Product {
    // 生成 32 位无中划线的 UUID
    @TableId(type = IdType.ASSIGN_UUID)
    private String id;
    private String productName;
    private BigDecimal price;
}

使用示例

Product product = new Product();
product.setProductName("笔记本电脑");
product.setPrice(new BigDecimal("5999"));

productMapper.insert(product);

// 输出示例:5f8a7b9c1d2e3f4a5b6c7d8e9f0a1b2c
System.out.println("UUID主键:" + product.getId());

注意事项

  • 生成 32 位无中划线的 UUID 字符串
  • 主键类型必须为 String
  • UUID 无序,可能影响数据库索引性能(B+ 树索引对有序值更友好)
  • 占用存储空间大(32 字节 vs Long 的 8 字节)
  • 全局唯一,无需依赖数据库

3.4 INPUT:手动输入策略

适用场景

  • Oracle 数据库使用序列生成主键
  • 业务自定义主键规则(如订单号、用户编号)
  • 从外部系统获取主键值(如分布式 ID 生成器)
  • 需要完全自定义主键生成逻辑

配置方式

Oracle 序列方式:

@Data
@TableName("user")
@KeySequence(value = "SEQ_USER", clazz = Long.class)
public class User {
    // 使用 INPUT 策略,主键值需手动设置
    @TableId(type = IdType.INPUT)
    private Long id;
    private String name;
}

注册序列生成器:

@Configuration
public class MyBatisPlusConfig {
    
    @Bean
    public IKeyGenerator oracleKeyGenerator() {
        return new OracleKeyGenerator();
    }
}

业务自定义 ID 方式:

@Data
@TableName("custom_code")
public class CustomCode {
    @TableId(type = IdType.INPUT)
    private String code;
    private String name;
}

使用示例

// 方式一:Oracle 序列
User user = new User();
user.setId(sequenceGenerator.nextId("SEQ_USER"));  // 调用序列
user.setName("Alice");
userMapper.insert(user);

// 方式二:业务自定义
CustomCode customCode = new CustomCode();
customCode.setCode("CODE20260210001");  // 业务自定义主键
customCode.setName("测试编码");
customCodeMapper.insert(customCode);

注意事项

  • 插入前必须手动设置主键值,否则会报错
  • 需自行保证主键的唯一性
  • Oracle 场景需配合 @KeySequence 注解和序列生成器
  • 适合对主键规则有特殊要求的业务场景

3.5 NONE:无特定策略

适用场景

  • 主键由其他框架或中间件生成
  • 需要完全手动控制主键
  • 与全局配置配合使用

配置方式

@Data
@TableName("manual_id")
public class ManualId {
    @TableId(type = IdType.NONE)
    private String customId;
    private String name;
}

使用示例

ManualId entity = new ManualId();
entity.setCustomId("CUSTOM_" + System.currentTimeMillis());  // 手动设置
entity.setName("手动主键");
manualIdMapper.insert(entity);

注意事项

  • 插入时必须确保主键有值,否则会报错
  • 等效于未配置策略,实际使用全局配置
  • 插入失败可能导致主键冲突

四、全局配置方式

4.1 YAML 配置(推荐)

# application.yml
mybatis-plus:
  global-config:
    db-config:
      # 全局主键生成策略
      id-type: assign_id
      # 自定义 ID 生成器
      key-generator: com.baomidou.mybatisplus.extension.incrementer.OracleKeyGenerator

可选值:

  • auto:数据库自增
  • assign_id:雪花算法(默认)
  • assign_uuid:UUID
  • input:手动输入
  • none:无特定策略

4.2 Java 配置类

@Configuration
public class MyBatisPlusConfig {
    
    @Bean
    public GlobalConfig globalConfig() {
        GlobalConfig config = new GlobalConfig();
        // 设置全局主键策略为雪花算法
        config.getDbConfig().setIdType(IdType.ASSIGN_ID);
        
        // 自定义 ID 生成器
        config.setIdentifierGenerator(new CustomIdentifierGenerator());
        
        return config;
    }
    
    // 自定义 ID 生成器实现
    static class CustomIdentifierGenerator implements IdentifierGenerator {
        @Override
        public Number nextId(Object entity) {
            // 自定义 ID 生成逻辑
            String prefix = entity.getClass().getSimpleName().toUpperCase();
            return Long.parseLong(prefix + System.currentTimeMillis());
        }
    }
}

4.3 Spring XML 配置(传统 Spring 项目)

<bean id="globalConfig" class="com.baomidou.mybatisplus.core.config.GlobalConfig">
    <property name="dbConfig">
        <bean class="com.baomidou.mybatisplus.core.config.GlobalConfig$DbConfig">
            <property name="idType" value="ASSIGN_ID"/>
        </bean>
    </property>
</bean>

五、主键回填机制

5.1 原理说明

主键回填是指执行 insert() 方法后,MyBatis-Plus 会自动将生成的主键值填充到实体对象的对应属性中,无需额外操作。

5.2 回填验证

User user = new User();
user.setName("Alice");
userMapper.insert(user);

// 主键已自动回填到实体对象中
Long id = user.getId();
System.out.println("回填的主键ID:" + id);

注意:

  • insert() 方法的返回值是影响行数,不是主键值
  • 主键值通过实体对象获取
  • 批量插入时,集合中每个实体的主键均会被回填

5.3 批量插入回填

<!-- UserMapper.xml -->
<insert id="insertBatch" useGeneratedKeys="true" keyProperty="id">
    INSERT INTO user (name) VALUES
    <foreach item="item" collection="list" separator=",">
        (#{item.name})
    </foreach>
</insert>

注意事项:

  • MySQL 需开启 useGeneratedKeys
  • Oracle 需配合序列使用 IdType.INPUT
  • PostgreSQL 建议使用 RETURNING 子句回填主键

六、自定义主键生成器

6.1 业务 ID 生成器(前缀 + 时间戳)

@Component
public class BizIdGenerator implements IdentifierGenerator {
    
    @Override
    public Number nextId(Object entity) {
        // 获取实体类名作为前缀
        String prefix = entity.getClass().getSimpleName().toUpperCase();
        
        // 生成业务 ID:前缀 + 时间戳
        String bizId = prefix + System.currentTimeMillis();
        
        return Long.parseLong(bizId);
    }
}

6.2 分布式 ID 生成器(基于 Redis)

@Component
public class RedisIdGenerator implements IdentifierGenerator {
    
    @Autowired
    private StringRedisTemplate redisTemplate;
    
    @Override
    public Number nextId(Object entity) {
        String key = "id:generator:" + entity.getClass().getSimpleName();
        
        // Redis 原子自增
        Long id = redisTemplate.opsForValue().increment(key);
        
        // 设置过期时间(可选)
        if (id == 1) {
            redisTemplate.expire(key, 7, TimeUnit.DAYS);
        }
        
        return id;
    }
}

6.3 注册自定义生成器

方式一:声明为 Bean

@Component
public class CustomIdGenerator implements IdentifierGenerator {
    @Override
    public Long nextId(Object entity) {
        // 自定义逻辑
        return ...;
    }
}

方式二:通过配置类注册

@Bean
public IdentifierGenerator customIdGenerator() {
    return new CustomIdGenerator();
}

方式三:使用 MybatisPlusPropertiesCustomizer

@Bean
public MybatisPlusPropertiesCustomizer plusPropertiesCustomizer() {
    return props -> 
        props.getGlobalConfig().setIdentifierGenerator(new CustomIdGenerator());
}

七、常见问题与避坑指南

7.1 主键未回填

排查步骤:

  1. 检查数据库表主键是否设置自增(MySQL 需 AUTO_INCREMENT
  2. 确认 @TableId 注解的 type 与数据库策略匹配
  3. 切勿从 insert() 返回值获取主键,应从实体对象获取
  4. 检查是否正确配置了 useGeneratedKeys(批量插入场景)

7.2 时钟回拨问题

现象:
雪花算法依赖系统时钟,若服务器时钟回拨,可能导致 ID 重复。

解决方案:

  1. 保证服务器时钟同步

    • 使用 NTP 服务进行时钟同步
    • 生产环境建议配置时钟同步策略
  2. 使用带检测的雪花算法

    • MyBatis-Plus 默认实现已做时钟回拨检测
    • 自定义实现时可添加检测逻辑
  3. 降级策略

    • 检测到时钟回拨时,等待或使用备用策略

7.3 UUID 无序导致的性能问题

问题:
UUID 无序特性导致数据插入时频繁发生页分裂,影响索引性能。

解决方案:

  1. 优先使用 ASSIGN_ID(雪花算法)

    • 生成的 ID 有序递增,索引效率高
    • 适合分布式场景
  2. 优化索引设计

    • 考虑使用辅助索引
    • 对有序性要求高的字段单独设计
  3. 批量插入优化

    • 使用批量插入减少 IO 操作
    • 考虑调整索引策略

7.4 类型转换异常

错误示例:

// 错误:ASSIGN_UUID 要求主键类型为 String
@TableId(type = IdType.ASSIGN_UUID)
private Long id;  // ❌ 类型不匹配

正确做法:

// 正确:ASSIGN_UUID 主键类型必须为 String
@TableId(type = IdType.ASSIGN_UUID)
private String id;  // ✅ 类型正确

7.5 分库分表主键冲突

问题:
在分库分表场景下,使用 AUTO 策略会导致不同分表的主键重复。

解决方案:

  1. 绝对禁止使用 AUTO

    • 分库分表后,不同分表的自增主键会重复
  2. 优先选择 ASSIGN_ID

    • 雪花算法生成的主键全局唯一、有序
    • 适合分库分表场景
  3. 避免使用 ASSIGN_UUID

    • 无序主键会导致分表后数据分布不均

八、选型建议与最佳实践

8.1 场景选型对照表

业务场景 推荐策略 理由
小型应用 / 单库单表 AUTO 简单高效,依赖数据库原生能力
分布式系统 / 微服务 ASSIGN_ID 全局唯一有序,支持分库分表
字符串主键 / 无序性要求 ASSIGN_UUID 分布式唯一,无需依赖数据库
业务编码主键 INPUT 完全自定义,贴合业务规则
Oracle 数据库 INPUT + @KeySequence 使用序列生成主键

8.2 最佳实践

  1. 优先使用默认策略

    • 大部分场景下,ASSIGN_ID(雪花算法)是最佳选择
    • 避免过度设计
  2. 统一全局配置

    • 项目中大部分实体类使用相同策略时,通过全局配置统一设置
    • 特殊需求时通过局部注解覆盖
  3. 类型安全

    • 确保实体类主键类型与策略匹配
    • 避免类型转换异常
  4. 时钟同步

    • 使用雪花算法时,保证服务器时钟同步
    • 避免时钟回拨问题
  5. 索引优化

    • 根据主键策略优化数据库索引
    • 有序主键提升查询性能
  6. 批量操作

    • 批量插入时注意主键回填机制
    • 配置 useGeneratedKeys 参数

8.3 配置优先级总结

主键策略确定

是否已配置 @TableId 注解?

使用局部注解策略

是否已配置全局策略?

使用全局配置策略

使用默认策略: ASSIGN_ID

生成主键并回填

九、完整代码示例

9.1 依赖配置

<!-- pom.xml -->
<dependencies>
    <!-- Spring Boot 3.x -->
    <dependency>
        <groupId>com.baomidou</groupId>
        <artifactId>mybatis-plus-spring-boot3-starter</artifactId>
        <version>3.5.16</version>
    </dependency>
    
    <!-- Spring Boot 2.x -->
    <dependency>
        <groupId>com.baomidou</groupId>
        <artifactId>mybatis-plus-boot-starter</artifactId>
        <version>3.5.16</version>
    </dependency>
    
    <!-- Spring Boot 4.x -->
    <dependency>
        <groupId>com.baomidou</groupId>
        <artifactId>mybatis-plus-spring-boot4-starter</artifactId>
        <version>3.5.16</version>
    </dependency>
</dependencies>

9.2 配置文件

# application.yml
mybatis-plus:
  configuration:
    map-underscore-to-camel-case: true  # 驼峰转下划线
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl  # SQL 日志
  global-config:
    db-config:
      id-type: assign_id  # 全局主键策略
      table-prefix: tbl_  # 表前缀

9.3 实体类示例

@Data
@TableName("user")
public class User {
    @TableId(type = IdType.ASSIGN_ID)
    private Long id;
    
    @TableField("username")
    private String username;
    
    private Integer age;
    
    @TableLogic  // 逻辑删除字段
    private Integer deleted;
    
    @Version  // 乐观锁字段
    private Integer version;
    
    @TableField(fill = FieldFill.INSERT)  // 自动填充
    private LocalDateTime createTime;
}

十、总结

MyBatis-Plus 3.5.16 的主键生成策略设计精妙,覆盖了从单库单表到分布式分库分表的全场景需求。掌握主键策略的选型和配置,是开发高效、可扩展系统的核心技能。

核心要点回顾

  1. 默认策略ASSIGN_ID(雪花算法),适合大部分场景
  2. 配置优先级:局部注解 > 全局配置 > 默认策略
  3. 主键回填:插入后自动填充到实体对象,通过实体获取主键
  4. 分布式场景:优先选择 ASSIGN_ID,禁止使用 AUTO
  5. 自定义扩展:实现 IdentifierGenerator 接口可定制 ID 生成逻辑

升级注意事项

从旧版本升级到 3.5.16 时,注意以下变化:

  1. 弃用策略ID_WORKERUUIDID_WORKER_STR 已弃用,请使用 ASSIGN_IDASSIGN_UUID 替代
  2. Spring Boot 4 支持:新增 mybatis-plus-spring-boot4-starter 模块
  3. JSQLParser 模块化:从 3.5.9 开始,JSQLParser 支持已独立,需自行引入 mybatis-plus-jsqlparsermybatis-plus-jsqlparser-4.9

通过合理选择和配置主键生成策略,MyBatis-Plus 能无缝实现主键管理,显著提升开发效率和系统性能。希望本文能帮助你在实际项目中做出更明智的技术选型!


参考资源:

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐