Java 后端开发中事务是一个非常重要的知识点。只要涉及数据库操作就很容易遇到事务问题。比如用户下单、扣减库存、账户转账、保存订单和订单详情这些业务通常都要求多个数据库操作要么全部成功要么全部失败。在 Spring 中事务管理主要有两种方式一种是编程式事务。一种是声明式事务。很多初学者刚接触 Spring 事务时经常分不清这两种事务到底有什么区别也不知道实际开发中应该怎么选。这篇文章就来总结一下编程式事务和声明式事务的区别。一、先简单理解什么是事务事务可以理解为一组数据库操作的执行单元。这一组操作要么全部成功要么全部失败。比如转账业务用户 A 给用户 B 转账 100 元大概会有两个操作// A 用户余额减少 100 update account set money money - 100 where user_id A; // B 用户余额增加 100 update account set money money 100 where user_id B;这两个操作必须同时成功。如果 A 扣钱成功了但是 B 加钱失败了就会出现数据不一致的问题。所以这个时候就需要事务。事务有四个经典特性也就是 ACID原子性事务中的操作要么全部成功要么全部失败。一致性事务执行前后数据要保持一致。隔离性多个事务之间不能互相干扰。持久性事务提交后数据修改会被持久保存。二、Spring 中事务管理的两种方式Spring 提供了两种常见的事务管理方式编程式事务声明式事务简单来说编程式事务在代码中手动控制事务的开启、提交和回滚。声明式事务通过注解或配置的方式让 Spring 帮我们管理事务。实际开发中最常用的是声明式事务也就是 Transactional 注解。三、什么是编程式事务编程式事务就是通过写代码的方式来管理事务。也就是说什么时候开启事务、什么时候提交事务、什么时候回滚事务都由程序员在代码中手动控制。在 Spring 中常见的编程式事务方式是使用 TransactionTemplate。示例代码Service public class OrderService { Autowired private TransactionTemplate transactionTemplate; Autowired private OrderMapper orderMapper; Autowired private StockMapper stockMapper; public void createOrder() { transactionTemplate.execute(status - { try { // 1. 创建订单 orderMapper.insertOrder(); // 2. 扣减库存 stockMapper.reduceStock(); return true; } catch (Exception e) { // 手动设置回滚 status.setRollbackOnly(); return false; } }); } }在这个例子中事务的执行范围非常清楚。transactionTemplate.execute() 里面的代码就是事务控制的范围。如果里面的业务执行成功事务就会提交。如果发生异常或者手动调用 status.setRollbackOnly()事务就会回滚。四、什么是声明式事务声明式事务就是通过注解或配置的方式来管理事务。开发者不需要在业务代码中手动开启事务、提交事务和回滚事务只需要告诉 Spring这个方法需要事务。最常见的方式就是使用 Transactional 注解。示例代码Service public class OrderService { Autowired private OrderMapper orderMapper; Autowired private StockMapper stockMapper; Transactional public void createOrder() { // 1. 创建订单 orderMapper.insertOrder(); // 2. 扣减库存 stockMapper.reduceStock(); } }这段代码中只要在方法上加上 TransactionalSpring 就会自动帮我们管理事务。方法正常执行完成事务提交。方法执行过程中抛出运行时异常事务回滚。所以声明式事务最大的特点就是业务代码更加简洁事务控制和业务逻辑分离。五、编程式事务和声明式事务的核心区别1. 使用方式不同编程式事务需要在代码中手动控制事务。比如使用 TransactionTemplate 包裹业务逻辑。声明式事务只需要加注解比如Transactional public void createOrder() { // 业务逻辑 }Spring 会通过 AOP 自动完成事务控制。2. 对业务代码的侵入性不同编程式事务需要把事务控制代码写进业务方法中。比如transactionTemplate.execute(status - { // 业务逻辑 });这会让业务代码和事务代码耦合在一起。声明式事务只需要一个注解业务代码更加干净。Transactional public void createOrder() { // 业务逻辑 }所以从代码简洁性来看声明式事务更适合大多数业务场景。3. 控制粒度不同编程式事务的控制粒度更细。因为你可以明确控制哪一段代码在事务中哪一段代码不在事务中。比如public void createOrder() { // 这部分不在事务中 checkUser(); transactionTemplate.execute(status - { // 只有这里在事务中 orderMapper.insertOrder(); stockMapper.reduceStock(); return true; }); // 这部分也不在事务中 sendMessage(); }这种方式适合一些对事务范围要求非常精确的场景。声明式事务通常是以方法为单位。比如Transactional public void createOrder() { checkUser(); orderMapper.insertOrder(); stockMapper.reduceStock(); sendMessage(); }这个方法里的逻辑都会在同一个事务中执行。如果方法中包含一些比较耗时的操作比如远程调用、发送消息、上传文件就可能导致事务时间过长。4. 可读性和维护性不同声明式事务的可读性更好。看到 Transactional就知道这个方法需要事务。编程式事务虽然控制更灵活但是代码会更复杂。尤其是业务逻辑比较多的时候事务代码和业务代码混在一起后期维护成本会更高。5. 使用场景不同声明式事务适合大多数常规业务。比如创建订单扣减库存保存用户信息修改账户余额批量插入数据编程式事务适合需要精细控制事务范围的场景。比如只想让某一小段代码开启事务事务中不能包含远程调用需要根据业务结果手动决定是否回滚一个方法中有多段不同事务逻辑六、声明式事务的底层原理简单理解Spring 声明式事务的底层主要依赖 AOP。也就是说Spring 会为加了 Transactional 的类或方法创建代理对象。当外部调用这个方法时实际调用的是代理对象的方法。代理对象会在目标方法执行前开启事务在目标方法执行后提交事务如果出现异常就回滚事务。大概流程如下调用代理对象方法 ↓ 开启事务 ↓ 执行目标方法 ↓ 没有异常提交事务 出现异常回滚事务所以 Transactional 不是简单的标记它背后其实是 Spring AOP 在起作用。这也是为什么有些情况下声明式事务会失效。七、声明式事务常见失效场景1. 方法不是 publicTransactional 一般建议加在 public 方法上。如果加在 private 方法或 protected 方法上可能不会生效。错误示例Transactional private void createOrder() { // 业务逻辑 }2. 同一个类内部方法调用这是非常常见的事务失效场景。比如Service public class OrderService { public void submitOrder() { createOrder(); } Transactional public void createOrder() { // 创建订单 } }submitOrder() 和 createOrder() 在同一个类中。这种内部调用不会经过 Spring 代理对象所以 Transactional 可能不会生效。解决方式一般是把事务方法放到另一个 Service 中或者通过代理对象调用。3. 异常被 catch 掉了默认情况下Spring 遇到运行时异常才会回滚。如果异常被 catch 掉没有继续抛出Spring 就认为方法正常执行完成于是事务会提交。错误示例Transactional public void createOrder() { try { orderMapper.insertOrder(); stockMapper.reduceStock(); } catch (Exception e) { // 异常被吞掉了事务可能不会回滚 e.printStackTrace(); } }正确做法Transactional public void createOrder() { try { orderMapper.insertOrder(); stockMapper.reduceStock(); } catch (Exception e) { throw new RuntimeException(e); } }或者手动设置回滚Transactional public void createOrder() { try { orderMapper.insertOrder(); stockMapper.reduceStock(); } catch (Exception e) { TransactionAspectSupport.currentTransactionStatus().setRollbackOnly(); } }4. 抛出的是检查异常默认情况下Spring 事务只会对 RuntimeException 和 Error 进行回滚。如果抛出的是普通的检查异常比如 Exception默认不会回滚。示例Transactional public void createOrder() throws Exception { orderMapper.insertOrder(); throw new Exception(普通异常); }如果希望遇到所有异常都回滚可以这样写Transactional(rollbackFor Exception.class) public void createOrder() throws Exception { orderMapper.insertOrder(); throw new Exception(普通异常); }实际开发中很多人会直接写Transactional(rollbackFor Exception.class)这样可以避免因为异常类型导致事务不回滚。5. 数据库引擎不支持事务比如 MySQL 中如果使用的是 MyISAM 引擎就不支持事务。InnoDB 才支持事务。可以通过下面 SQL 查看表引擎SHOW TABLE STATUS WHERE Name 表名;如果事务怎么都不生效也要检查一下数据库表是否支持事务。八、实际开发中应该怎么选大多数情况下优先使用声明式事务。也就是Transactional(rollbackFor Exception.class)原因很简单代码简洁使用方便可读性好维护成本低和 Spring 结合更自然比如常见的订单创建Transactional(rollbackFor Exception.class) public void createOrder() { orderMapper.insertOrder(); stockMapper.reduceStock(); }但是如果你需要非常精确地控制事务范围可以考虑编程式事务。比如有些代码不应该放在事务中远程接口调用发送 MQ 消息发送短信上传文件复杂计算这些操作如果放在事务中可能会让事务持续时间变长影响数据库连接资源。这种情况下可以用编程式事务控制一小段核心数据库操作public void createOrder() { checkUser(); transactionTemplate.execute(status - { orderMapper.insertOrder(); stockMapper.reduceStock(); return true; }); sendMessage(); }这样事务只包住真正需要保证一致性的数据库操作。九、两者对比总结对比点编程式事务声明式事务使用方式手动写代码控制事务使用注解或配置常见方式TransactionTemplateTransactional代码侵入性较高较低控制粒度更细通常以方法为单位可读性代码相对复杂简洁清晰灵活性更强稍弱适用场景精细控制事务范围大多数业务场景底层实现直接调用事务管理 APISpring AOP 代理十、简单总结编程式事务和声明式事务本质上都是为了管理数据库事务。编程式事务是通过代码手动控制事务优点是灵活控制粒度细缺点是代码侵入性强写起来比较麻烦。声明式事务是通过 Transactional 注解交给 Spring 管理优点是简单、清晰、维护成本低缺点是需要注意 AOP 代理导致的一些事务失效问题。实际开发中一般优先使用声明式事务Transactional(rollbackFor Exception.class)只有在需要精细控制事务范围时再考虑使用编程式事务。简单记一句话声明式事务适合大多数业务场景编程式事务适合少数需要精细控制事务边界的场景。