最近开发时遇到了需要使用事务的场景,之前的开发场景,都没有使用过事务,因此记录一下使用实践,以及踩坑。回想之前的开发经历,发现有一些场景是需要使用事务的,只不过当时开发经验有限,没想到使用事务😓😓
背景
需求背景很简单,就是有一些对数据库的操作,要求要不都执行成功,要不回滚,不允许出现一半成功、一半失败的情况,因此考虑使用事务实现。在Spring中,我们既可以使用@Transcational注解实现事务,也可以通过代码手动实现事务。
编程式事务
编程式事务,即通过代码实现,需手动设置事务状态,示例代码如下:
@Service
public class UserService {@Autowiredprivate TransactionTemplate transactionTemplate;@Autowiredprivate UserRepository userRepository;public void transferMoney(Long fromId, Long toId, BigDecimal amount) {// 使用TransactionTemplate进行事务管理transactionTemplate.execute(new TransactionCallbackWithoutResult() {@Overrideprotected void doInTransactionWithoutResult(TransactionStatus status) {try {User fromUser = userRepository.findById(fromId).orElseThrow(() -> new RuntimeException("用户不存在"));User toUser = userRepository.findById(toId).orElseThrow(() -> new RuntimeException("用户不存在"));fromUser.setBalance(fromUser.getBalance().subtract(amount));toUser.setBalance(toUser.getBalance().add(amount));userRepository.save(fromUser);userRepository.save(toUser);} catch (Exception e) {// 手动回滚事务status.setRollbackOnly();throw e;}}});}
}
声明式事务
声明式事务,即通过注解实现,其原理是通过AOP切面实现的。使用注解,可以将具体业务与事务处理部分解耦,代码侵入性很低,所以我们在开发过程中,使用注解的场景更多一些。但是使用注解也会有一些限制,我们会在后面说明。声明式事务示例代码如下:
@Service
public class UserService {@Autowiredprivate UserRepository userRepository;// 使用@Transactional注解声明事务@Transactional(rollbackFor = Exception.class)public void transferMoney(Long fromId, Long toId, BigDecimal amount) {User fromUser = userRepository.findById(fromId).orElseThrow(() -> new RuntimeException("用户不存在"));User toUser = userRepository.findById(toId).orElseThrow(() -> new RuntimeException("用户不存在"));fromUser.setBalance(fromUser.getBalance().subtract(amount));toUser.setBalance(toUser.getBalance().add(amount));userRepository.save(fromUser);userRepository.save(toUser);}
}
问题描述
/**
* 子模块处理真实逻辑
*/
public abstract void handleProcess(UserContext context);@Override
public boolean process(UserContext context) {// 前置逻辑处理try {handleProcess(context);} catch(Throwable t) {log.error("子模块处理异常:{}", ExceptionUtils.logStackTrace(t));return false;}return true;
}
问题代码如上所示,上述代码所在类是一个抽象类,里面有一个抽象方法。我们有一个子类继承了该抽象类,并实现了抽象方法。其相关代码如下所示:
public class ChildrenProcessPlugin extends BaseProcessPlugin {@ResourceUserService userService@Override@Transactional(rollbackFor = Exception.class)public void handleProcess(UserContext context) {// 前置处理userService.processUser();// 更新库表操作updateUserData();}
}
我在子类实现的handleProcess方法上添加了@Transactional注解,希望该方法的执行可以实现事务性,即handleProcess方法中抛出异常时,整个方法对数据库的操作可以自动回滚。(注意,userService.processUser()方法也被@Transactional注解修饰)。
但是在实际测试时,发现测试结果并不符合预期:当updateUserData方法抛出异常时,userService.processUser()方法中对数据库的操作并没有回滚。
问题原因
首先请求了Claude大模型,它并没有分析出我写的代码存在问题,我让他修复代码,它给出了编程式事务的方式,用这种方法,确实可以解决问题,但是这不是我想要的,因此继续去网上查询资料。在https://juejin.cn/post/6844904096747503629中发现了问题所在,这篇文章介绍了6种注解失效的场景,我们的场景符合第4类:**同一个类中方法调用,导致@Transactional注解失效。**详细介绍就是比如有一个类Test,它的一个方法A,A再调用本类的方法B(无论方法B是用public还是private修饰),但方法A没有被事务注解修饰,而B方法被事务注解修饰。则在外部方法调用方法A之后,方法B的注解是不会生效的。那么为什么不起作用呢?其实还是由于注解的事务是使用Spring AOP注解实现的,因为只有当事务方法被当前类以外的方法调用时,才会由Spring生成的代理对象来管理。Spring创建的代理对象只能拦截从外部进入类的方法调用。当在类的内部进行方法调用时,这些调用不会经过代理类,因此不会触发事务管理逻辑。
问题解决
将**@Transactional注解**加在最外层被调用的方法上,且注意最外层被调用的方法不得catch住所有异常而不抛出。如果无法在最外层被调用的地方添加注解(比如该方法是个抽象方法,其他实现方法不需要事务性),则可以通过编程式事务显式处理。
后话
除了此种场景外,其他**@Transactional**失效的场景我们也需要注意下:
- @Transactional注解需要应用在public方法上,非public方法不生效;
- @Transactional注解属性propagation设置错误,没有开启事务;
- @Transactional注解属性rollbackFor设置错误(默认针对非检查异常或者Error才回滚事务)
- 异常被你的catch”吃了”导致**@Transactional**失效
- 数据库引擎本身不支持事务(如果数据库引擎本身不支持事务,如MyISAM,则事务不会生效)