Spring事务失效的10大场景
对于从事java开发工作的同学来说,Spring的事务肯定是再熟悉不过了,我们一般就用一个简单的注解:@Transactional,就能轻松搞定事务。但是如果使用不当,也会坑到你怀疑人生。
那今天我们就来聊一聊,事务失效的场景。
总的来说分为两种,一种是事务不生效,一种是事务不回滚
一、事务不生效
1.访问权限问题
@Nullable
protected TransactionAttribute computeTransactionAttribute(Method method, @Nullable Class<?> targetClass) {
// Don't allow no-public methods as required.
// 仅只有public 修饰的方法 事务才能生效
if (allowPublicMethodsOnly() && !Modifier.isPublic(method.getModifiers())) {
return null;
}
// The method may be on an interface, but we need attributes from the target class.
// If the target class is null, the method will be unchanged.
Method specificMethod = AopUtils.getMostSpecificMethod(method, targetClass);
// First try is the method in the target class.
TransactionAttribute txAttr = findTransactionAttribute(specificMethod);
if (txAttr != null) {
return txAttr;
}
// Second try is the transaction attribute on the target class.
txAttr = findTransactionAttribute(specificMethod.getDeclaringClass());
if (txAttr != null && ClassUtils.isUserLevelMethod(method)) {
return txAttr;
}
if (specificMethod != method) {
// Fallback is to look at the original method.
txAttr = findTransactionAttribute(method);
if (txAttr != null) {
return txAttr;
}
// Last fallback is the class of the original method.
txAttr = findTransactionAttribute(method.getDeclaringClass());
if (txAttr != null && ClassUtils.isUserLevelMethod(method)) {
return txAttr;
}
}
return null;
}
spring事务的源码,在AbstractFallbackTransactionAttributeSource类的computeTransactionAttribute方法中有个判断,如果目标方法不是public,则TransactionAttribute返回null,即不支持事务。
因此如果我们自定义方法,它的访问权限不是public,而是protected、default或者private,spring则不会提供事务功能。
2.方法用final修饰
如果某个方法不想被子类重写,这时我们可以将该方法定义成final。spring事务底层使用了aop,也就是通过jdk动态代理或者cglib,帮我们生成了代理类,在代理类中实现的事务功能。
但如果某个方法用final修饰了,那么在它的代理类中,就无法重写该方法,而无法添加事务功能。
注意: 如果某个方法是static的,同样无法通过动态代理,变成事务方法。
3.方法内部调用
有时候我们需要在某个Service类的某个方法中,调用另外一个事务方法,这个是我们经常会犯错的地方。示例如下:
@Service
public class OrderServiceImpl implements OrderService {
@Override
public void updateOrder() {
//TODO
updateOrderItem();
}
@Transactional(rollbackFor = Exception.class)
public void updateOrderItem() {
//TODO
}
}
那么问题来了,如果有些场景,确实想在同一个类的某个方法中,调用它自己的另外一个方法,该怎么办呢?
3.1: 将事务方法抽出来,放到新的类中
@Service
public class OrderServiceImpl implements OrderService {
@Resource
private OrderItemServiceImpl orderItemService;
@Override
public void updateOrder() {
//TODO
orderItemService.updateOrderItem();
}
}
@Service
class OrderItemServiceImpl {
@Transactional(rollbackFor = Exception.class)
public void updateOrderItem() {
//TODO
}
}
3.2: 将OrderServiceImpl 自己注入自己。
@Service
public class OrderServiceImpl implements OrderService {
@Resource
private OrderServiceImpl orderService;
@Override
public void updateOrder() {
//TODO
orderService.updateOrderItem();
}
@Transactional(rollbackFor = Exception.class)
public void updateOrderItem() {
//TODO
}
}
3.3: 通过AopContent类
在该Service类中使用AopContext.currentProxy()获取代理对象
@Service
public class OrderServiceImpl implements OrderService {
@Resource
private OrderServiceImpl orderService;
@Override
public void updateOrder() {
//TODO
//该Service类中使用AOPProxy获取代理对象
((OrderServiceImpl)AopContext.currentProxy()).updateOrderItem();
}
@Transactional(rollbackFor = Exception.class)
public void updateOrderItem() {
//TODO
}
}
4.未被spring容器管理
我们平时开发过程中,有个细节很容易被忽略。即使用spring事务的前提是:对象要被spring管理,需要创建bean实例。通常情况下,我们通过@Controller、@Service、@Component、@Repository等注解,可以自动实现bean实例化和依赖注入的功能
5.多线程调用
@Service
public class OrderServiceImpl implements OrderService {
@Resource
private OrderItemServiceImpl orderItemService;
@Override
public void updateOrder() {
//TODO
//启动线程调用updateOrderItem()
new Thread(()-> {
orderItemService.updateOrderItem();
});
}
}
@Service
class OrderItemServiceImpl {
@Transactional(rollbackFor = Exception.class)
public void updateOrderItem() {
//TODO
}
}
上面示例中事务是否成功?
我们先理解一些内容。
1:同一个事务,其实是指同一个数据库连接,只有拥有同一个数据库连接才能同时提交和回滚。如果在不同的线程,拿到的数据库连接肯定是不一样的,所以是不同的事务
2: spring的事务是通过数据库连接来实现的
由此我们可以得出,多线程下事务是不生效的。
二、事务不回滚
1: 错误的传播特性
我们在使用@Transactional注解时,是可以指定propagation参数的,扩展其配置不支持事务
@Service
class OrderItemServiceImpl {
@Transactional(rollbackFor = Exception.class,propagation = Propagation.NOT_SUPPORTED)
public void updateOrderItem() {
//TODO
}
}
我们可以看出事务注解的参数 事务传播特性定义成了Propagation.NOT_SUPPORTED,这种类型的传播特性不支持事务,如果有事务则会抛异常。
目前只有这三种传播特性才会创建新事务:NESTED,REQUIRES_NEW,REQUIRED
2.自己吞了异常
自己捕获了异常,导致没有异常抛出,因此事务失效。
3: 手动抛了别的异常
即使开发者没有手动捕获异常,但如果抛的异常不正确,spring事务也不会回滚
@Service
class OrderItemServiceImpl {
@Transactional
public void updateOrderItem() throws Exception {
try {
//更新业务
} catch (Exception e) {
throw new Exception();
}
}
}
默认回滚的是:RuntimeException,如果你想触发其他异常的回滚,需要在注解上配置一下,如:@Transactional(rollbackFor = Exception.class), 这个配置仅限于 Throwable 异常类及其子类。
4.自定义了回滚异常
在使用@Transactional注解声明事务时,有时我们想自定义回滚的异常,spring也是支持的。可以通过设置rollbackFor参数,来完成这个功能。
但如果这个参数的值设置错了,就会引出一些莫名其妙的问题,例如:
@Service
public class OrderServiceImpl implements OrderService {
@Resource
private OrderItemServiceImpl orderItemService;
@Override
@Transactional(rollbackFor = BackingStoreException.class)
public void updateOrder() {
//TODO
orderItemService.updateOrderItem();
}
}
@Service
class OrderItemServiceImpl {
public void updateOrderItem() {
//更新业务
}
}
程序报错了,抛了SqlException、DuplicateKeyException等异常。而BusinessException是我们自定义的异常,报错的异常不属于BusinessException,所以事务也不会回滚。
注意:
即使rollbackFor有默认值,但阿里巴巴开发者规范中,还是要求开发者重新指定该参数 。因为如果使用默认值,一旦程序抛出了Exception,事务不会回滚,这会出现很大的bug。所以,建议一般情况下,将该参数设置成:Exception或Throwable
5: 事务嵌套
@Service
@Slf4j
public class OrderServiceImpl implements OrderService {
@Resource
private OrderItemServiceImpl orderItemService;
@Override
@Transactional
public void updateOrder() {
//TODO
try {
orderItemService.updateOrderItem();
} catch (Exception e) {
log.info("异常信息: " + e);
}
}
}
@Service
class OrderItemServiceImpl {
@Transactional(propagation = Propagation.NESTED)
public void updateOrderItem() {
//更新业务
}
}
可以将内部嵌套事务放在try/catch中,并且不继续往上抛异常。这样就能保证,如果内部嵌套事务中出现异常,只回滚内部事务,而不影响外部事务。