前言
在日常开发中,我们经常使用Spring
事务。最近,一个朋友去面试,被问到了这样一个面试题:在什么情况下,Spring 事务会失效?
今天,我将和大家聊聊Spring
事务失效的 9 种场景。
1. 抛出检查异常(checked exceptions)
例如,你的事务控制代码如下:
java
@Transactional
public void transactionTest() throws IOException {
User user = new User();
UserService.insert(user);
throw new IOException();
}
如果没有特别指定@Transactional
,Spring 默认只会在遇到运行时异常RuntimeException
或错误时回滚,而检查异常如IOException
不会触发回滚。
typescript
public boolean rollbackOn(Throwable ex) {
return (ex instanceof RuntimeException || ex instanceof Error);
}
解决方案:
知道原因后,解决方案也很简单。配置rollbackFor
属性,例如:@Transactional(rollbackFor = Exception.class)
。
java
@Transactional(rollbackFor = Exception.class)
public void transactionTest() throws IOException {
User user = new User();
UserService.insert(user);
throw new IOException();
}
2. 业务方法本身捕获并处理了异常
ini
@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 就无法处理事务了。
看了上面的代码,你可能会觉得这么简单的问题,自己不可能犯这种低级错误。但我想告诉你,我身边几乎有一半的人都曾因此困扰过。
在编写业务代码时,代码可能会更复杂,有很多嵌套的方法。稍不注意,就很容易触发这个问题。举个简单的例子,假设你有一个审计功能,每次方法执行完后,将审计结果保存到数据库中。那么代码可能会写成这样:
java
@Service
public class TransactionService {
@Transactional(rollbackFor = Exception.class)
public void transactionTest() throws IOException {
User user = new User();
UserService.insert(user);
throw new IOException();
}
}
下面的切面会作用于TransactionService
:
ini
@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. 同一个类中的方法调用
java
@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()
。
java
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
。
kotlin
// 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
。如果你不了解这些传播策略的原理,很容易导致事务失效。
java
@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 管理
typescript
// @Service
public class OrderServiceImpl implements OrderService {
@Transactional
public void updateOrder(Order order) {
// update order
}
}
如果此时@Service
注解被注释掉,这个类就不会被 Spring 加载为 Bean,那么这个类就不会被 Spring 管理,事务自然也会失效。
解决方案:
确保每个使用事务注解的Service
都被 Spring 管理。
8. 多线程调用
scss
@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 项目中配置以下代码:
css
@EnableTransactionManagement
解决方案:
确保在项目中正确配置了事务管理器。
总结
本文简要阐述了 Spring 事务的实现原理,并列出了 9 种 Spring 事务失效的场景。相信很多朋友可能都遇到过这些问题。文章也详细解释了失效的原因,希望大家对 Spring 事务有新的理解。