MyBatisPlus + ShardingJDBC 批量插入不返回主键ID

news2024/12/23 5:35:40

本文讲述一个由 ShardingJDBC 使用不当引起的悲惨故事。

一. 问题重现

有一天运营反馈我们部分订单状态和第三方订单状态无法同步。

根据现象找到了不能同步订单状态是因为 order 表的 thirdOrderId 为空导致的,但是这个字段为啥为空,排查过程比较波折。

过滤掉复杂的业务逻辑,当时的代码可以简化为这样:

Order order;
// 业务在特定情况会生成新的订单
if (特定条件) {
    order = buildOrders();
	orderService.saveBatch(Lists.newArrayList(order));
}


// 省略复杂的业务逻辑
// ...

// 调用第三方下单
ThirdOrder thirdOrder = callThirdPlaceOrder()
// 设置order表 thirdOrderId 字段
order.setThirdOrderId(thirdOrder.getOrderId());
// 设置 order_item 表 thirdOrderId 字段
orderItems.foreach(e -> e.setThirdOrderId(thirdOrder.getOrderId()));

// 更新 order 表
orderService.updateById(order);
// 更新 order_item 表
orderItemService.updateBatchById(itemUpdateList);

我们发现这类有问题的订单,order 表 thirdOrderId 为空,但是 order_item 表 thirdOrderId 更新成功了,使我们直接排除了这里母单更新“失败”的问题,因为两张表的更新操作在一个事务里面,子单更新成功了说明这里的代码逻辑应该没有问题。

就是这里的错觉,让我们走了很多弯路。我们排查了所有可能存在并发更新、先读后写、数据覆盖的地方,结合业务日志,翻遍了业务代码仍然无法确认问题具体在哪里。最后只能在可能出现问题的地方补充了日志,同时我们也在此处更新 order 表的地方加上了日志,最后发现在执行 orderService.saveBatch 后 order 的 id 为空,导致 order 的更新并没有成功。

说实话找到问题的那一刻有点颠覆我的认知,在我的印象中,MyBatisPlus批量插入的方法是可以返回ID,经过实验,在当前项目环境中,save()方法会返回主键ID,但是saveBatch()方法不会。这种颠覆认知的新

二. 源码分析

2.1 JDBC如何获取批量插入数据的ID

要想摸清楚批量插入后为什么没有获取到主键ID,我们得先了解一下JDBC如何批量插入数据,以及在批量插入操作后,获取数据库的主键值。

// 创建一个 PreparedStatement 对象,并指定获取自动生成的主键
PreparedStatement pstmt = conn.prepareStatement("INSERT INTO order_info (column1, column2) VALUES (?, ?)", Statement.RETURN_GENERATED_KEYS);

// 执行批量插入操作
pstmt.setString(1, "value1"); // 设置参数值
pstmt.setString(2, "value2"); // 设置参数值
pstmt.addBatch(); // 添加批量操作

// ... 添加更多批量操作

// 执行批量操作
pstmt.executeBatch();

// 获取生成的主键
ResultSet generatedKeys = pstmt.getGeneratedKeys();
while (generatedKeys.next()) {
    int primaryKey = generatedKeys.getInt(1); // 假设主键为整数类型,如果是其他类型,请根据实际情况调整
    System.out.println("Generated Primary Key: " + primaryKey);
}

// 关闭相关资源
generatedKeys.close();
pstmt.close();
conn.close();

在执行批量插入操作后,我们可以通过 Statement.getGeneratedKeys() 方法获取数据库主键值。

2.2 MyBatis 批量插入原理

MyBatis-Plus 是对 MyBatis 的一种增强,底层还是依赖于MyBatis SqlSession API对数据库进行的操作,而SqlSession执行批量插入大概分为如下几步:

try (SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.BATCH)) {
    YourMapper mapper = sqlSession.getMapper(YourMapper.class);

    List<YourEntity> entities = new ArrayList<>();
    // 添加要插入的实体对象到列表中

    for (YourEntity entity : entities) {
        // 调用插入方法,但此时还未真正执行
        mapper.insert(entity); 
    }

    // 批量执行SQL
    sqlSession.flushStatements(); 

    sqlSession.commit(); // 提交事务
} catch (Exception e) {
    sqlSession.rollback(); // 发生异常时回滚事务
}

2.3 Myabtis-Plus + ShardingJDBC 批量插入数据为什么无法获取ID

MyBatis-Plus 执行批量插入操作本质上和MyBatis是一致的,Myabtis-Plus saveBtach方法:

/**
 * 插入(批量)
 *
 * @param entityList 实体对象集合
 */
@Transactional(rollbackFor = Exception.class)
default boolean saveBatch(Collection<T> entityList) {
    return saveBatch(entityList, DEFAULT_BATCH_SIZE);
}

/**
 * 批量插入
 *
 * @param entityList ignore
 * @param batchSize  ignore
 * @return ignore
 */
@Transactional(rollbackFor = Exception.class)
@Override
public boolean saveBatch(Collection<T> entityList, int batchSize) {
    String sqlStatement = getSqlStatement(SqlMethod.INSERT_ONE);
    return executeBatch(entityList, batchSize, (sqlSession, entity) -> sqlSession.insert(sqlStatement, entity));
}

进入executeBatch:

   /**
     * 执行批量操作
     *
     * @param entityClass 实体类
     * @param log         日志对象
     * @param list        数据集合
     * @param batchSize   批次大小
     * @param consumer    consumer
     * @param <E>         T
     * @return 操作结果
     * @since 3.4.0
     */
    public static <E> boolean executeBatch(Class<?> entityClass, Log log, Collection<E> list, int batchSize, BiConsumer<SqlSession, E> consumer) {
        Assert.isFalse(batchSize < 1, "batchSize must not be less than one");
        return !CollectionUtils.isEmpty(list) && executeBatch(entityClass, log, sqlSession -> {
            int size = list.size();
            int i = 1;
            for (E element : list) {
                // 执行 sqlSession 的 insert 方法
                consumer.accept(sqlSession, element);
                if ((i % batchSize == 0) || i == size) {
                    // 每达到 batchSize 就执行SQL
                    sqlSession.flushStatements();
                }
                i++;
            }
        });
    }

在 executeBatch 中 MyBatis-Plus 会循环调用 SqlSession.insert 缓存插入语句,每 batchSize 提交一次SQL。

进入 DefaultSqlSession.flushStatements():

  @Override
  public List<BatchResult> flushStatements() {
    try {
      return executor.flushStatements();
    } catch (Exception e) {
      throw ExceptionFactory.wrapException("Error flushing statements.  Cause: " + e, e);
    } finally {
      ErrorContext.instance().reset();
    }
  }

委托 BaseExecutor.flushStatements() 执行:

  @Override
  public List<BatchResult> flushStatements() throws SQLException {
    return flushStatements(false);
  }

  public List<BatchResult> flushStatements(boolean isRollBack) throws SQLException {
    if (closed) {
      throw new ExecutorException("Executor was closed.");
    }
    return doFlushStatements(isRollBack);
  }

最终 doFlushStatements() 方法由各个子类去实现,BaseExecutorBatchExecutorReuseExecutorSimpleExecutorClosedExecutorMybatisBatchExecutorMybatisReuseExecutorMybatisSimpleExecutor这几种实现。

Mybatis 开头的是 Mybatis-Plus 提供的实现,分别对应 MyBatis 的 simple、reuse、batch执行器类别。不管哪个执行器,里面都会有一个 StatementHandler 接口来负责具体执行SQL。

而在 MyBatis-Plus 批量插入的场景中,是由 MybatisBatchExecutor#doFlushStatements 执行的:

@Override
public List<BatchResult> doFlushStatements(boolean isRollback) throws SQLException {
    try {
        List<BatchResult> results = new ArrayList<>();
        if (isRollback) {
            return Collections.emptyList();
        }
        for (int i = 0, n = statementList.size(); i < n; i++) {
            Statement stmt = statementList.get(i);
            applyTransactionTimeout(stmt);
            BatchResult batchResult = batchResultList.get(i);
            try {
                // 1. 此处调用JDBC PreparedStatement API,批量执行SQL
                batchResult.setUpdateCounts(stmt.executeBatch());
                MappedStatement ms = batchResult.getMappedStatement();
                List<Object> parameterObjects = batchResult.getParameterObjects();
                KeyGenerator keyGenerator = ms.getKeyGenerator();
                if (Jdbc3KeyGenerator.class.equals(keyGenerator.getClass())) {
                    Jdbc3KeyGenerator jdbc3KeyGenerator = (Jdbc3KeyGenerator) keyGenerator;
                    // 2. 使用 jdbc3KeyGenerator 获取批量执行的所生成的 ID
                    jdbc3KeyGenerator.processBatch(ms, stmt, parameterObjects);
                } else if (!NoKeyGenerator.class.equals(keyGenerator.getClass())) { //issue #141
                    for (Object parameter : parameterObjects) {
                        keyGenerator.processAfter(this, ms, stmt, parameter);
                    }
                }
                // Close statement to close cursor #1109
                closeStatement(stmt);
            } catch (BatchUpdateException e) {
                StringBuilder message = new StringBuilder();
                message.append(batchResult.getMappedStatement().getId())
                    .append(" (batch index #")
                    .append(i + 1)
                    .append(")")
                    .append(" failed.");
                if (i > 0) {
                    message.append(" ")
                        .append(i)
                        .append(" prior sub executor(s) completed successfully, but will be rolled back.");
                }
                throw new BatchExecutorException(message.toString(), e, results, batchResult);
            }
            results.add(batchResult);
        }
        return results;
    } finally {
        for (Statement stmt : statementList) {
            closeStatement(stmt);
        }
        currentSql = null;
        statementList.clear();
        batchResultList.clear();
    }
}

在 1 处,执行批量插入语句后,然后在2处调用 Jdbc3KeyGenerator.jdbc3KeyGenerator 获取ID:

// org.apache.ibatis.executor.keygen.Jdbc3KeyGenerator#processBatch
public void processBatch(MappedStatement ms, Statement stmt, Object parameter) {
    final String[] keyProperties = ms.getKeyProperties();
    if (keyProperties == null || keyProperties.length == 0) {
      return;
    }
    // 本质上,还是调用JDBC Statement.getGeneratedKeys() 方法获取ID(参考文中2.1示例)
    try (ResultSet rs = stmt.getGeneratedKeys()) {
      final ResultSetMetaData rsmd = rs.getMetaData();
      final Configuration configuration = ms.getConfiguration();
      if (rsmd.getColumnCount() < keyProperties.length) {
        // Error?
      } else {
        assignKeys(configuration, rs, rsmd, keyProperties, parameter);
      }
    } catch (Exception e) {
      throw new ExecutorException("Error getting generated key or setting result to parameter object. Cause: " + e, e);
    }
}

但是我们项目中如果使用的 ShardingJDBC,那么此时调用的就是 ShardingPreparedStatement.getGeneratedKeys():

通过 DEBUG,我们发现在我们项目中 ShardingPreparedStatement.getGeneratedKeys() 返回的是null值:

这也就找到了为什么MyBatis-Plus 和 ShardingJDBC 一起使用时获取不到ID值的问题,问题的根节并不在MyBatis这边,而是 ShardingJDBC 实现的 PreparedStatement 获取不到key。

2.4 为什么执行MyBatis-Plus save方法可以获取到主键

当我们调用 MyBatis-Plus save() 方法保存单条数据时,底层实际上还是调用的 ShardingPreparedStatement.getGeneratedKeys() 方法,获取插入后的主键key:

@Override
public ResultSet getGeneratedKeys() throws SQLException {
    Optional<GeneratedKeyContext> generatedKey = findGeneratedKey();
    if (preparedStatementExecutor.isReturnGeneratedKeys() && generatedKey.isPresent()) {
        return new GeneratedKeysResultSet(generatedKey.get().getColumnName(), generatedValues.iterator(), this);
    }
    if (1 == preparedStatementExecutor.getStatements().size()) {
        return preparedStatementExecutor.getStatements().iterator().next().getGeneratedKeys();
    }
    return new GeneratedKeysResultSet();
}

但是在执行单条数据插入时,1 == preparedStatementExecutor.getStatements().size() 是成立的,就会返回底层被真实被代理的MySQL JDBC 的 Statement 获取主键key:

至于 AbstractStatementExecutor.statements 为什么在执行单一语句的时候statements里不为空,但是批量插入的时候,这个list为空,可以参考下面的回答:

AbstractStatementExecutor.statements是ShardingJDBC中的一个重要数据结构,它用于存储待执行的SQL语句及其对应的数据库连接信息。在进行SQL操作时,ShardingJDBC会根据你的分片策略将SQL语句路由到相应的数据库节点,并生成对应的数据结构存储在statements这个列表里。

那么,为什么在执行单一SQL语句时,statements不为空,而在批量插入时,这个列表却为空呢?这主要是因为ShardingJDBC处理这两种情况的方式有所不同。

  1. 对于单一SQL语句,ShardingJDBC将其路由到正确的数据库节点(可能是多个),然后创建对应的PreparedStatement对象,这些对象被存储在statements列表中,以便后续执行和获取结果。
  2. 对于批量插入,ShardingJDBC采取了一种“延迟执行”的策略。具体来说,ShardingJDBC首先会解析和拆分批量插入语句,然后将拆分后的单一插入语句暂存起来,而不是立即创建PreparedStatement对象。这就导致了在批量插入过程中,statements列表为空。这样做的主要目的是为了提高批量插入的性能,因为创建PreparedStatement对象和管理数据库连接都是需要消耗资源的。

三. 总结

本文由故障现象定位到了具体的问题点是因为 MyBatis-Plus 批量插入没有返回数据库组件,而跟踪源码后我们却发现是因为ShardingJDBC不支持批量插入获取主键值。

ShardingJDBC不支持批量插入后获取主键,主要是因为在批量插入操作中,ShardingJDBC可能需要将数据插入到多个不同的数据库节点,在这种情况下,每个节点都可能有自己的主键生成规则,并且这些节点可能并不知道其他节点的主键值。因此,如果你需要在批量插入后获取自动生成的主键,可能需要通过其他方式实现,例如使用全局唯一ID作为主键。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1038465.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

NebulaGraph实战:2-NebulaGraph手工和Python操作

图数据库是专门存储庞大的图形网络并从中检索信息的数据库。它可以将图中的数据高效存储为点&#xff08;Vertex&#xff09;和边&#xff08;Edge&#xff09;&#xff0c;还可以将属性&#xff08;Property&#xff09;附加到点和边上。本文以示例数据集basketballplayer为例…

java的Map和Set集合

Set集合 一.HashSet HashSet 元素是无序的 向Hashset中添加元素时&#xff0c;是如何判断元素是否重复的: 添加元素时&#xff0c;如果用equals判断效率太低&#xff0c;因为equals是一个一个字符比较 HashSet底层用到hashCode和equals 一个内容&#xff1a;"sahdihwo&q…

FPGA 安装Quartus 13.1无法生成.sof文件

FPGA 安装Quartus 13.1无法生成.sof文件 安装环境编译无法生成 .sof文件分析原因 找资料1.第1篇文章2.第2篇文章 安装环境 Quarter II 13.0下载、安装、破解包括可能出现的几乎所有的问题详解野火FPGA安装视频 编译无法生成 .sof文件 分析原因 1.推测可能是破解失败。2.安装…

洛谷bfs题2---P1825 [USACO11OPEN] Corn Maze S

P1825 [USACO11OPEN] Corn Maze S import java.util.LinkedList; import java.util.Queue; import java.util.Scanner;public class Main {public static int N;//行public static int M;//列public static Queue<Integer> q new LinkedList<>();public static in…

变量、因子、缺失值、类型转换、剔除多余变量、随机抽样、用R使用SQL、trim、na.rm=TRUE、数据标准化应用

变量&#xff1a;名义型、有序型、连续型变量 名义型&#xff1a;普通事件类型&#xff0c;如糖尿病I型和糖尿病II型。 有序型&#xff1a;有顺序的事件类型&#xff0c;如一年级、二年级和三年级。 连续型&#xff1a;表示有顺序的数量&#xff0c;如年龄。 因子&#xff1a;…

现代架构设计:构建可伸缩、高性能的系统

文章目录 架构设计的基本原则1. 可伸缩性2. 可用性和容错性3. 性能4. 安全性5. 简单性 现代架构设计的关键概念1. 微服务架构2. 容器化3. 云原生4. 自动化和持续集成/持续交付&#xff08;CI/CD&#xff09; 构建可伸缩、高性能的系统的最佳实践1. 合理使用缓存2. 负载均衡3. 弹…

在Python中处理CSV文件的常见问题

当谈到数据处理和分析时&#xff0c;CSV&#xff08;Comma-Separated Values&#xff09;文件是一种非常常见的数据格式。它简单易懂&#xff0c;可以被绝大多数编程语言和工具轻松处理。在Python中&#xff0c;我们可以使用各种库和技巧来处理CSV文件&#xff0c;让我们一起来…

Server2101

B-1:数据库服务渗透测试 任务环境说明: 服务器场景:Server2101 服务器场景操作系统:未知(关闭连接) 1.通过分析靶机Server2101页面信息,寻找漏洞页面,将WEB服务存在SQL注入漏洞的页面名称作为Flag提交; nmap -p- 扫描发现靶机80和443端口有http、https服务 访问网站…

Nginx访问认证

访问认证 有时候&#xff0c;我们⼀些站点内容想要进⾏授权查看&#xff0c;只能输⼊账号密码之后才能访问&#xff0c;例如⼀些重要的内⽹平台&#xff0c;CRM &#xff0c; CMDB &#xff0c;企业内部 WIKI 等等。 htpasswd是Apache密码⽣成⼯具&#xff0c;Nginx⽀持auth_ba…

多卫星定位算法

多卫星定位算法 现已知有N(N>4)个卫星&#xff0c;每个卫星的坐标用 X s {X_s} Xs​表示&#xff0c;其对应的伪距用 r r r表示。 由于伪距不是准确的、真实的距离&#xff0c;它有所干扰。所以我们可以再根据三维空间中的距离公式&#xff0c;另外估计卫星和用户的距离为 …

Visual Studio Cpp CLR C# 替换

1、首先将文件中所有都替换 你需要的名字 替换为整个解决方案 2、新建工程取名 Laserbeam_upper 3、把原工程下的cpp放进来&#xff0c;并改名Laserbeam_upper 4、在这里逐步添加 属性表配置opencv 5、cpp需要修改的两个地方 6、CLR新建和添加 选类库新建、然后直接粘贴进来…

Learn Prompt-Prompt 高级技巧:API-Bank AgentBench

模型评估是Agent学习过程中至关重要的一环。通过分析数据来评估Agent的能力&#xff0c;可以客观地衡量它在特定任务或领域中的表现。数据评估是不断迭代和改进的基础。通过反复评估和分析数据&#xff0c;Agent可以逐步改进自身&#xff0c;并不断优化其能力。数据评估还可以将…

浅谈为什么多态只能是指针或引用

其实在很早之前&#xff0c;我一直没有注意到这个问题&#xff0c;直到今天碰见了一道题&#xff0c;顺便前面的博客中&#xff0c;继承写到&#xff0c;子类中不包含父类&#xff0c;子类只是继承了父类的成员变量和函数&#xff0c;由这一点&#xff0c;引发了我对切片以及赋…

I/O设备管理

目录 I/O设备管理&#xff08;1&#xff09; 第一节 I/O设备管理的基本概念 一、I/O设备管理的任务 二、I/O设备分类 三、I/O设备管理与文件管理的关系 第二节 I/O硬件和I/O软件的组成 一、I/O硬件组成 二、I/O软件组成 三、设备独立性 第三节 I/O设备控制方式 一…

虚拟机安装 centos

title: 虚拟机安装 centos createTime: 2020-12-13 12:00:27 updateTime: 2020-12-13 12:00:27 categories: linux tags: 虚拟机安装 centos 路线图 主机(宿主机) —> centos --> docker --> docker 镜像 --> docker 容器 — docker 服务 1.前期准备 一台 主机 或…

图像相关名词概述

颜色模式 通过赋予C的不同维度不同的含义&#xff0c;可以用来描述不同的颜色空间。颜色模式&#xff0c;是将某种颜色表现为数字形式的模型&#xff0c;或者说是一种记录图像颜色的方式。本单元主要讲述两个常用的颜色模式&#xff1a;RGB&#xff0c;HSV。 RGB模式是工业界的…

数字IC笔试千题解--多选题篇(三)

前言 出笔试题汇总&#xff0c;是为了总结秋招可能遇到的问题&#xff0c;做题不是目的&#xff0c;在做题的过程中发现自己的漏洞&#xff0c;巩固基础才是目的。 所有题目结果和解释由笔者给出&#xff0c;答案主观性较强&#xff0c;若有错误欢迎评论区指出&#xff0c;资料…

机器学习(19)---XGBoost入门

XGBoost 一、概述1.1 使用XGBoost库1.2 XGBoost的三大板块 二、集成算法及重要参数2.1 概述2.2 XGBoost的简单建模2.3 n_estimators学习曲线2.4 方差与泛化误差2.5 重要参数subsample2.6 迭代决策树&#xff1a;重要参数eta 三、XGBoost的智慧3.1 概述3.2 XGB的目标函数&#x…

26. 图论 - 树

文章目录 树生成树最小生成树图与人工智能人工智能数学基础Hi,你好。我是茶桁。 这一节课是我们AI秘籍整个数学篇的最后一节课。同样的,这节课的概念还是比较重要的。我们要来了解一下「树」。 树 树其实是图的一种,首先呢它是一个连通图,是一个不含圈的连通图。 什么叫…

开机自启动Linux and windows

1、背景 服务器由于更新等原因重启&#xff0c;部署到该服务上的响应的应用需要自启动 2、Linux 2.1 方式一 编写启动应用的sh脚本授权该脚本权限 chmod 777 xxx.sh 修改rc.loacl 位置&#xff1a;/etc/rc.local 脚本&#xff1a;sh /home/xxxx.sh & 授权rc.local …