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中,并且不继续往上抛异常。这样就能保证,如果内部嵌套事务中出现异常,只回滚内部事务,而不影响外部事务。