前言
本文主要是针对Mybatis-plus框架,在调用 saveBatch() 方法时,出现的 id 重复导致的异常报错进行分析,提供后续场景出现相同场景时应该如何定位问题,如何进行调整方案。
问题分析及解决方案
一、场景分析
1、Yaml配置文件
mybatis-plus:
global-config:
db-config:
field-strategy: not_empty
id-type: auto
db-type: mysql
logic-delete-field: deleted # 全局逻辑删除的实体字段名
logic-delete-value: 1 # 逻辑已删除值(默认为 1)
logic-not-delete-value: 0 # 逻辑未删除值(默认为 0)
banner: false
configuration:
map-underscore-to-camel-case: true
auto-mapping-behavior: full
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
mapper-locations: classpath*:mapper/**/*Mapper.xml
配置文件中已经配置ID生成逻辑
2、相关DO类
package com.domain.entity;
import java.util.Date;
@Data
@ToString
@Accessors(chain = true)
@EqualsAndHashCode(callSuper = false)
@TableName("bean")
public class BeanDO {
private static final long serialVersionUID = 227482132234482183L;
/**
*业务Id
*/
@TableId(value = "id",type = IdType.ASSIGN_ID)
private Long id;
/**
*所属机构编码
*/
@TableField("org_code")
private String orgCode;
/**
*所属机构id
*/
@TableField("org_id")
private Long orgId;
...
}
从DO Bean中可以看到,开发者已经在bean中加入@TableId(value = "id",type = IdType.ASSIGN_ID)注解,并且指定为mybatis-plus的雪花ID算法生成逻辑,配合YAML的配置,以及可以确定ID的生成逻辑是没有任何问题的。
3、实际执行代码
private void updateAndInsert() {
//新增集合
List<BeanDO> insertList = new ArrayList<>();
// ... 业务代码
this.saveBatch(insertList);
}
从实际的执行批量保存逻辑来看,调用的方法是在service层,使用this.saveBatch(Collection collection)方法,好像也没有任何问题。
那为什么还是出现了 InstrumentUseBasicInfoMapper.insert (batch index #1) failed. Cause: java.sql.BatchUpdateException: Duplicate entry ‘1695828558899920911’ for key ‘instrument_use_basic_info.PRIMARY’; 问题的呢?
二、问题分析思路
1、代码诊断步骤:
从上面的配置、bean、执行代码,看似好像一整个逻辑都没有任何问题。好像没有任何的思路,遇到这种问题的时候,别慌!
出现这种场景,可以参考以下几步来确认问题点:
第一步、优先先检查配置文件YAML是否有问题。
第二步、检查自身实现代码逻辑是否存在问题。
第三步、通过其他方式验证雪花ID是否生效。
第四步、以上几种场景都已经确认没有任何问题,那么就开始合理怀疑Mybatis-plus框架是否存在问题。
⚠️注意:作为一个开发者,对任何的框架都不要去盲目信任,认为一定不存在任何问题。所有的框架都是人编写的,既然是人编写的那么也一定会存在BUG,任何的一个框架都是存在BUG的,框架的完善也是不断进步,所以存在即合理。
2、假设出现问题点
1. 怀疑一:是不是@TableId(value = "id",type = IdType.ASSIGN_ID)注解,没有生效呢?
2. 怀疑二:什么场景下,才会导致ID生成重复呢?并发场景?
3. 怀疑三:为什么for + save不会导致ID重复呢?
4. 怀疑四:Mybatis-plus雪花ID生成逻辑,是否与新增数据相关呢?
3、手动还原场景
1、怀疑一:是不是@TableId(value = “id”,type = IdType.ASSIGN_ID)注解,没有生效呢?
本地测试 this.save() 是否自动生成雪花ID
// 代码自己完善
测试结果:已生效
本地测试 this.saveBatch() 是否自动生成雪花ID
// 代码自己完善
测试结果:已生效
2、怀疑二:是不是 this.saveBatch() 方法中的对象重复了?
Man man = new Man();
man = Man.builder().age(1).email("@1").name("name1:").build();
list3.add(man);
list3.add(man);
list3.add(man);
manService.saveBatch(list3);
测试结果:ID不重复
3、怀疑三:什么场景下,才会导致ID生成重复呢?并发场景?
public Thread getThread(){
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
int i = 100000000;
for (int j = 0; j < i; j++) {
long value = identifierGenerator.nextId(new Object()).longValue();
list.add(value);
if (set.contains(value)) {
System.out.println(value);
continue;
}
set.add(value);
}
}
});
return thread;
}
public void idTest1() throws InterruptedException {
getThread().start();
getThread().start();
getThread().start();
getThread().start();
getThread().start();
getThread().start();
getThread().start();
getThread().start();
}
测试结果:发现ID重复
4、怀疑四:为什么for + save不会导致ID重复呢?
Man man = Man.builder().age(1).email("@1").name("name1:").build();
for (int j = 0; j < i; j++) {
manService.save(man);
}
测试结果:ID不重复
4、结论
通过以上几种场景验证,可以确定出现ID重复的场景,是在并发下导致雪花ID生成重复。
三、源码分析
1、看看this.save()源码
可以看到 this.saveBatch() 调用的方法是 default boolean saveBatch(Collection entityList), 而该方法去调用的 boolean saveBatch(Collection entityList, int batchSize);
在public boolean saveBatch(Collection entityList, int batchSize)可以看到,一共执行了两行代码,
第一行,String sqlStatement = getSqlStatement(SqlMethod.INSERT_ONE); ,可以根据单词意思,这个是去获取SQL声明,稍微理解一下就是SQL模版;
第二行,这行代码才是我们的关注的重点
return executeBatch(entityList, batchSize, (sqlSession, entity) -> sqlSession.insert(sqlStatement, entity));
拆分一下这行代码,sqlSession.insert(sqlStatement, entity) 猜测一下,这段代码,就是根据实体类去替换sql模版里面的值进行新增,所以我们要找的ID部分应该不在这里进行维护;
那就重点看一下,executeBatch() 这个方法
好像没啥好看的,接着看里面的代码
到这里之后,发现idxLimit,终于看到第一个跟ID相关的单词,分析idxLimit的生成逻辑;可以看到 idxLimit 的逻辑就是一个简单的自增逻辑,并且idxLimit有一个大小限制batchSize;batchSize是一个参数,往回代码找一下,可以确认 batchSize = 1000;
com.baomidou.mybatisplus.extension.service.IService#DEFAULT_BATCH_SIZE
到此为止,我们可以先下一个初步的结论:并发场景下,ID重复问题,应该是有由于不同请求进来的时候,ID自增的时候,没有在程序内存中进行同步自增造成的;不同请求下,自增ID的生命周期不同,在自增的逻辑下各干各的,从而导致ID自增重复的问题;
2、Mybatis-plus中ID默认生成器 – DefaultIdentifierGenerator
首先,我们来分析一下 DefaultIdentifierGenerator,这里面有一个 sequence.nextId() 方法,进入看一下
可以看到,netxId() 中有一个关键字 synchronized,说明这个方法是支持并发的,接下来看一下重点代码
这两行代码,可以说是告诉了我们ID的生成逻辑,第一行代码,告诉我们如果时间戳相同,雪花ID则走的是自增逻辑,反着,将序列化进行随机
第二行代码,可以告诉我们每次调用nextId都会去更新 lastTimetamp 常量;
四、问题总结
调用 this.saveBatch() 批量新增出现ID重复问题,原因是 this.saveBatch() 方法不支持并发场景,在并发场景下 this.saveBatch() 与 this.save() 生成雪花ID会导致冲突,因为ID的自增逻辑没有在内存中进行同步,导致并发场景下取到的ID自增初始值可能一样,在自增场景下导致重复;
五、改善方案
取消 @TableId(value = “id”,type = IdType.ASSIGN_ID) 的使用 改成手动生成雪花ID,使用Mybatis-plus中com.baomidou.mybatisplus.core.toolkit.IdWorker#getId(java.lang.Object);