You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Java8+SpringBoot+JDBI3+HSQLDB并发事务数据泄露问题排查

我来帮你分析这个问题——你的JDBI3+HSQLDB测试中出现Serializable隔离级下的数据泄露,但原生JDBC测试正常,大概率是配置或测试同步逻辑的细节问题,以下是具体排查方向和解决方案:


1. 先检查HSQLDB连接URL的参数是否生效

HSQLDB的hsqldb.tx_level参数值需要大写(比如SERIALIZABLE),而你写的是小写的serializable,这很可能导致参数不生效,数据库默认使用READ_COMMITTED隔离级别——这就解释了为什么线程2能看到线程1提交的变更(READ_COMMITTED允许读取已提交的事务数据)。

你可以在JDBI事务中直接通过JDBC连接验证隔离级别:

jdbi.inTransaction(tx -> {
  Connection conn = tx.getConnection();
  // 预期值是8(对应Connection.TRANSACTION_SERIALIZABLE)
  System.out.println("实际JDBC隔离级别:" + conn.getTransactionIsolation());
  // ... 其他逻辑
});

修复方案:把URL参数改成大写:

jdbc:hsqldb:${...};create=true;hsqldb.tx=mvlocks;hsqldb.tx_level=SERIALIZABLE

2. 强制JDBI事务显式指定隔离级别

即使URL参数正确,JDBI的默认事务行为可能依赖连接池的初始设置,而非强制覆盖。建议在启动事务时显式指定隔离级别,确保每个事务都严格使用Serializable:

// 线程1的事务显式指定隔离级
CompletableFuture<Void> first = CompletableFuture.runAsync(Unchecked.runnable(() -> {
  jdbi.useTransaction(TransactionIsolationLevel.SERIALIZABLE, tx -> {
    assertThat(tx.isInTransaction()).isTrue();
    LOGGER.info("first tx started");
    sync.run();
    Queries queries = tx.attach(Queries.class);
    queries.insert(2, 2);
  });
  LOGGER.info("first tx committed");
  Thread.sleep(100);
  sync.run();
}));

// 线程2的事务同样显式指定
CompletableFuture<Integer> subject = CompletableFuture.supplyAsync(Unchecked.supplier(() -> {
  return jdbi.inTransaction(TransactionIsolationLevel.SERIALIZABLE, tx -> {
    assertThat(tx.isInTransaction()).isTrue();
    LOGGER.info("second tx started");
    sync.run();
    sync.run();
    Queries queries = tx.attach(Queries.class);
    return queries.count();
  });
}));

3. 修正测试的同步逻辑(核心问题)

你的测试中,线程2的统计操作是在线程1提交事务之后才执行的,这和你预期的“线程2事务启动于线程1提交之前,看不到变更”的场景不符——即使隔离级别正确,这个同步逻辑也可能让线程2的事务在执行统计时,HSQLDB的Serializable实现允许读取已提交的变更(不同数据库的Serializable语义有差异,HSQLDB的mvlocks模式下是基于锁的串行执行,mvcc是快照隔离)。

调整同步逻辑,确保线程2的统计在线程1提交之前执行:

// 用两个屏障分别同步事务启动和统计/提交顺序
CyclicBarrier txStartBarrier = new CyclicBarrier(2);
CyclicBarrier countBeforeCommitBarrier = new CyclicBarrier(2);

CompletableFuture<Void> first = CompletableFuture.runAsync(Unchecked.runnable(() -> {
  jdbi.useTransaction(TransactionIsolationLevel.SERIALIZABLE, tx -> {
    LOGGER.info("first tx started");
    txStartBarrier.await(1, TimeUnit.SECONDS); // 等线程2启动事务
    Queries queries = tx.attach(Queries.class);
    queries.insert(2, 2);
    countBeforeCommitBarrier.await(1, TimeUnit.SECONDS); // 等线程2统计完再提交
  });
  LOGGER.info("first tx committed");
}));

CompletableFuture<Integer> subject = CompletableFuture.supplyAsync(Unchecked.supplier(() -> {
  return jdbi.inTransaction(TransactionIsolationLevel.SERIALIZABLE, tx -> {
    LOGGER.info("second tx started");
    txStartBarrier.await(1, TimeUnit.SECONDS); // 等线程1启动事务
    Queries queries = tx.attach(Queries.class);
    int count = queries.count(); // 此时线程1还没提交,符合预期场景
    countBeforeCommitBarrier.await(1, TimeUnit.SECONDS); // 通知线程1可以提交
    return count;
  });
}));

CompletableFuture.allOf(first, subject).get();
assertThat(subject.get()).isEqualTo(1);

4. 排查连接池的配置问题

如果使用了连接池(比如HikariCP),需要确保:

  • 连接池不会复用带有错误隔离级别的连接,建议配置连接初始化SQL强制设置隔离级:
    spring.datasource.hikari.connection-init-sql=SET TRANSACTION ISOLATION LEVEL SERIALIZABLE
    
  • 确认连接池的autoCommit默认值(通常是true)被JDBI的事务正确覆盖为false。

为什么原生JDBC测试正常?

你的原生JDBC测试中,startTransaction(c1)和startTransaction(c2)方法应该显式设置了隔离级别为Serializable(比如调用conn.setTransactionIsolation(Connection.TRANSACTION_SERIALIZABLE)),而JDBI测试依赖URL参数但参数写错,导致隔离级别未正确设置,这才出现了差异。

内容的提问来源于stack exchange,提问作者Marnes

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.15 07:30:50