首页 > 其他分享 >奇妙的 JDBC batch insert 到 Doris 异常的问题

奇妙的 JDBC batch insert 到 Doris 异常的问题

时间:2024-09-25 20:34:16浏览次数:11  
标签:insert JDBC return String INSERT ...... batch && sql

遇到一个很奇怪的异常,通过 JDBC batch insert 时,会报 `Unknown command(27)` 的异常。
![exception.png](https://upload-images.jianshu.io/upload_images/13187386-c3138cbb820d3f21.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

而且这个问题很容易复现,复现例子:

1. 建表语句

```SQL
create table t_selection_test (
a VARCHAR(96),
b VARCHAR(20),
c VARCHAR(96),
d DECIMAL(38, 10)
) DUPLICATE KEY (a)
DISTRIBUTED BY HASH(a) BUCKETS 10
PROPERTIES (
"replication_num" = "1"
);
```

2. 写入代码

```java
Connection conn = DriverManager.getConnection(
"jdbc:mysql://127.0.0.1:9030/test?rewriteBatchedStatements=true",
"root", "");
String sql = "INSERT INTO t_select_test (a, b, c, d) VALUES(?, ?, ?, ?)";
PreparedStatement ps = conn.prepareStatement(sql);

for (int i = 0; i < 4; i++) {
ps.setString(1, "a");
ps.setString(2, "b");
ps.setString(3, "c");
ps.setBigDecimal(4, BigDecimal.TEN);
ps.addBatch();
}

ps.executeBatch();
```

最开始怀疑是 Doris 的问题,查看 Fe 源码发现,Mysql 解析的代码在 `org.apache.doris.qe.MysqlConnectProcessor#dispatch` 方法

```java
private void dispatch() throws IOException {
int code = packetBuf.get();
MysqlCommand command = MysqlCommand.fromCode(code);
if (command == null) {
ErrorReport.report(ErrorCode.ERR_UNKNOWN_COM_ERROR);
ctx.getState().setError(ErrorCode.ERR_UNKNOWN_COM_ERROR, "Unknown command(" + code + ")");
LOG.warn("Unknown command(" + code + ")");
return;
}

......

}
```

其中 `MysqlCommand` 中确实没有 code 为 27 的命令定义,查看 MySQL 的命令报文定义,发现 27(16 进制的 0x1B)为 `COM_SET_OPTION` 指令,用于打开或关闭多语句执行,具体定义如下:

只有两个枚举值

- MYSQL_OPTION_MULTI_STATEMENTS_ON - 1
- MYSQL_OPTION_MULTI_STATEMENTS_OFF - 0

这个时候就感觉可能是 mysql-connector-java 的问题,又试了几个版本的 connector 包,最终发现在 8.0.28 以及之前的版本会有这个问题,然后就开始了我们的 debug 之旅。

## 问题分析

版本众多,我们就只来看看问题分界的两个版本:8.0.28 和 8.0.29

### 8.0.28

首先我们通过测试代码中的 `ps.executeBatch()` 方法进入

调用的是实现类方法 `com.mysql.cj.jdbc.StatementImpl#executeBatch`

```java
public int[] executeBatch() throws SQLException {
return Util.truncateAndConvertToInt(executeBatchInternal());
}
```

进来直接调用 `executeBatchInternal` 方法,实际是调用的实现类方法 `com.mysql.cj.jdbc.ClientPreparedStatement#executeBatchInternal`

```java
protected long[] executeBatchInternal() throws SQLException {
synchronized (checkClosed().getConnectionMutex()) {

......

try {

......

if (!this.batchHasPlainStatements && this.rewriteBatchedStatements.getValue()) {
// line 425
if (getParseInfo().canRewriteAsMultiValueInsertAtSqlLevel()) {
return executeBatchedInserts(batchTimeout);
}

if (!this.batchHasPlainStatements && this.query.getBatchedArgs() != null
&& this.query.getBatchedArgs().size() > 3 /* cost of option setting rt-wise */) {
// line 431
return executePreparedBatchAsMultiStatement(batchTimeout);
}
}

return executeBatchSerially(batchTimeout);
} finally {
this.query.getStatementExecuting().set(false);

clearBatch();
}
}
}
```

 

在 425 行 `canRewriteAsMultiValueInsertAtSqlLevel` 判断为 false,继续向下执行(这个地方很重要,我们后面分析)

然后会最终走到 431 行的 `executePreparedBatchAsMultiStatement` 方法中

```java
protected long[] executePreparedBatchAsMultiStatement(int batchTimeout) throws SQLException {
synchronized (checkClosed().getConnectionMutex()) {

......

try {

......

try {
if (!multiQueriesEnabled) {
// line 494
((NativeSession) locallyScopedConn.getSession()).enableMultiQueries();
}

......

} finally {
if (batchedStatement != null) {
batchedStatement.close();
batchedStatement = null;
}
}

......

}
}
```

然后我们继续执行,直到执行 494 行的 `enableMultiQueries` 异常发生

然后我们再次 debug 进入到这个方法,看看它干了什么

```java
public void enableMultiQueries() {
sendCommand(this.commandBuilder.buildComSetOption(((NativeProtocol) this.protocol).getSharedSendPacket(), 0), false, 0);
((NativeServerSession) getServerSession()).preserveOldTransactionState();
}
```

诶,就是它了,`buildComSetOption` ,顾名思义,构建了 `COM_SET_OPTION` 命令并发送。

找到了罪魁祸首,我们就来看看为什么他要发送这个指令

回到 425 行的这个判断,为什么明明我们在 JDBC url 上面开启了 `rewriteBatchedStatements`,但是却没有执行呢?

我们点开 `canRewriteAsMultiValueInsertAtSqlLevel` 方法,发现只是简单的返回了 `ParseInfo` 的 `canRewriteAsMultiValueInsert` 属性,默认值为 `false`, 那我们看看这个属性有没有被赋值

```java
public boolean canRewriteAsMultiValueInsertAtSqlLevel() {
return this.canRewriteAsMultiValueInsert;
}
```

经过 Debug 发现,在`ClientPreparedStatement` 初始化时,调用 `com.mysql.cj.ParseInfo#ParseInfo(java.lang.String, com.mysql.cj.Session, java.lang.String)` 构造方法后再调用`com.mysql.cj.ParseInfo#ParseInfo(java.lang.String, com.mysql.cj.Session, java.lang.String, boolean)` 这个构造方法构建 `ParseInfo` 对象

```java
public ParseInfo(String sql, Session session, String encoding) {
this(sql, session, encoding, true);
}

public ParseInfo(String sql, Session session, String encoding, boolean buildRewriteInfo) {

......

// line 185
if (buildRewriteInfo) {
this.canRewriteAsMultiValueInsert = this.numberOfQueries == 1 && !this.parametersInDuplicateKeyClause
&& canRewrite(sql, this.isOnDuplicateKeyUpdate, this.locationOfOnDuplicateKeyUpdate, this.statementStartPos);
if (this.canRewriteAsMultiValueInsert && session.getPropertySet().getBooleanProperty(PropertyKey.rewriteBatchedStatements).getValue()) {
buildRewriteBatchedParams(sql, session, encoding);
}
}
}
```

 

然后我们继续向下看

在 185 行,由于`buildRewriteInfo` 为 `true`,进入到 if 语句中

`canRewriteAsMultiValueInsert` 为 3 个判断条件的的与值, 其中:

1. `this.numberOfQueries == 1` 判断是不是只有一个语句,这个当然为 `true`

2. `!this.parametersInDuplicateKeyClause` 判断参数不在 `ON DUPLICATE KEY` 的语法中,这个也为 `true`

3. `canRewrite` 这个方法我们跟进去看

```java
protected static boolean canRewrite(String sql, boolean isOnDuplicateKeyUpdate, int locationOfOnDuplicateKeyUpdate, int statementStartPos) {
// Needs to be INSERT or REPLACE.
// Can't have INSERT ... SELECT or INSERT ... ON DUPLICATE KEY UPDATE with an id=LAST_INSERT_ID(...).

if (StringUtils.startsWithIgnoreCaseAndWs(sql, "INSERT", statementStartPos)) {
// line 660
if (StringUtils.indexOfIgnoreCase(statementStartPos, sql, "SELECT", OPENING_MARKERS, CLOSING_MARKERS, SearchMode.__MRK_COM_MYM_HNT_WS) != -1) {
return false;
}
if (isOnDuplicateKeyUpdate) {
int updateClausePos = StringUtils.indexOfIgnoreCase(locationOfOnDuplicateKeyUpdate, sql, " UPDATE ");
if (updateClausePos != -1) {
return StringUtils.indexOfIgnoreCase(updateClausePos, sql, "LAST_INSERT_ID", OPENING_MARKERS, CLOSING_MARKERS,
SearchMode.__MRK_COM_MYM_HNT_WS) == -1;
}
}
return true;
}

return StringUtils.startsWithIgnoreCaseAndWs(sql, "REPLACE", statementStartPos)
&& StringUtils.indexOfIgnoreCase(statementStartPos, sql, "SELECT", OPENING_MARKERS, CLOSING_MARKERS, SearchMode.__MRK_COM_MYM_HNT_WS) == -1;
}
```

 

有注释提示,只有 INSERT 或 REPLACE 语句才能被重写,而 `INSERT SELECT` 和 `INSERT ON DUPLICATE KEY UPDATE` 语法不能被重写

这个 `SELECT` 是不是有点似曾相识? 我们接着往下走

直到走到 660 行这个判断,然后返回了 `false`

恍然大悟,这个地方判断 sql 是不是 `INSERT SELECT` 语句,因为我们表名中包含 select 字串,所以这个 `StringUtils.indexOfIgnoreCase` 方法返回值不是 -1

而 `SearchMode.__MRK_COM_MYM_HNT_WS` 配合第 4、5 两个参数,不匹配两个标记所引用的内容,这也就是为什么表名通过重音标号引用后没有出现异常

出现问题的地方找到了,我们再来看执行正常的版本是如何处理的

### 8.0.29

还是通过测试代码中的 `ps.executeBatch()` 方法进入

一样走到`com.mysql.cj.jdbc.ClientPreparedStatement#executeBatchInternal`

```java
protected long[] executeBatchInternal() throws SQLException {
synchronized (checkClosed().getConnectionMutex()) {

......

try {

......

if (!this.batchHasPlainStatements && this.rewriteBatchedStatements.getValue()) {
// line 408
if (getQueryInfo().isRewritableWithMultiValuesClause()) {
return executeBatchedInserts(batchTimeout);
}

if (!this.batchHasPlainStatements && this.query.getBatchedArgs() != null
&& this.query.getBatchedArgs().size() > 3 /* cost of option setting rt-wise */) {
return executePreparedBatchAsMultiStatement(batchTimeout);
}
}

return executeBatchSerially(batchTimeout);
} finally {
this.query.getStatementExecuting().set(false);

clearBatch();
}
}
}
```

走到 408 行,`isRewritableWithMultiValuesClause` 判断为 `true`,调用 `executeBatchedInserts` 执行批量写入,正常结束返回

这个地方和 8.0.28 版本明显不一样了,我们来看看 `isRewritableWithMultiValuesClause` 是如何判断的

这块也是相似的直接返回了 `isRewritableWithMultiValuesClause` 成员变量的值

```java
public boolean isRewritableWithMultiValuesClause() {
return this.isRewritableWithMultiValuesClause;
}
```

debug 看到在构建 `ClientPreparedStatement` 时, 通过 `com.mysql.cj.QueryInfo#QueryInfo(java.lang.String, com.mysql.cj.Session, java.lang.String)` 构造方法构造 `QueryInfo` 对象时进行了赋值

```java
public QueryInfo(String sql, Session session, String encoding) {

......
// line 97
boolean rewriteBatchedStatements = session.getPropertySet().getBooleanProperty(PropertyKey.rewriteBatchedStatements).getValue();
......

// Skip comments at the beginning of queries.
this.queryStartPos = strInspector.indexOfNextAlphanumericChar();
if (this.queryStartPos == -1) {
this.queryStartPos = this.queryLength;
} else {
this.numberOfQueries = 1;
this.statementFirstChar = Character.toUpperCase(strInspector.getChar());
}

// Only INSERT and REPLACE statements support multi-values clause rewriting.
// line 127
boolean isInsert = strInspector.matchesIgnoreCase(INSERT_STATEMENT) != -1;
if (isInsert) {
strInspector.incrementPosition(INSERT_STATEMENT.length()); // Advance to the end of "INSERT".
}
boolean isReplace = !isInsert && strInspector.matchesIgnoreCase(REPLACE_STATEMENT) != -1;
if (isReplace) {
strInspector.incrementPosition(REPLACE_STATEMENT.length()); // Advance to the end of "REPLACE".
}

// Check if the statement has potential to be rewritten as a multi-values clause statement, i.e., if it is an INSERT or REPLACE statement and
// 'rewriteBatchedStatements' is enabled.
boolean rewritableAsMultiValues = (isInsert || isReplace) && rewriteBatchedStatements;

......
// complex grammar check

// line 271
this.isRewritableWithMultiValuesClause = rewritableAsMultiValues;

......
}
```

首先在 97 行,获取到 session 中设置的 `rewriteBatchedStatements` 属性

在 127 行,判断是不是 `INSERT` 或 `REPLACE` 语句,并且开启了 `rewriteBatchedStatements` 属性,

然后进行了一系列复杂的匹配,来判断是否为 `INSERT INTO VALUES` 这样的语法,并且带有 ? 占位符

最后在 271 行把判断结果赋值给 `isRewritableWithMultiValuesClause`

## 总结

所以,这个异常的问题就在于,判断是否可以重写 batch statement sql 的时候,对于 `INSERT INTO VALUES` 语法的判断上,8.0.28 以及更早的版本的逻辑不够严谨,8.0.29 上面进行了更加复杂的判断,从而避免了这样的错误

标签:insert,JDBC,return,String,INSERT,......,batch,&&,sql
From: https://www.cnblogs.com/gnehil/p/18432142

相关文章

  • ShardingSphere-JDBC垂直分片
    文章目录1、订单库db_order1.2、创建数据库2、用户库db_user2.1、创建atguigu-mysql-user容器2.2、登录atguigu-mysql-user容器2.3、设置密码2.4、创建数据库3、创建实体类TOrder4、创建实体类TUser5、创建TOrderMapper6、创建TUserMapper7、application.yml......
  • 【YashanDB知识库】如何配置jdbc驱动使getDatabaseProductName()返回Oracle
    本文转自YashanDB官网,具体内容请见https://www.yashandb.com/newsinfo/7352676.html?templateId=1718516问题现象某些三方件,例如工作流引擎activiti,暂未适配yashandb,使用中会出现如下异常:问题的风险及影响影响客户业务无法进行。问题影响的版本所有的yashandbjdbc驱动版本。问题......
  • Java反序列化利用链篇 | JdbcRowSetImpl利用链分析
    JdbcRowSetImpl利用链前言首先说明一下:利用链都有自己的使用场景,要根据场景进行选择不同的利用链。JdbcRowSetImpl利用链用于fastjson反序列化漏洞中。为什么?因为fastjson会在反序列化类时自动调用set开头的方法(不一定是setter方法),而JdbcRowSetImpl中存在一个set开头的方法,即......
  • Java后端开发中的任务调度:使用Spring Batch实现批处理
    Java后端开发中的任务调度:使用SpringBatch实现批处理大家好,我是微赚淘客返利系统3.0的小编,是个冬天不穿秋裤,天冷也要风度的程序猿!在现代企业应用中,批处理是处理大规模数据的重要方式。SpringBatch为我们提供了强大的工具来实现批处理任务。本文将详细介绍如何使用SpringBatch......
  • JDBC中Druid连接池的配置与使用
    Druid连接池:        支持所有JDBC兼容的数据库,包括Oracle、MySql、Derby、SQLServer等。        简单SQL语句用时10微秒以内,复杂SQL用时30微秒。        网站:https://github.com/alibaba/druid/releases应用: 1.复制上面的链接下载druid.文件,......
  • SpringBoot整合ShardingJdbc分表
    项目中处理接收设备上报日志需求,上报数据量大,因此对数据进行按日期分表处理。使用技术:ShardingJdbc+rabbitMq+jpa+多线程处理引入所需jar:<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</a......
  • 1. 如何在Java中连接MySQL数据库?请解释使用JDBC连接的步骤。
    要在Java中连接MySQL数据库,通常使用JDBC(JavaDatabaseConnectivity)API。这是一个用于执行SQL语句的JavaAPI,可以用来访问关系型数据库。下面是使用JDBC连接MySQL数据库的详细步骤:1.添加MySQLJDBC驱动首先,需要确保项目中包含MySQL的JDBC驱动程序。这个驱动程序通常是一个......
  • ‌‌JDBC和‌ODBC的区别
    JDBC和ODBC都是用于数据库连接的接口,但它们在技术背景、跨平台性、驱动程序来源、使用方式和配置、性能和安全性以及应用场景等方面存在显著差异。‌技术背景和语言支持‌JDBC是‌Java数据库连接技术,完全基于Java语言,因此与Java程序无缝集成。ODBC是一种开放、标准化的数据库连......
  • YOLOV8 det 多batch TensorRT 推理(python )
    由于我当前的项目需求是推理四张,所以,demo部分也是基于4张进行演示的,不过基于此套路,可以实现NCHW的任意尺度推理,推理代码如下:importnumpyasnpfromnumpyimportndarrayfromtypingimportList,Tuple,UnionfrommodelsimportTRTModule#isort:skipimportar......
  • JDBC简介与应用:Java数据库连接的核心概念和技术
    简短介绍JDBC及其重要性。简短介绍JDBCJDBC(JavaDatabaseConnectivity)是一种用于执行SQL语句的JavaAPI并且独立于特定的数据库厂商。它允许开发者以一种标准的方式从Java应用程序中访问关系型数据库,这意味着一旦你掌握了JDBC的基本操作,你可以轻松地将你的应用程......