跪拜 Guibai
← Back to the summary

MySQL JDBC Driver Crashes on OceanBase — Use the Official Client Instead

Happy Moment

To quickly rank up in Honor of Kings, I found a booster on WeChat. Me: How much per hour for Honor of Kings boosting? Booster: 30 per hour, bro. After I transferred 30, I sent: You're broke, what are you even living for? Booster: You're acting tough with just 30 bucks? Me: That's my in-game ID. Me: I hope the line you just typed is also your in-game ID. Booster recalled a message and said: Bro, sorry, my hand slipped.

Hello there, sassy

ArrayIndexOutOfBoundsException

ob-mysql cluster deployment, version: 5.7.25-OceanBase-v3.2.4.8

oceanbase version

mysql-connector-java version: 8.0.28

mybatis-plus version: 3.1.0

Table: tbl_order

-- Execute in OceanBase's test database
CREATE TABLE IF NOT EXISTS `tbl_order` (
    `id`         INT            NOT NULL AUTO_INCREMENT COMMENT 'Primary Key',
    `order_no`   VARCHAR(64)    NOT NULL COMMENT 'Order Number',
    `goods_name` VARCHAR(255)   NOT NULL COMMENT 'Goods Name',
    `amount`     DECIMAL(10, 2) NOT NULL COMMENT 'Amount',
    `user_id`    INT            NOT NULL COMMENT 'User ID',
    PRIMARY KEY (`id`)
) COMMENT='Order Table';

Because the problem occurs intermittently, I wrote a demo that easily reproduces the issue.

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;

/**
 * Batch insert + update, simulating ArrayIndexOutOfBoundsException under MySQL JDBC 8.0.28 + OceanBase
 *
 * @author: 青石路
 * @date: 2026/8/26
 */
@RunWith(SpringRunner.class)
@SpringBootTest
public class BatchInsertTest {

    @Resource
    private TblOrderService orderService;

    /**
     * Multi-threaded batch insert + batch update test
     * 
     * Test scenario:
     * - 10 threads executing concurrently
     * - Each thread executes 10 rounds, each round: insert 500 records + update 500 records
     * - Total: 100 batches of inserts (50,000 records) + 100 batches of updates (50,000 records)
     * 
     * Purpose: Increase the probability of triggering ArrayIndexOutOfBoundsException through high concurrency + cross-node routing
     */
    @Test
    public void testBatchInsertAndUpdateMultiThread() throws InterruptedException {
        int threadCount = 10;           // Number of threads
        int batchesPerThread = 10;      // Number of batches per thread
        int batchSize = 500;            // Records per batch
        
        ExecutorService executor = Executors.newFixedThreadPool(threadCount);
        CountDownLatch latch = new CountDownLatch(threadCount);
        List<Throwable> exceptions = new CopyOnWriteArrayList<>();
        
        System.out.println("=== Starting multi-threaded batch test ===");
        System.out.println("Threads: " + threadCount + ", Batches per thread: " + batchesPerThread + ", Batch size: " + batchSize);
        System.out.println("Total: " + (threadCount * batchesPerThread) + " insert batches + " + (threadCount * batchesPerThread) + " update batches");
        
        for (int t = 0; t < threadCount; t++) {
            final int threadId = t;
            executor.submit(() -> {
                try {
                    for (int b = 0; b < batchesPerThread; b++) {
                        // Generate unique order number prefix
                        String prefix = "T" + threadId + "B" + b + "_";
                        
                        // 1. Batch insert
                        List<TblOrder> insertList = buildOrderList(batchSize, prefix);
                        boolean insertResult = orderService.saveBatch(insertList);
                        System.out.println("[Thread" + threadId + "] Insert batch " + b + ": " + insertResult);
                        
                        // 2. Query the just-inserted records (to get auto-generated IDs)
                        List<String> orderNos = insertList.stream()
                                .map(TblOrder::getOrderNo)
                                .collect(Collectors.toList());
                        List<TblOrder> insertedOrders = orderService.list(
                                new LambdaQueryWrapper<TblOrder>()
                                        .in(TblOrder::getOrderNo, orderNos)
                        );
                        
                        // 3. Batch update
                        insertedOrders.forEach(order -> {
                            order.setAmount(order.getAmount().add(new BigDecimal("10")));
                            order.setGoodsName(order.getGoodsName() + "_updated");
                        });
                        boolean updateResult = orderService.updateBatchById(insertedOrders);
                        System.out.println("[Thread" + threadId + "] Update batch " + b + ": " + updateResult);
                    }
                } catch (Throwable e) {
                    exceptions.add(e);
                    System.err.println("[Thread" + Thread.currentThread().getName() + "] Exception: " + e.getMessage());
                    e.printStackTrace();
                } finally {
                    latch.countDown();
                }
            });
        }
        
        // Wait for all threads to complete, maximum wait time 10 minutes
        boolean completed = latch.await(10, TimeUnit.MINUTES);
        executor.shutdown();
        
        System.out.println("\n=== Test completed ===");
        System.out.println("Timed out: " + !completed);
        System.out.println("Total exceptions: " + exceptions.size());
        
        // Print all exception stack traces
        if (!exceptions.isEmpty()) {
            System.out.println("\n=== Exception details ===");
            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("Test Goods" + prefix + i)
                    .setAmount(new BigDecimal("99.99"))
                    .setUserId(i);
            list.add(order);
        }
        return list;
    }
}

The code is simple: 10 threads simultaneously do batch inserts and updates. Running it will likely throw an exception: ArrayIndexOutOfBoundsException

ArrayIndexOutOfBoundException

Problem Investigation

From the exception stack trace, only one line is related to our code:

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

That is:

boolean insertResult = orderService.saveBatch(insertList);

Isn't it just a simple call to MyBatis-Plus's batch save interface? What could go wrong? Obviously, you can't tell the cause at a glance. So what to do?

The immediate answer is to ask AI. Just throw the exception stack trace at AI and let it analyze possible causes.

Ask Qoder for possible causes

Qoder quickly gave the root cause.

Qoder found the root cause

Simply put, MySQL JDBC driver (8.0.28) is incompatible with OceanBase (3.2.4.8); OceanBase does not respond with the information the driver expects.

From problem occurrence to finding the cause, do you think it was fast? But the real situation might be much more complex than the demo, for example: OceanBase deployment mode differs (local single node vs. customer's cluster), OceanBase versions differ, there isn't such detailed context for AI to read, etc. Then the speed of finding the cause would be much slower.

Of course, besides AI, we can also search for the cause on OceanBase's official site.

We need to know: the problem we encounter has definitely been encountered by others before (provided we are sure it's not a problem with our own code).

Search for cause on OceanBase official site

You can also quickly find the cause. But in reality, we often don't go to OceanBase's official site to look for the cause, because subconsciously we think it has nothing to do with OceanBase.

Problem Resolution

AI found the cause and naturally provided solutions.

Qoder solutions

I tried both solutions, and indeed both worked. AI recommended: Add trackSessionState=true to JDBC URL, because it's a simple adjustment. But OceanBase officially recommends changing the MySQL JDBC driver version.

OceanBase official recommended solution

But I think neither of these two solutions is the most suitable.

Since we are using OceanBase database, why not use the official driver provided by OceanBase?

Using OceanBase's official JDBC driver, wouldn't that be the most compatible? With this question, I asked Qoder.

Questioning why not use OB official driver

So the best solution is to use oceanbase-client, adjusting two places:

  1. pom.xml

    Replace the original:

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

    with:

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

    Adjust driver-class-name and url to OceanBase's format:

    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
    

Summary

  1. With AI being so powerful nowadays, using it as the first choice to troubleshoot problems is the most efficient; the more context you provide, the better.
  2. Choose the official match, official match, official match; don't choose a third party. The official match has the best compatibility (only referring to drivers, don't let your minds wander).
  3. Is the solution AI gives necessarily the optimal one? Not necessarily. We need to discern and not blindly worship it.