如何正确配置Spring R2DBC事务?事务回滚与隔离问题排查
Spring R2DBC+H2事务配置问题及解决方案
问题场景
我在使用Spring R2DBC和H2数据库实现事务时,遇到事务边界配置问题。当前代码手动管理连接和事务,执行流程如下:
- 创建连接并开启SERIALIZABLE级别的事务
- 执行
CREATE TABLE CUSTOMER语句 - 延迟60秒
- 执行
INSERT INTO CUSTOMER语句 - 提交事务,异常时回滚
但测试时,在延迟窗口期通过H2控制台执行ALTER TABLE CUSTOMER DROP COLUMN name,后续INSERT操作抛出语法错误,但建表操作并未回滚。需求是实现以下任一效果:
- 通过隔离策略锁定表,阻止ALTER操作执行
- 保证事务正常回滚,撤销建表操作
现有代码
主启动类
@SpringBootApplication @EnableTransactionManagement public class R2dbcApplication { public static void main(String[] args) { SpringApplication.run(R2dbcApplication.class, args); } }
Runner组件
@Component class Runner { @Autowired ConnectionFactory cf; @EventListener(ApplicationStartedEvent.class) //@Transactional(isolation = Isolation.SERIALIZABLE, rollbackFor = Exception.class) public Mono<Void> runOnStartup(){ Mono emitter = Mono.from(cf.create()) .flatMap(c-> Mono.from(c.setTransactionIsolationLevel(IsolationLevel.SERIALIZABLE)) .then(Mono.from(c.setAutoCommit(false))) .then(Mono.from(c.beginTransaction())) .then(Mono.from(c.createStatement( "CREATE TABLE CUSTOMER (id INTEGER, name VARCHAR(255));" ).execute())) .then(Mono.delay(Duration.ofSeconds(60))) .then(Mono.from(c.createStatement( "INSERT INTO CUSTOMER VALUES ('100','XXX');" ).execute())) .then(Mono.from(c.commitTransaction())) .onErrorResume(ex -> Mono.from(c.rollbackTransaction()).then(Mono.error(ex))) .doFinally((st)->c.close())); emitter.subscribe(); return Mono.empty(); } }
配置文件application.yml
spring: jdbc: url: jdbc:h2:mem:~/db/testdb;DB_CLOSE_DELAY=-1;LOCK_MODE=3 username: sa password: initialization-mode: always r2dbc: url: r2dbc:h2:mem:///~/db/testdb;DB_CLOSE_DELAY=-1;LOCK_MODE=3 username: sa password: initialization-mode: always
报错信息
reactor.core.Exceptions$ErrorCallbackNotImplemented: io.r2dbc.spi.R2dbcBadGrammarException: [21002] [21S02] Column count does not match; SQL statement: INSERT INTO CUSTOMER VALUES ('100','XXX') [21002-214]
解决方案
方案一:实现事务正常回滚
问题根源有两点:
- H2默认会自动提交DDL语句,即使开启了事务,导致建表操作提前脱离事务
- 手动管理连接和订阅,Spring无法追踪反应式流的错误,回滚逻辑未被正确触发
修正步骤:
- 修改H2连接URL:让DDL支持事务,添加
MVCC=TRUE和AUTOCOMMIT=FALSE参数
spring: r2dbc: url: r2dbc:h2:mem:///~/db/testdb;DB_CLOSE_DELAY=-1;LOCK_MODE=3;MVCC=TRUE;AUTOCOMMIT=FALSE
- 使用Spring声明式事务+DatabaseClient:替代手动连接管理,让Spring自动处理事务边界
@Component class Runner { private final DatabaseClient databaseClient; // 构造注入DatabaseClient public Runner(DatabaseClient databaseClient) { this.databaseClient = databaseClient; } @EventListener(ApplicationStartedEvent.class) @Transactional(isolation = Isolation.SERIALIZABLE, rollbackFor = Exception.class) public Mono<Void> runOnStartup() { return databaseClient.sql("CREATE TABLE CUSTOMER (id INTEGER, name VARCHAR(255));") .fetch() .rowsUpdated() .then(Mono.delay(Duration.ofSeconds(60))) .then(databaseClient.sql("INSERT INTO CUSTOMER VALUES ('100','XXX');") .fetch() .rowsUpdated()) .then(); } }
- 移除手动订阅:由Spring事件监听机制自动处理订阅,确保错误能被捕获并触发回滚
方案二:通过隔离策略锁定表阻止ALTER操作
要阻止其他会话修改表结构,需保证事务持有表的排他锁,结合以下配置:
- 保持H2连接URL的
LOCK_MODE=3(表级锁)和MVCC=TRUE、AUTOCOMMIT=FALSE配置 - 使用
SERIALIZABLE隔离级别(已在@Transactional注解中配置)
此时,事务执行CREATE TABLE后会持有表的排他锁,其他会话执行ALTER TABLE时会被阻塞,直到当前事务提交或回滚,从而避免表结构被修改。
关键注意点
- H2的DDL默认自动提交,必须通过URL配置开启事务支持
- Spring R2DBC事务管理依赖反应式上下文,尽量使用
DatabaseClient和声明式事务,避免手动操作连接 - 反应式方法内不要手动调用
subscribe(),否则Spring无法追踪流的状态变化
内容的提问来源于stack exchange,提问作者user3747396
相关产品推荐
相关产品推荐

