Spring 的 Transaction 到底该怎么玩
我们在这篇文章中来详细地谈一谈在日常的开发中,我们对于数据库事务的相关问题。
我们都知道,目前 Java 最流行的框架 Spring 对于数据库的事务控制已经做到了统一,Spring 针对 Java Transaction API (JTA)、JDBC、Hibernate 和 Java Persistence API (JPA) 等事务 API,实现了一致的编程模型,而 Spring 的声明式事务功能更是提供了极其方便的事务配置方式,配合 Spring Boot 的自动配置,目前我们开发的 Spring Boot 项目基本上只需要在方法上标记 @Transactional 注解,即可一键开启方法的事务性配置。
@Transactional 加上事务就生效了么
在很多业务开发中,处理事务的重要性被忽略了。虽然大家知道可以用@Transactional注解来简化事务配置,但很多人对事务是否正确生效并没有实际测试过真正生效。特别是在系统刚开始开发的那一个阶段,虽然没有真正生效。但这也不会造成太大问题,但随着系统变得越来越复杂、负载越来越大,到最后才会引发了许多数据不一致的问题,这个时候发现,就最需要大量人工来排查修复。
我想给大家表达换就是:正确处理事务不仅仅是给方法加上个注解那么简单! 它需要认真思考每个数据库操作在业务逻辑中的作用,以确保数据的完整性和一致性。而且,还要面对一系列潜在的问题,比如事务的嵌套、异常处理、事务传播等等。
为了保证事务的可靠性,我们还需要仔细规划和设计事务,全面考虑各种业务场景,并进行充分的测试来验证事务是否按预期运行。同时,作为开发者我们也需要充分理解事务处理的重要性,并且在开发中加以实践。
说到底就是处理数据库事务在业务系统中是非常重要的一步。我们应该认真对待事务处理,合理设计和正确配置事务,才能确保数据的安全和一致性,避免日后人力物力的浪费。
下面我们结合一个 demo 案例来讨论一下声明式事务的用法
@Data @NoArgsConstructor @TableName("t_task") public class Task extends BaseEntity { private static final long serialVersionUID = 1L; /** * taskId */ @TableId(type = IdType.AUTO) private Long taskId; /** * taskName */ private String taskName; } @Service @RequiredArgsConstructor @Slf4j public class TaskServiceImpl extends ServiceImpl<TaskMapper, Task> implements TaskService{ @Override public void test(Task task) { //todo this.svaeTaskStatus(task, null) } @Transactional private Task saveTaskStatus(Task task, TaskStatus status) { save(task); if(status == null) { throw RunTimeException("null status"); } task.setStatus(status); this.updateById(task); return task; } } @GetMapping("/test") public ResponseData result() { taskService.test(new Task(1L), null) return ResponseData.ok(); }
上面的代码很简单, 一个 task 实体类,一个 test 接口来保存ID =1 的task,如果taskstatus = null, 就抛异常。按道理来讲,这个接口必会抛异常,然后回滚,数据库里面不会有数据保存。但是,真实运行后代码你会发现,数据库里面出现了数据,saveTaskStatus 并没有回滚。
这是因为:
当使用Spring的默认AOP机制(动态代理)时,@Transactional注解只对public方法有效。这是因为动态代理无法代理私有(private)方法,所以在私有方法上标记@Transactional是没有效果的。
如果我们希望在私有方法上使用@Transactional注解,可以考虑使用AspectJ静态织入的方式来实现AOP。AspectJ允许在编译期或类加载期间将切面织入到目标类中,从而使得@Transactional注解对私有方法也能生效。但需要注意的是,AspectJ的配置相对复杂,需要额外的步骤来使其生效。
那我们把 saveTaskStatus 方法必成 public 再试试呢?
结果发现还是不行
这又是为什么呢?那是因为必须通过代理过的类从外部调用目标方法才能生效。
我们要想解决这个问题,就是当使用Spring的AOP技术对标记有@Transactional注解的方法进行增强时,实际上会创建一个代理对象来包装原始的目标类。这个代理对象拥有相同的接口,并在调用被@Transactional注解修饰的方法之前或之后添加事务处理逻辑。
为了在同一个类中调用被@Transactional注解修饰的方法,我们可以在该类中注入一个代理对象,然后通过这个代理对象调用被增强的方法。这样做是为了确保事务能够正确地被处理。
上面的例子中,我们只需要在 Service 中注入自己就可以了
@Autowired private TaskService self;
这样就可以保证在Spring代理的时候能够把 self 变量作为代理对象来正确处理事务了。虽然这样看起来可以解决问题,但是看着这样的编码有点奇怪吧?其实最好的解决办法就是分层处理,当然也可以把事务控制提上来到 test 方法上。
事务生效出现异常就回滚了么
通过AOP实现事务处理可以理解为,在标记了@Transactional注解的方法中,Spring会在方法开始执行前自动开启一个事务,在方法执行过程中监视是否发生了异常。如果方法执行过程中发生了异常,并且异常符合事务回滚的条件,Spring会在catch块中触发事务回滚操作,将所有数据库操作撤销,确保数据的一致性和完整性。如果方法执行没有异常,则事务会在方法执行结束时自动提交,将所有数据库操作生效。
因此,使用@Transactional注解可以将事务的处理逻辑从业务方法中抽离出来,使业务方法只需要关注业务逻辑本身,而不需要显式地处理事务操作。Spring AOP会在运行时动态地织入事务处理的代码,从而实现了声明式事务管理,使得事务的配置和处理更加便捷和灵活。这样的设计让开发人员更专注于业务逻辑的实现,而无需过多关心事务的细节。同时,也确保了在异常发生时,数据库操作可以正确地回滚,避免了数据不一致性的问题。
这里的符合事务回滚的条件是:
- 只有异常传播出了标记了 @Transactional 注解的方法,事务才能回滚。
- 默认情况下,出现 RuntimeException(非受检异常)或 Error 的时候,Spring 才会回滚事务。
@Service @Slf4j public class Service { @Autowired private Taskservice taskservice; //异常无法传播出方法,导致事务无法回滚 @Transactional public void createTask(String name) { try { taskservice.save(new Task(name)); throw new RuntimeException("error"); } catch (Exception ex) { log.error("create task failed", ex); } } //即使出了受检异常也无法让事务回滚 @Transactional public void creatTask2(String name) throws ArithmeticException { taskservice.save(new Task(name)); compute(); } //因为除0,一定会抛出一个ArithmeticException private void compute() throws ArithmeticException { int a = 1 / 0 ; } }
现在,我们来看如何解决上述问题这 2 种情况,对应的解决方法如下:
第一,如果你希望自己捕获异常进行业务处理的话,也没关系,可以手动设置让当前事务处于回滚状态:
@Transactional public void createTask(String name) { try { taskservice.save(new Task(name)); throw new RuntimeException("error"); } catch (Exception ex) { log.error("create task failed", ex); TransactionAspectSupport.currentTransactionStatus().setRollbackOnly(); } }
第二,在注解中声明,期望遇到所有的 Exception 都回滚事务(来突破默认不回滚受检异常的限制):
@Transactional(rollbackFor = Exception.class) public void creatTask2(String name) throws ArithmeticException { taskservice.save(new Task(name)); compute(); }
事务传播机制搞明白了么
相信很多同学在面试或者看八股文的时候多多少少都会看过这样的问题:
A 方法上加了 @Transactional
B 方法上也加了, 和 A 在同一个类
C 方法不加, 和 A 不在同一个类
问题: C 方法调用 A 会怎么样
B 方法中调用 A 会怎么样
这种问题主要就是针对 Spring 的事务传播机制和动态代理。
我们先看一下 Spring 的传播机制
1. REQUIRED:(必须) 如果以前有事务,就和之前的事务共用一个事务,没有就创建一个事务。 EQUIRES_NEW(新的事务) 创建一个新的事务,如果以前有事务,暂停前面的事务,也就是说总是用新事务。 3. SUPPORTS(支持) 之前有事务,就和之前事务共用的方式运行,没有事务也可以。 MANDATORY(强制) 一定要有事务,如果没事务就报错。 OT_SUPPORTED(不支持) 不支持在事务内运行,如果已经有事务了,就挂起当前存在的事务。 5. NEVER(从不使用) 不支持在事务内运行,如果已经有事务了,抛异常。 ESTED 开启一个子事务(MySQL不支持),需要支持还原点功能的数据库才行 @Transactional默认的传播行为是REQUIRED,且一般情况下只用REQUIRED和REQUIRES_NEW
了解了之后我们来看几个例子
/** *场景1 *A() 跟着回滚 *B() 使用新事务 不回滚 *自己操作数据库 回滚 */ @Transactional public void test(){ A(); //事务:REQUIRED B(); //事务:REQUIRES_NEW Mapper.xxx(); int i = 1/0; } /** *场景2 *C方法抛异常:A,C共用add方法的事务,所以回滚,B使用新事务执行成功,D和自己操作数据库无法执行。 *场景3 *D方法抛异常:外事务感知到异常。A,C回滚,自己操作数据库不到,D自己回滚,B成功。 *场景4 *自己操作数据库出现异常:同理,A和C回滚,B和D成功。 *场景5 *若add()上没有@Transactional呢?效果还是一样,因为A传播行为是REQUIRED ,没有事务传播给它则自己创建一个事务,后面C则和A共享。B,D仍是自己新建事务。但是若自己操作数据库抛异常则。所有以上执行过方法不会回滚 * */ @Transactional public void test(){ A(); //REQUIRED B(); //REQUIRES_NEW C(); //REQUIRED D(); //REQUIRES_NEW Mapper.xxx(); //自己操作数据 } /** *场景6: *C用try-catch执行;C出了异常回滚,由于异常被捕获,add()的事务没有感知异常。A,B,D都执行成功,C自己回滚。 */ @Transactional public void test(){ A(); //REQUIRED B(); //REQUIRES_NEW try { C(); //REQUIRED }catch (Exception e){ e.printStackTrace(); } D(); //REQUIRES_NEW }