likes
comments
collection
share

解析 JDBC 的批处理参数 rewriteBatchedStatements

作者站长头像
站长
· 阅读数 33

提升SQL性能的一大原则就是降低访问数据库的次数,能合并下发的SQL是很好的手段。也就是将insert into t values();insert into t values();转换成insert into t values(),();。 在JDBC中,这种转换的大概流程是这样的:

PreparedStatement preparedStatement = conn.prepareStatement("insert into t (c1,c2) values(?,?)");

判断执行的SQL是否可以做batch重写。这里需要url使用rewriteBatchedStatements=true,会变成insert into t (id) values(1),(2)。这里其实有一些本身应该符合预期的情况却出现rewriteBatched失效的情况。


我们看下JDBC源码: 直接来到 ParseInfo.java,这里是对SQL进行解析进而进行条件判断,JDBC解析SQL的方式比较粗暴,通过字符判断,通过I判断是否是insert语句、通过L判断是否是load data语句、通过on, duplicate, key, update判断是否是on duplicate key update语句,通过?获得preparestatment占位符的位置,通过;判断这个请求包含几条SQL等。

比如占位符的判断,这里会记录下每一个?的位置,用staticSql进行分组,例如insert into t (c1) values (?)会解析成长度为3的数组:

  1. INSERT INTO t (c1,c2) VALUES (
  2. ,
  3. )
for(i = this.statementStartPos; i < this.statementLength; ++i){
    char c = sql.charAt(i);
    if ((c == '?')) {
        endpointList.add(new int[] { lastParmEnd, i });
        lastParmEnd = i + 1;
    }
}

之后判断是否可以进行SQL重写,判断条件有3个,最基本的2个条件:

  1. this.numberOfQueries == 1,必须是一条SQL,即不能是sql;sql的形式。
  2. !this.parametersInDuplicateKeyClause,这指的是on duplicate key update 后不能有占位符。举个例子,insert into t (c1) values (?) on duplicate key update c1 = ?是不能重写的,insert into t (c1) values (?) on duplicate key update c1 = 123这种可以。(感觉这里JDBC实现的偷懒了) 除了这两个条件必须为true,还有canRewrite()函数是判断条件,我们去看看里面的逻辑:
  3. 如果是insert语句,如果SQL中存在select,则不能被重写。
  4. on duplicate key update 语句不能使用 LAST_INSERT_ID
if (StringUtils.startsWithIgnoreCaseAndWs(sql, "INSERT", statementStartPos)) {
    if (StringUtils.indexOfIgnoreCase(statementStartPos, sql, "SELECT", "\"'`", "\"'`", StringUtils.SEARCH_MODE__MRK_COM_WS) != -1) {
        return false;
    }
    if (isOnDuplicateKeyUpdate) {
        int updateClausePos = StringUtils.indexOfIgnoreCase(locationOfOnDuplicateKeyUpdate, sql, " UPDATE ");
        if (updateClausePos != -1) {
            return StringUtils.indexOfIgnoreCase(updateClausePos, sql, "LAST_INSERT_ID", "\"'`", "\"'`", StringUtils.SEARCH_MODE__MRK_COM_WS) == -1;
        }
    }
    return true;
}

所以这里有一个比较特殊的case,如果SQL中包含select,则rewriteBatchedStatements失效。比如下面这个例子,表结构如下:

mysql> desc t1;
+------------+----------+------+------+---------+-------+
| Field      | Type     | Null | Key  | Default | Extra |
+------------+----------+------+------+---------+-------+
| id         | int(11)  | YES  |      | NULL    |       |
| selectNum  | int(11)  | YES  |      | NULL    |       |
| updateTime | datetime | YES  |      | NULL    |       |
+------------+----------+------+------+---------+-------+
3 rows in set (0.01 sec)

使用上面的方式进行批量插入,理想情况到数据库应该是 insert into t (id, selectNum, updateTime) values(1,11,'2021-01-01 11:11:11'),(1,11,'2021-01-01 11:11:11');插入两条数据。 我们debug一下看看数据库中收到的是什么

解析 JDBC 的批处理参数 rewriteBatchedStatements

可见,数据库收到的还是单SQL,没有整合成一条语句。所以考虑到batch insert的情况,表结构尽量不要带有select字符。

而正常情况,应该会开始构建batch value结构,是在buildRewriteBatchedParams

if (this.canRewriteAsMultiValueInsert && session.getPropertySet().getBooleanProperty(PropertyKey.rewriteBatchedStatements).getValue()) {
    buildRewriteBatchedParams(sql, session, encoding);
}

这里会解析value后的内容,比如insert into t (c1,c2) values (?,?)就是(?,?)。这样SQLvalueparseInfo都有了。

excuteBatch

我们直接看如果符合canRewriteAsMultiValueInsertAtSqlLevel是如何处理的,在executeBatchedInserts。 获取这个insert batch有几个需要做合并的,这里的batchedArgs是程序执行addBatch()时候赋值的。

int numBatchedArgs = this.query.getBatchedArgs().size();

prepareBatchedInsertSQL中的((PreparedQuery<?>) this.query).getParseInfo().getParseInfoForBatch(numBatches)value进行拼接。 JDBC中实现了一个visitor,会将SQLvalue的组成merge到一起:

解析 JDBC 的批处理参数 rewriteBatchedStatements

  1. INSERT INTO t (c1,c2) VALUES (
  2. ,
  3. ),(
  4. ,
  5. )

结果就是拼出来的是insert into t (c1,c2) values(,),(,) 这里还需要填写占位符,所以程序会来到这里getSqlForBatch,这里用到的sqlStrings就是上面提到的visitor数组,可以看到,循环这个数组,在每个的中间补上?就变成insert into t (c1,c2) values(?,?),(?,?)了。

final byte[][] sqlStrings = this.staticSql;
final int sqlStringsLength = sqlStrings.length;

for (int i = 0; i < sqlStringsLength; i++) {
    size += sqlStrings[i].length;
    size++; // for the '?'
}

StringBuilder buf = new StringBuilder(size);

for (int i = 0; i < sqlStringsLength - 1; i++) {
    buf.append(StringUtils.toString(sqlStrings[i], this.charEncoding));
    buf.append("?");
}

buf.append(StringUtils.toString(sqlStrings[sqlStringsLength - 1]));

return buf.toString();

这样整个拼接流程就结束了。可以看到,有的时候虽然我们设置了rewriteBatchedStatements=true,但在某些特殊的语句格式下,拼接依然不能生效,在实现上,感觉JDBC实现的比较偷懒,比如不能有select,这个可能处于考虑到insert into ... select这种情况,但简单粗暴的禁止,也会给很多人带来困惑。

转载自:https://juejin.cn/post/7004385021330980901
评论
请登录