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

