好好的OceanBase官方驱动你不用,非要用第三方驱动,ArrayIndexOutOfBoundsException了吧

开心一刻

为了王者快速上分,微信上找了个陪玩
我:王者陪玩多少钱一个小时
陪玩: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

-- 在 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

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

ArrayIndexOutOfBoundException

问题排查

出异常堆栈信息来看,跟我们代码相关的就一行

at com.qsl.test.BatchInsertTest.lambda$testBatchInsertAndUpdateMultiThread$1(BatchInsertTest.java:77)

也就是

boolean insertResult = orderService.saveBatch(insertList);

不就是简单的调用 mybatis-plus 的批量保存接口吗,能有什么问题?很显然,一眼看不出原因,那怎么办呢?

当下肯定是问 AI 呀,直接将异常堆栈丢给AI,让其分析可能的原因

问Qoder可能原因

Qoder 很快给出了根因

Qoder找到根因

说白了就是 MySQL JDBC 驱动(8.0.28)与 OceanBase (3.2.4.8)不兼容,OceanBase并未响应驱动想要的信息

从出现问题到找到原因,你们是不是觉得很快?但实际情况可能比 demo 要复杂很多,比如OceanBase部署模式不一致(本地单节点、客户现场是集群)、OceanBase 版本不一致、没有那么详细的上下文供 AI 读取等等,那找到原因的速度就要慢很多了

当然,除了 AI 我们还能从 OceanBase 官方搜索原因

我们要知道:我们遇到的问题肯定有其他人已经碰到过了(前提是确定不是我们写的代码问题)

OceanBase官方搜原因

也能很快找到原因。但实际情况是,我们往往不会去 OceanBase 官方寻找原因,因为我们潜意识里认为跟 OceanBase 没关系

问题解决

AI 找到了原因,自然会给出解决方案

Qoder解决方案

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

OceanBase官方推荐方案

但我觉得这两个方案都不是最合适的

既然用的是 OceanBase 数据库,为什么不用 OceanBase 官方提供的驱动?

用 OceanBase 官方的 JDBC 驱动,是不是才是最兼容的?我带着这个疑问问了 Qoder

质疑为什么不用OB官方驱动

所以最佳解决方案是用 oceanbase-client,调整两处即可

  1. pom.xml

    将原来的

    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>8.0.25</version>
    </dependency>
    

    替换成

    <dependency>
        <groupId>com.oceanbase</groupId>
        <artifactId>oceanbase-client</artifactId>
        <version>2.4.12</version>
    </dependency>
    
  2. application.yml

    driver-class-name 和 url 调整 oceanbase 的写法

    spring:
      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
    

总结

  1. 当下 AI 这么强大的情况下,首选它来排查问题是最高效的;上下文给的越足越好
  2. 选择官配、官配、官配,不要选择第三者,官配才是兼容性最好的(仅仅指驱动,你们不要乱想哈)
  3. AI 给的方案一定是最优的吗,不一定,我们需要甄别,不要盲目信奉它
posted @ 2026-08-28 09:08  青石路  阅读(103)  评论(1)    收藏  举报