开心一刻
为了王者快速上分,微信上找了个陪玩
我:王者陪玩多少钱一个小时
陪玩:30一小时,哥
我转了30后,发了个:你没钱你活着干啥
陪玩:30块钱你装你妈呢?
我:这是我的游戏ID
我:你打的这句话,我也希望是你的游戏ID
陪玩撤回了一条消息,来了句:哥,不好意思,我手快了

ArrayIndexOutOfBoundsException
ob-mysql 集群部署,版本:5.7.25-OceanBase-v3.2.4.8

mysql-connector-java 版本:8.0.28
mybatis-plus 版本:3.1.0
表:tbl_order
java
-- 在 OceanBase 的 test 库中执行
CREATE TABLE IF NOT EXISTS `tbl_order` (
`id` INT NOT NULL AUTO_INCREMENT COMMENT '主键',
`order_no` VARCHAR(64) NOT NULL COMMENT '订单编号',
`goods_name` VARCHAR(255) NOT NULL COMMENT '商品名称',
`amount` DECIMAL(10, 2) NOT NULL COMMENT '金额',
`user_id` INT NOT NULL COMMENT '用户ID',
PRIMARY KEY (`id`)
) COMMENT='订单表';
因为问题是偶现,所以我写了个容易复现问题的 demo
java
package com.qsl.test;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.qsl.entity.TblOrder;
import com.qsl.service.TblOrderService;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import javax.annotation.Resource;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
/**
* 批量插入 + 更新,模拟 MySQL JDBC 8.0.28 + OceanBase 下的 ArrayIndexOutOfBoundsException
*
* @author: 青石路
* @date: 2026/8/26
*/
@RunWith(SpringRunner.class)
@SpringBootTest
public class BatchInsertTest {
@Resource
private TblOrderService orderService;
/**
* 多线程批量插入 + 批量更新测试
*
* 测试场景:
* - 10 个线程并发执行
* - 每个线程执行 10 轮,每轮:插入 500 条 + 更新 500 条
* - 总计:100 批插入(50000 条)+ 100 批更新(50000 条)
*
* 目的:通过高并发 + 跨节点路由,增加触发 ArrayIndexOutOfBoundsException 的概率
*/
@Test
public void testBatchInsertAndUpdateMultiThread() throws InterruptedException {
int threadCount = 10; // 线程数
int batchesPerThread = 10; // 每个线程执行的批次数
int batchSize = 500; // 每批记录数
ExecutorService executor = Executors.newFixedThreadPool(threadCount);
CountDownLatch latch = new CountDownLatch(threadCount);
List<Throwable> exceptions = new CopyOnWriteArrayList<>();
System.out.println("=== 开始多线程批量测试 ===");
System.out.println("线程数: " + threadCount + ", 每线程批数: " + batchesPerThread + ", 每批大小: " + batchSize);
System.out.println("总计: " + (threadCount * batchesPerThread) + " 批插入 + " + (threadCount * batchesPerThread) + " 批更新");
for (int t = 0; t < threadCount; t++) {
final int threadId = t;
executor.submit(() -> {
try {
for (int b = 0; b < batchesPerThread; b++) {
// 生成唯一的订单号前缀
String prefix = "T" + threadId + "B" + b + "_";
// 1. 批量插入
List<TblOrder> insertList = buildOrderList(batchSize, prefix);
boolean insertResult = orderService.saveBatch(insertList);
System.out.println("[线程" + threadId + "] 插入批次 " + b + ": " + insertResult);
// 2. 查询刚插入的记录(获取自增ID)
List<String> orderNos = insertList.stream()
.map(TblOrder::getOrderNo)
.collect(Collectors.toList());
List<TblOrder> insertedOrders = orderService.list(
new LambdaQueryWrapper<TblOrder>()
.in(TblOrder::getOrderNo, orderNos)
);
// 3. 批量更新
insertedOrders.forEach(order -> {
order.setAmount(order.getAmount().add(new BigDecimal("10")));
order.setGoodsName(order.getGoodsName() + "_updated");
});
boolean updateResult = orderService.updateBatchById(insertedOrders);
System.out.println("[线程" + threadId + "] 更新批次 " + b + ": " + updateResult);
}
} catch (Throwable e) {
exceptions.add(e);
System.err.println("[线程" + Thread.currentThread().getName() + "] 异常: " + e.getMessage());
e.printStackTrace();
} finally {
latch.countDown();
}
});
}
// 等待所有线程完成,最多等待 10 分钟
boolean completed = latch.await(10, TimeUnit.MINUTES);
executor.shutdown();
System.out.println("\n=== 测试完成 ===");
System.out.println("是否超时: " + !completed);
System.out.println("异常总数: " + exceptions.size());
// 打印所有异常堆栈
if (!exceptions.isEmpty()) {
System.out.println("\n=== 异常详情 ===");
exceptions.forEach(e -> {
System.out.println("---");
e.printStackTrace();
});
}
}
private List<TblOrder> buildOrderList(int count, String prefix) {
List<TblOrder> list = new ArrayList<>(count);
for (int i = 1; i <= count; i++) {
TblOrder order = new TblOrder()
.setOrderNo(prefix + System.currentTimeMillis() + String.format("%04d", i))
.setGoodsName("测试商品" + prefix + i)
.setAmount(new BigDecimal("99.99"))
.setUserId(i);
list.add(order);
}
return list;
}
}
代码很简单,就是 10 个线程同时批量插入、更新;执行下,大概率会异常:ArrayIndexOutOfBoundsException

问题排查
出异常堆栈信息来看,跟我们代码相关的就一行
java
at com.qsl.test.BatchInsertTest.lambda$testBatchInsertAndUpdateMultiThread$1(BatchInsertTest.java:77)
也就是
boolean insertResult = orderService.saveBatch(insertList);
不就是简单的调用 mybatis-plus 的批量保存接口吗,能有什么问题?很显然,一眼看不出原因,那怎么办呢?
当下肯定是问 AI 呀,直接将异常堆栈丢给AI,让其分析可能的原因

Qoder 很快给出了根因

说白了就是 MySQL JDBC 驱动(8.0.28)与 OceanBase (3.2.4.8)不兼容,OceanBase并未响应驱动想要的信息
从出现问题到找到原因,你们是不是觉得很快?但实际情况可能比 demo 要复杂很多,比如OceanBase部署模式不一致(本地单节点、客户现场是集群)、OceanBase 版本不一致、没有那么详细的上下文供 AI 读取等等,那找到原因的速度就要慢很多了
当然,除了 AI 我们还能从 OceanBase 官方搜索原因
我们要知道:我们遇到的问题肯定有其他人已经碰到过了(前提是确定不是我们写的代码问题)

也能很快找到原因。但实际情况是,我们往往不会去 OceanBase 官方寻找原因,因为我们潜意识里认为跟 OceanBase 没关系
问题解决
AI 找到了原因,自然会给出解决方案

两种方案我都试了,确实都没问题。AI 推荐的方案是:JDBC URL 加 trackSessionState=true,因为调整简单嘛,但 OceanBase 官方推荐调整 MySQL JDBC 驱动版本

但我觉得这两个方案都不是最合适的
既然用的是 OceanBase 数据库,为什么不用 OceanBase 官方提供的驱动?
用 OceanBase 官方的 JDBC 驱动,是不是才是最兼容的?我带着这个疑问问了 Qoder

所以最佳解决方案是用 oceanbase-client,调整两处即可
-
pom.xml
将原来的
xml<dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.25</version> </dependency>替换成
xml<dependency> <groupId>com.oceanbase</groupId> <artifactId>oceanbase-client</artifactId> <version>2.4.12</version> </dependency> -
application.yml
driver-class-name 和 url 调整 oceanbase 的写法
yamlspring: datasource: driver-class-name: com.oceanbase.jdbc.Driver url: jdbc:oceanbase://192.168.1.150:2883/qsl?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&useSSL=false&zeroDateTimeBehavior=convertToNull&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
总结
- 当下 AI 这么强大的情况下,首选它来排查问题是最高效的;上下文给的越足越好
- 选择官配、官配、官配,不要选择第三者,官配才是兼容性最好的(仅仅指驱动,你们不要乱想哈)
- AI 给的方案一定是最优的吗,不一定,我们需要甄别,不要盲目信奉它