直接手动commit可能破坏Spring事务一致性!

Spring 应用中事务提交应由框架自动管理,手动 commit 会破坏一致性;推荐使用 @Transactional 声明式事务,极少数场景可程序化控制。

手动调用 commit?你可能正在亲手毁掉 Spring 的事务一致性!

Spring 中使用 JdbcTemplate 或 DataSource 时,要不要手动 commit?这个问题看似简单,实则暗藏玄机。很多从原生 JDBC 转过来的开发者,习惯性地想在代码里调用 connection.commit(),甚至尝试从 DataSource 拿连接后手动控制事务——殊不知,这种“自作聪明”的做法,正在悄悄破坏 Spring 事务管理的底层契约,轻则事务失效,重则数据不一致,甚至酿成生产事故!

今天我们就彻底讲清楚:在 Spring 的世界里,事务的 commit 到底由谁说了算?为什么你不能、也不该自己动手?又在哪些极端场景下,才能谨慎启用程序化事务?这篇文章将带你穿透抽象迷雾,看清 Spring 事务的真正掌控者。

混乱的根源:JdbcTemplate 隐藏了连接,也隐藏了 commit

在原生 JDBC 编程中,事务控制非常直白:你从 DriverManager 拿到一个 Connection,调用 setAutoCommit(false) 关闭自动提交,执行几条 SQL,最后根据业务逻辑决定调用 commit() 还是 rollback()。整个过程透明、可控,你就是事务的“老板”。

但当你切换到 Spring + JdbcTemplate 后,一切都变了。JdbcTemplate 的设计哲学是“只负责执行 SQL,不负责管理事务”。它会在内部向 DataSource 申请一个连接,执行你传入的 SQL,然后立刻把连接还回连接池——这个过程对你完全透明。你甚至拿不到真实的 Connection 对象,自然也就找不到地方调用 commit()。

于是很多开发者开始“另辟蹊径”:有人尝试从 DataSource 手动获取 Connection,关闭 auto-commit,执行 SQL,再手动 commit;还有人误以为 DataSource 本身能 commit(其实它只是连接工厂,根本不是事务载体)。这些操作看似“还原了 JDBC 的自由”,实则是在和 Spring 的事务基础设施“对着干”。因为 Spring 的事务上下文(Transaction Context)是通过 ThreadLocal 绑定在当前线程上的,你手动拿的连接很可能根本不在 Spring 管理的事务中,导致你的 commit 根本没作用,或者更糟——提前提交了部分操作,而其他操作还在等待 Spring 的统一回滚,最终造成数据半写入、半丢失的“薛定谔状态”。

真正的事务掌控者:Spring 容器才是那个发号施令的人

在 Spring 应用中,事务的主人从来就不是你的 Service、DAO 或 Repository,而是 Spring 容器本身。Spring 通过 PlatformTransactionManager(平台事务管理器)统一协调所有事务行为。当你在方法上打上 @Transactional 注解,Spring 就会在方法执行前开启一个事务,将当前线程与一个数据库连接绑定,所有在这个方法内通过 JdbcTemplate 发出的 SQL,都会自动使用这个连接,并参与同一个事务。

这意味着:commit 和 rollback 的时机完全由 Spring 决定。方法正常执行完毕?Spring 自动 commit。方法抛出未被捕获的 RuntimeException?Spring 自动 rollback。你不需要、也不应该在业务代码中写任何 commit() 语句——因为那会绕过 Spring 的统一调度,破坏“事务一致性”这一核心承诺。

举个最典型的例子:订单服务里,先插入订单记录,再扣减库存。如果这两个操作不在同一个事务里,就可能出现“订单创建成功但库存没扣”或者“库存扣了但订单没建”的灾难场景。而 Spring 的 @Transactional 正是为了解决这个问题而生的——它用声明式的方式,把两个看似独立的 SQL 操作,牢牢绑定在同一个原子单元中。

最佳实践:用 @Transactional 声明你的事务边界

90% 以上的业务场景,都应该使用声明式事务管理。你只需要在 Service 层的方法上加上 @Transactional 注解,剩下的交给 Spring。来看一段标准代码:

@Service
public class OrderService {
    private JdbcTemplate jdbcTemplate;
    public OrderService(JdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
    }
    @Transactional
    public void placeOrder(long orderId, long productId) {
        jdbcTemplate.update(
          "insert into orders(id, product_id) values (?, ?)",
          orderId,
          productId
        );
        jdbcTemplate.update(
         
"update products set stock = stock - 1 where id = ?",
          productId
        );
    }
}

注意:方法内部没有任何 commit 或 rollback!Spring 会在 placeOrder() 开始前启动事务,在方法结束后根据是否抛异常决定提交或回滚。代码干净、逻辑清晰、事务可靠。

但要让 @Transactional 生效,你还得配置一个事务管理器。对于纯 JDBC 场景,使用 DataSourceTransactionManager:

@Configuration
@EnableTransactionManagement
public class TransactionConfig {
    @Bean
    public PlatformTransactionManager transactionManager(
      DataSource dataSource
    ) {
        return new DataSourceTransactionManager(dataSource);
    }
}

@EnableTransactionManagement 这个注解就是开启 Spring 事务代理的总开关。一旦配置完成,所有 @Transactional 标记的方法都会被 Spring 的 AOP 代理拦截,自动织入事务控制逻辑。

你可能会问:怎么验证 Spring 真的自动 commit 了?写个测试就知道了:

@SpringBootTest
class OrderServiceTest {
    @Autowired
    private OrderService orderService;
    @Test
    @Transactional
    void givenTransactionalMethod_whenNoException_thenTransactionCommits() {
        orderService.placeOrder(1L, 100L);
    }
}

这个测试方法本身也带 @Transactional,所以 Spring 会在测试开始时开启事务,调用 placeOrder() 时复用同一个事务(传播行为默认 REQUIRED),测试结束时自动回滚——既验证了业务方法的事务行为,又保证了测试数据不会污染数据库。这就是 Spring 事务测试的精妙之处!

极少数例外:当你真的需要程序化控制 commit

虽然声明式事务覆盖绝大多数场景,但总有些“刺头”需求——比如事务的提交与否要依赖某个复杂的业务判断,或者需要在循环中分批提交。这时候,就得祭出程序化事务管理(Programmatic Transaction Management)。

核心武器是 PlatformTransactionManager。你手动调用它的 getTransaction() 开启事务,执行 SQL,最后显式调用 commit()。注意:这里仍然不要碰 JDBC 的 Connection!全程使用 Spring 提供的 TransactionStatus 对象:

@Repository
public class PaymentRepository {
    private JdbcTemplate jdbcTemplate;
    private PlatformTransactionManager transactionManager;
    public PaymentRepository(
      JdbcTemplate jdbcTemplate,
      PlatformTransactionManager transactionManager
    ) {
        this.jdbcTemplate = jdbcTemplate;
        this.transactionManager = transactionManager;
    }
    public void processPayment(long paymentId, long amount) {
        TransactionDefinition definition =
          new DefaultTransactionDefinition();
        TransactionStatus status =
          transactionManager.getTransaction(definition);
        jdbcTemplate.update(
          "insert into payments(id, amount) values (?, ?)",
          paymentId,
          amount
        );
        jdbcTemplate.update(
         
"update accounts set balance = balance - ? where id = 1",
          amount
        );
        transactionManager.commit(status);
    }
}

关键点来了:如果不调用 transactionManager.commit(status),事务会在方法结束时被 Spring 自动回滚!这意味着程序化事务是“默认不提交”的,你必须显式确认。这种设计防止了开发者忘记 commit 导致数据丢失。

我们同样可以用测试验证:

@SpringBootTest
class PaymentRepositoryTest {
    @Autowired
    private PaymentRepository paymentRepository;
    @Test
    void givenProgrammaticTransaction_whenCommitIsCalled_thenChangesArePersisted() {
        paymentRepository.processPayment(1L, 200L);
    }
}

只要 processPayment() 里调用了 commit,数据就持久化;一旦注释掉那行,数据库就空空如也。这种“所见即所得”的控制感,正是程序化事务的价值所在。

但请记住:程序化事务是“高权限操作”,只在必要时使用。滥用它会让代码耦合事务逻辑,丧失声明式的简洁性和可维护性。

为什么直接对 DataSource 或 Connection 调用 commit 是大忌?

有些开发者觉得:“我就想自己控制,Spring 太啰嗦了!”于是他们强行从 DataSource 拿连接:

Connection conn = dataSource.getConnection();
conn.setAutoCommit(false);
// 执行 SQL
conn.commit();

听着很爽,但后果很严重

因为 Spring 的事务上下文是通过 TransactionSynchronizationManager 管理的,你手动拿的连接根本不在 Spring 的事务链路中。更可怕的是,如果你在一个 @Transactional 方法内部偷偷干这事,你的 commit 会提前提交部分数据,而 Spring 还以为整个事务没结束,一旦后续 SQL 失败,Spring 会回滚——但你 already committed 的数据却无法回滚!这就造成了“部分成功、部分失败”的脏数据状态。

想象一个支付场景:你的账户扣款 SQL 被你手动 commit 了,但商户到账 SQL 因网络超时失败,Spring 回滚了后者——结果就是:钱从你账户扣了,但商户没收到!这就是典型的分布式不一致,而这完全是因为你绕过了 Spring 的事务协调机制。

Spring 的 PlatformTransactionManager 能确保:无论你调用多少个 Repository,只要在同一个 @Transactional 方法内,所有操作都共享同一个连接、同一个事务状态。而你手动 commit,等于在“统一战线”内部搞分裂,后果不堪设想。

总结:信任 Spring,让事务归位

Spring 的事务管理不是束缚,而是保护。它用清晰的职责划分,把“什么时候提交”这个复杂问题从你的业务逻辑中剥离出去,让你专注在“做什么”而不是“怎么做”。JdbcTemplate 不提供 commit 接口,不是功能缺失,而是设计哲学——事务控制权必须集中在统一的事务管理器手中。

所以,下次再想写 connection.commit() 时,请停下来问自己:我真的需要打破 Spring 的事务契约吗?99% 的答案都是“不”。拥抱 @Transactional,配置好 DataSourceTransactionManager,让 Spring 成为你事务的“守夜人”。只有在极少数、经过充分评估的场景下,才谨慎使用 PlatformTransactionManager 进行程序化控制。

记住:在 Spring 的世界里,你不是事务的司机,而是乘客。系好安全带,让框架带你安全抵达一致性终点。