Spring事务失效的9大场景与解决方法
作者:写bug写bug
前言
在日常开发中,我们经常使用Spring事务。最近,一个朋友去面试,被问到了这样一个面试题:在什么情况下,Spring 事务会失效?
今天,我将和大家聊聊Spring事务失效的 9 种场景。
1. 抛出检查异常(checked exceptions)
例如,你的事务控制代码如下:
@Transactional
public void transactionTest() throws IOException {
User user = new User();
UserService.insert(user);
throw new IOException();
}
如果没有特别指定@Transactional,Spring 默认只会在遇到运行时异常RuntimeException或错误时回滚,而检查异常如IOException不会触发回滚。
public boolean rollbackOn(Throwable ex) {
return (ex instanceof RuntimeException || ex instanceof Error);
}解决方案:
知道原因后,解决方案也很简单。配置rollbackFor属性,例如:@Transactional(rollbackFor = Exception.class)。
@Transactional(rollbackFor = Exception.class)
public void transactionTest() throws IOException {
User user = new User();
UserService.insert(user);
throw new IOException();
}2. 业务方法本身捕获并处理了异常
@Transactional(rollbackFor = Exception.class)
public void transactionTest() {
try {
User user = new User();
UserService.insert(user);
int i = 1 / 0;
} catch (Exception e) {
e.printStackTrace();
}
}在这个场景中,事务失效的原因也很简单。Spring 是否回滚事务取决于你是否抛出了异常。如果你自己捕获了异常,Spring 就无法处理事务了。
看了上面的代码,你可能会觉得这么简单的问题,自己不可能犯这种低级错误。但我想告诉你,我身边几乎有一半的人都曾因此困扰过。
在编写业务代码时,代码可能会更复杂,有很多嵌套的方法。稍不注意,就很容易触发这个问题。举个简单的例子,假设你有一个审计功能,每次方法执行完后,将审计结果保存到数据库中。那么代码可能会写成这样:
@Service
public class TransactionService {
@Transactional(rollbackFor = Exception.class)
public void transactionTest() throws IOException {
User user = new User();
UserService.insert(user);
throw new IOException();
}
}下面的切面会作用于TransactionService:
@Component
publicclass AuditAspect {
@Autowired
private AuditService auditService;
@Around(value = "execution (* com.dylan.service.*.*(..))")
public Object around(ProceedingJoinPoint pjp) {
try {
Audit audit = new Audit();
Signature signature = pjp.getSignature();
MethodSignature methodSignature = (MethodSignature) signature;
String[] strings = methodSignature.getParameterNames();
audit.setMethod(signature.getName());
audit.setParameters(strings);
Object proceed = pjp.proceed();
audit.success(true);
return proceed;
} catch (Throwable e) {
log.error("{}", e);
audit.success(false);
}
auditService.save(audit);
returnnull;
}
}在上面的例子中,如果程序执行异常,事务也会失效。原因是Spring的事务切面优先级最低。如果异常被切面捕获,Spring 自然无法正确处理事务,因为事务管理器无法捕获到异常。
解决方案:
只需移除try-catch。虽然我们知道在处理事务时,业务代码不能自己捕获异常,但只要代码变得复杂,我们很容易不小心犯错。
3. 同一个类中的方法调用
@Service
publicclass DefaultTransactionService implements Service {
public void saveUser() throws Exception {
// do something
doInsert();
}
@Transactional(rollbackFor = Exception.class)
public void doInsert() throws IOException {
User user = new User();
UserService.insert(user);
thrownew IOException();
}
}这也是一个容易出错的场景。事务失效的原因也很简单。因为 Spring 的事务管理功能是通过动态代理实现的,而 Spring 默认使用 JDK 动态代理,JDK 动态代理通过接口实现,并通过反射调用目标类。简单理解,在saveUser()方法中,调用this.doInsert()时,this是真实对象,因此会直接执行doInsert的业务逻辑,而不是代理逻辑,从而导致事务失效。
解决方案:
方案 1:直接在saveUser方法上添加@Transactional注解。
方案 2:可以将这两个方法拆分到不同的类中。
方案 3:不使用注解实现事务,而是使用编程式事务来包裹需要开启事务的代码块。例如:transactionTemplate.execute()。
public void doInsert() throws IOException {
transactionTemplate.execute(() -> {
User user = new User();
UserService.insert(user);
throw new IOException();
});
}4. 方法使用了final或static关键字
如果 Spring 使用 Cglib 代理实现(当你的代理类没有实现接口时),而你的业务方法恰好使用了final或static关键字,那么事务控制也会失效。因为 Cglib 使用字节码增强技术生成被代理类的子类,并重写被代理类的方法来实现代理。如果被代理的方法使用了final或static关键字,子类就无法重写被代理的方法。
如果 Spring 使用 JDK 动态代理实现,JDK 动态代理是基于接口实现的,那么被final和static修饰的方法也无法被代理。
总之,如果方法连代理都没有,那么事务回滚肯定无法实现。
解决方案:
尽量移除方法上的final或static关键字。
5. 方法不是public
如果方法不是public,Spring 事务也会失效,因为在 Spring 事务管理的源码AbstractFallbackTransactionAttributeSource中,computeTransactionAttribute()方法会判断目标方法是否是public。如果不是public,则返回null。
// Don't allow no-public methods as required.
if (allowPublicMethodsOnly() && !Modifier.isPublic(method.getModifiers())) {
return null;
}解决方案:
将当前方法的访问级别改为public。
6. 传播机制使用不当
Spring事务的传播机制指的是当多个事务方法相互调用时,事务应该如何传播的策略。Spring提供了七种事务传播机制:REQUIRED、SUPPORTS、MANDATORY、REQUIRES_NEW、NOT_SUPPORTED、NEVER、NESTED。如果你不了解这些传播策略的原理,很容易导致事务失效。
@Service
publicclass TransactionsService {
@Autowired
private UserMapper userMapper;
@Autowired
private AddressMapper addressMapper;
@Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class)
public void doInsert(User user, Address address) throws Exception {
// do something
userMapper.insert(user);
saveAddress(address);
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void saveAddress(Address address) {
// do something
addressMapper.insert(address);
}
}
在上面的例子中,如果用户插入失败,不会导致saveAddress()回滚,因为这里使用的传播机制是REQUIRES_NEW。REQUIRES_NEW的原理是,如果当前方法没有事务,则创建一个新事务。如果当前方法已经有事务,则挂起当前事务并创建一个新事务。父事务会等到当前事务完成后才提交。如果父事务发生异常,不会影响子事务的提交。
解决方案:
将事务传播策略改为默认值REQUIRED。REQUIRED的原理是,如果当前有事务,则加入该事务。如果没有事务,则创建一个新事务。父事务和被调用的事务处于同一个事务中。即使被调用的事务捕获了异常,整个事务仍然会回滚。
7. 没有被 Spring 管理
// @Service
public class OrderServiceImpl implements OrderService {
@Transactional
public void updateOrder(Order order) {
// update order
}
}如果此时@Service注解被注释掉,这个类就不会被 Spring 加载为 Bean,那么这个类就不会被 Spring 管理,事务自然也会失效。
解决方案:
确保每个使用事务注解的Service都被 Spring 管理。
8. 多线程调用
@Service
publicclass UserService {
@Autowired
private UserMapper userMapper;
@Autowired
private RoleService roleService;
@Transactional
public void add(UserModel userModel) throws Exception {
userMapper.insertUser(userModel);
new Thread(() -> {
try {
test();
} catch (Exception e) {
roleService.doOtherThing();
}
}).start();
}
}
@Service
publicclass RoleService {
@Transactional
public void doOtherThing() {
try {
int i = 1 / 0;
System.out.println("save role table data");
} catch (Exception e) {
thrownew RuntimeException();
}
}
}我们可以看到,在事务方法add中,调用了事务方法doOtherThing,但doOtherThing是在另一个线程中被调用的。
这会导致两个方法不在同一个线程中,获取的数据库连接也不同,因此是两个不同的事务。如果在doOtherThing方法中抛出异常,add方法是不可能回滚的。
我们所说的同一个事务,实际上指的是同一个数据库连接。只有在同一个数据库连接下,才能同时提交和回滚。如果在不同的线程中,获取的数据库连接肯定不同,因此它们是不同的事务。
解决方案:
这有点像分布式事务。尽量确保在同一个事务中处理。
9. 没有配置开启事务
如果在项目中没有配置 Spring 的事务管理器,即使使用了 Spring 的事务管理功能,Spring 的事务也不会生效。例如,如果你是一个 Spring Boot 项目,并且没有在 Spring Boot 项目中配置以下代码:
@EnableTransactionManagement
解决方案:
确保在项目中正确配置了事务管理器。
总结
本文简要阐述了 Spring 事务的实现原理,并列出了 9 种 Spring 事务失效的场景。相信很多朋友可能都遇到过这些问题。文章也详细解释了失效的原因,希望大家对 Spring 事务有新的理解。
到此这篇关于Spring事务失效的9大场景与解决方法的文章就介绍到这了,更多相关Spring事务失效内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!
