ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

MyBatis-Plus主键生成策略与雪花算法实战:从IdType到全局配置

MyBatis-Plus主键生成策略与雪花算法实战:从IdType到全局配置 简介MyBatis-Plus主键生成策略是 Java 开发者使用该持久层框架时经常需要面对的配置要点这份 PDF 资料专门梳理了从实体类局部设置到全局配置文件的完整主键策略使用方法。内容以 TableId 注解和 application.yml 全局配置为主线逐项对比数据库自增、UUID、UUID 转 Int64、雪花算法、用户自定义输入等策略的适用场景与限制并指出局部注解优先级高于全局配置这一关键规则同时针对分布式系统中的 ID 生成问题扩展分析了 Redis生成与Snowflake算法在性能和有序性上的取舍。学完以后可以清楚理解不同主键生成方式在单机、分库分表条件下的差异掌握如何通过全局默认策略与局部注解覆盖方式灵活组合配置解决主键冲突、存储膨胀、查询效率下降等实际问题也为后续选择合适的主键方案提供决策参考。资源为 1 个 PDF 文档压缩包约 95KB内容紧凑但完整适合需要快速查阅或系统复习的框架使用者。目前已有 6918 人学习下载是同类技术主题中较受关注的资料之一。1. 主键生成策略不只是选个 ID从 MyBatis-Plus 的默认雪花算法说起接手一个老项目时你会发现所有表的主键都是 19 位 bigint长度超过前端 JavaScript 的安全整数范围于是「主键 ID 精度丢失」成了第一个线上 bug。这其实是 MyBatis-Plus 默认用「雪花算法」生成主键导致的。很多人以为在实体上加一个TableId就算配完了但 MyBatis-Plus 的IdType枚举里定义了 6 种策略局部注解、全局 YAML、数据库字段类型三者之间到底谁说了算很多人并没搞清。这里直接把枚举源码、注解配置、全局配置和分布式 ID 生成方案放在一起拆开讲让新手能快速选型也让老手避开 UUID 映射到 Long、全局配置不生效、自定义 SQL 查出逻辑删除数据这些容易翻车的细节。2. IdType 枚举与局部主键策略TableId 的六种用法与数据库字段匹配2.1 IdType 枚举源码里藏着六种策略MyBatis-Plus 把主键策略定义在一个枚举类IdType中源码可以简化成下面这样public enum IdType { AUTO(0), // 数据库自增依赖数据库自身能力 NONE(1), // 未设置主键类型实际会查全局配置查不到则默认雪花算法 INPUT(2), // 用户手动输入也可配合 MetaObjectHandler 字段填充 ID_WORKER(3), // 全局唯一IDLong 类型底层是雪花算法 UUID(4), // 全局唯一UUIDString 类型 ID_WORKER_STR(5); // 全局唯一IDString 类型底层同样是雪花算法 private final int key; IdType(int key) { this.key key; } public int getKey() { return key; } }枚举的key只是内部标识写业务代码时直接用IdType.AUTO这种字面量即可。六个值里最容易误解的是NONE它的注释写着“未设置主键类型”但实际执行时会先读全局id-type全局也没配置时才落到框架内置的IdentifierGenerator默认实现也就是雪花算法。ID_WORKER与ID_WORKER_STR的区别不在算法而在返回值类型前者产出Long后者产出String。如果数据库主键字段是varchar选ID_WORKER_STR更合适否则数字转字符串的隐式转换在部分 JDBC 驱动里会出问题。2.2 TableId 局部策略的实体类实战局部策略通过在实体主键字段上加TableId(type IdType.XXX)指定这里把六种策略放在同一个类里演示实际项目中一个实体只有一个主键字段public class User { TableId(type IdType.AUTO) private Long id; TableId(type IdType.NONE) private Long columnId; TableId(type IdType.UUID) private String uuid; TableId(type IdType.ID_WORKER) private Long orderId; TableId(type IdType.ID_WORKER_STR) private String flowId; TableId(type IdType.INPUT) private Long customId; // 其他业务字段省略 }TableId有两个常用属性value用来指定数据库主键列名默认按驼峰转下划线映射type用来指定主键策略。需要特别注意的是UUID策略字段必须声明为String因为 MyBatis-Plus 底层会执行UUID.randomUUID().toString().replace(-, )如果你把字段写成LongMyBatis 在设置参数时直接抛String cannot be cast to Long。ID_WORKER_STR同理字段类型必须是String数据库建议用varchar(64)避免 19 位雪花 ID 在跨系统传输时被前端当Number截断。2.3 数据库字段类型与策略匹配对照不同策略对 Java 类型和数据库字段类型有硬性要求对照关系如下IdType推荐 Java 类型数据库建议类型典型业务场景AUTOLong / Integerbigint 自增单库单表数据量可控NONELongbigint跟随全局配置不显式指定INPUT任意与 Java 类型一致导入历史数据、手动分配主键UUIDStringvarchar(32)分布式但不在乎排序和存储空间ID_WORKERLongbigint分布式默认推荐ID_WORKER_STRStringvarchar(64)需要避免大数据量传输精度问题实际开发中最容易踩的坑是数据库字段类型与策略不匹配。AUTO策略要求数据库主键必须真的配置了自增否则 MyBatis-Plus 会把主键字段置空交给数据库最终报 SQL 异常ID_WORKER策略生成的是Long如果数据库主键是varchar并且 JDBC 驱动没做隐式转换插入时也会失败。所以局部策略定下来后先对着表结构确认字段类型再启动项目。3. 全局主键策略application.yml 配置与优先级规则3.1 global-config 下的 db-config 完整配置全局主键策略在application.yml中配置三层结构不能写错mybatis-plus: mapper-locations: - classpath*:com/mp/mapper/*.xml global-config: db-config: id-type: id_worker_str注意id-type的可选值对应枚举名的下划线小写auto、none、input、uuid、id_worker、id_worker_str。配置了id_worker_str之后所有没有显式写TableId(type...)的实体主键都会按雪花算法生成字符串 ID。这里容易犯的错是层级id-type必须挂在global-config.db-config下面而不是global-config的直接子级。如果你在 2.x 升级到 3.x 的项目里看到mybatis-plus.global-config.id-type这种写法那是旧版的配置位置新版本读不到也不会报错主键就一直保持默认雪花算法。3.2 局部策略与全局策略的优先级主键策略的优先级可以简单概括为局部注解大于全局 YAML 大于框架默认。看下面这个表格配置位置写法示例优先级实体字段TableId(type IdType.AUTO)最高全局 YAMLdb-config.id-type: auto次之MP 默认雪花算法ASSIGN_ID最低一个常见误解是TableId(type IdType.NONE)真的等于“没有策略”。实际上它会在项目启动时查询全局id-type如果全局也是none才走到默认雪花算法。所以用NONE不是什么都不配而是把决策权交给全局配置。局部与全局可以混用全局配置成uuidA 表实体上写TableId(type IdType.AUTO)那么 A 表插入时仍然走数据库自增B 表实体没有注解则使用全局的 UUID。这种写法在多数据源项目里很实用比如一个库里主键已经由触发器生成另一个库需要 MP 管理按表覆盖即可。3.3 配置不生效时的排查路径如果发现插入数据时主键不是预期策略按下面顺序查确认实体引的是com.baomidou.mybatisplus.annotation.TableId不是 JPA 的javax.persistence.Id。两者混用会导致 MP 的注解扫描落空。检查配置文件层级id-type是否在mybatis-plus.global-config.db-config下。常见错误是把id-type写到mybatis-plus.global-config下3.x 版本不会读取。确认数据库字段类型与策略匹配。AUTO策略对应自增列ID_WORKER对应数值列UUID对应字符列。如果全局和局部都没有问题看看是不是自定义了MetaObjectHandler在插入时手动覆盖了主键字段。这种情况在代码里最隐蔽。全局配置生效后插入时不需要手动给主键赋值。使用INPUT策略时则需要提供自定义填充逻辑常见做法是写一个MetaObjectHandlerComponent public class CustomIdFill implements MetaObjectHandler { Override public void insertFill(MetaObject metaObject) { // 主键为空时才填充避免覆盖用户手动传入的 ID Object id getFieldValByName(id, metaObject); if (id null) { setFieldValByName(id, IdWorker.getId(), metaObject); } } }MetaObjectHandler是 MyBatis-Plus 的公共字段填充接口insertFill会在每次 insert 前被调用。这里的判断逻辑很关键先查主键字段是否已经有值有值就保留没值才填充这样INPUT和自动填充可以共存不会误伤手动指定的主键。4. 分布式主键生成方案对比从数据库自增到雪花算法4.1 数据库自增简单但单点风险高数据库自增是单机时代最省事的方案MySQL 的AUTO_INCREMENT和 SQL Server 的IDENTITY都能保证单调递增对分页和排序天然友好。缺点也很明显不同数据库语法不一致一主多从场景下只有主库能生成 ID主库一旦不可用新增数据的入口直接卡死。常见的优化方案是部署多个主库不同节点设置不同起始值、相同步长Master 节点起始 ID步长生成序列M1131, 4, 7, 10M2232, 5, 8, 11M3333, 6, 9, 12这样每个库生成的 ID 全局不重复但扩容代价很高如果原来步长是 3想再扩一个主库变成步长 4已经生成的序列和新步长不对齐必须提前规划好节点数量一般适用于主库数固定、几乎不扩容的集群。4.2 UUID全局唯一但不建议做主键UUID 的好处是客户端就能生成不依赖数据库性能极好。但 36 个字符太长去横线也要 32 个字符作为主键不仅占存储还会让 InnoDB 聚簇索引在随机插入时频繁页分裂降低查询效率。至于UUID to Int64这种变种本质是把 UUID 截断或用哈希转成Long解决了长度和可读性问题但在极端情况下仍有碰撞风险而且生成后是无序的不适合作为范围查询的索引字段。如果业务只需要全局唯一、不要求递增并且单表数据量不大UUID 可以用数据量一旦上来还是优先考虑雪花算法。4.3 Redis INCR适合每天从 0 开始的流水号Redis 生成 ID 依赖单线程命令的原子性最常用的是INCR-- 生成当天订单号日期 自增数 INCR order:20250608每次执行都返回一个递增的整数天然适合订单号、流水号这类“日期 序号”结构。只要每天换一个 key就能实现当天从 0 开始计数。Redis 集群可以给各节点设置不同的初始值和相同步长比如 5 个节点分别从 1、2、3、4、5 开始步长 5各节点生成的 ID 全局不重复。这个方案的代价是如果系统里没有 Redis为了生成 ID 单独引入一个中间件运维成本和复杂度都不小一般只用在已经重度依赖 Redis 的团队。4.4 雪花算法MP 默认方案的位分配与自定义生成器MyBatis-Plus 的ID_WORKER和ID_WORKER_STR底层都使用 Twitter Snowflake 算法的 Java 实现。一个 64 位 long 被拆成四段bit 位数用途1bit符号位固定 041bit毫秒时间戳约 69 年5bit数据中心 ID5bit机器 ID12bit同一毫秒内的序列号0-4095这个结构决定了单个节点在每毫秒最多生成 4096 个不重复 ID对绝大多数业务都够用。默认实现会根据网卡和进程信息自动生成workerId和datacenterId如果你想在集群里固定机器位可以自定义IdentifierGeneratorBeanBean public IdentifierGenerator identifierGenerator() { return new DefaultIdentifierGenerator(2L, 1L); }参数说明第一个参数是workerId第二个是datacenterId。多节点部署时保证这两个值的组合不重复即可。需要注意的是雪花算法存在时钟回拨风险如果机器时间往回跳同一毫秒内生成的序列可能重复团队内通常依赖 NTP 时钟同步来规避更严格的做法是参考美团的 Leaf 方案在回拨时等待或切换到数据库段 ID。4.5 其他方案ZooKeeper 与 MongoDB ObjectIdZooKeeper 可以通过 znode 数据版本号生成唯一序列但每次获取 ID 都要调用多次 ZK API并发竞争大时还需要分布式锁吞吐量在高并发场景下不够理想因此使用较少。MongoDB 的 ObjectId 和雪花算法思路类似用 12 字节表示时间戳、机器号、进程号和计数器适合分片环境下大量生成唯一 ID但如果业务主库是 MySQL强行用 ObjectId 作为主键在 Java 里还需要处理org.bson.types.ObjectId类型反而增加转换成本。多数团队最后会在“数据库自增”和“雪花算法”之间二选一中间几种方案只留给特定场景。5. 主键策略与逻辑删除字段查询的联动一个容易忽略的坑5.1 查询 deleted 记录时参数类型要和主键策略一致MyBatis-Plus 的逻辑删除TableLogic只对BaseMapper自带方法生效比如deleteById会转成UPDATE ... SET deleted 1 WHERE id ?selectById会自动拼接AND deleted 0。但自己写Select时这些默认行为全部失效public interface UserMapper extends BaseMapperUser { // 主键用雪花算法生成查询参数必须用 Long 接收 Select(SELECT * FROM user WHERE id #{id} AND deleted 0) User selectActiveById(Param(id) Long id); }这里有两个容易同时出现的坑。第一个是参数类型当主键策略是ID_WORKER时Long没有问题如果换成了ID_WORKER_STR或UUIDMapper 方法的 id 参数必须同步改成String否则 MyBatis 在设置 SQL 参数时可能类型转换异常或者查出来的结果永远是空。第二个是条件拼接自定义Select不会像selectById那样自动帮你过滤deleted字段必须手动写上deleted 0。5.2 用一条 SQL 日志验证两个配置是否同时生效建议在新项目起步阶段做一次这样的验证插入一条数据并打印生成的雪花 ID然后调用deleteById执行逻辑删除观察控制台 SQL 日志输出的是UPDATE user SET deleted1 WHERE id?而不是DELETE FROM user。接下来再用不带deleted条件的自定义 SQL 查询刚才那个 ID会看到被删除的数据仍然能查出来补上AND deleted 0之后才查不到。这个过程能同时确认三件事主键策略是否按预期生成、逻辑删除是否真正生效、自定义查询是否会绕过逻辑删除条件。主键策略决定了插入时的 ID 长相逻辑删除条件决定了查询时能不能看到这些 ID两个配置本身没有依赖关系但在同一个项目里几乎一定会一起出现早验证早安心。本文还有配套的精品资源点击获取
返回列表