Spring 事务注解@Transactional使用注意事项

本文解析了Spring框架中事务管理的关键要点,包括@Transactional注解的正确使用、自调用场景下的事务失效、事务边界内的异常捕捉误区及如何避免在事务内发送消息等问题。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

缺省情况下,Spring 事务基于Spring AOP技术,此时使用事务注解 @Transactional 需要留意以下问题 :

1. 不要在 protected,private 或者包内可见方法上使用注解 @Transactional ;

不要在 protected,private 或者包内可见方法上使用注解 @Transactional,用了也无效。

要点 : 缺省情况下, @Transactional 注解要应用在可见性为 public 的方法上 ;

下面信息摘自Spring官方文档 :

Method visibility and @Transactional

When using proxies, you should apply the @Transactional annotation only to methods with public visibility. If you do annotate protected, private or package-visible methods with the @Transactional annotation, no error is raised, but the annotated method does not exhibit the configured transactional settings. Consider the use of AspectJ (see below) if you need to annotate non-public methods.

2. @Transactional注解的方法在自调用场景中无效 ;

自调用(self-invocation)中@Transactional事务注解无效的例子 :


@Service
class FooService{
    // 该方法有事务注解
    @Transactional
    public void methodB(){
         //...
    }

    // 注意:该方法没有事务注解
    public void methodA(){
        // 这里调用了同一个对象的另一个方法 methodB(), 虽然 methodB 上使用了
        // 注解 @Transactional, 但事实上却无效
        this.methodB();
    }
}

要想解决上面自调用方式带来的@Transactional无效问题,可以使用下面的方法 :

@Service
class BarService{
    @Autowired
    FooService fooService;

    // 该方法会调用服务 fooService 的方法 methodB(),因为当前对象
    // 和 fooService 是两个对象,所以解决了上面提到的自调用问题,
    // 也就是说,methodB()方法上的事务注解会生效
    public void invokeMethodBOfFooService(){
        fooService.methodB();
    }
}

下面信息摘自Spring官方文档 :

In proxy mode (which is the default), only external method calls coming in through the proxy are intercepted. This means that self-invocation, in effect, a method within the target object calling another method of the target object, will not lead to an actual transaction at runtime even if the invoked method is marked with @Transactional.

3. 不要从@Transactional方法内向外传递任何对当前事务做结论的消息

例子 :

@Service
class OrderService {
    @Transactional
    public void completeOrder(int orderId){
    	// 1. 数据库操作 : 操作订单表,将订单状态修改为完成
    	final OrderStatus targetStatus=OrderStatus.COMPLETED;
        updateOrderStatus(orderId,targetStatus);

        // 2. 数据库操作 : 往订单变更记录表中插入一条记录,记录相应的订单变化
        addOrderChangeRecord(orderId,targetStatus);

		// 3. 发送消息操作 : 向外部通知订单完成的消息
        notifyOnOrderComplete(orderId);
    }

该例子中,我们模拟了这样一个场景:完成订单,并在订单完成时向外发出消息(这样该消息的订阅者就可以做出相应的处理)。上面的代码表面上看起来正是在做这样的事情,但实际上却是有问题的,现在我们来分析一下这个问题。我们先将上面的逻辑拆解成如下表所示,很快就能指出问题所在:

执行步骤/时序描述
1.事务开始由Spring通过AOP机制准备事务
2.方法开始public void completeOrder(int orderId){// 可以简单理解成方法体开始的{位置
3.方法内步骤1执行        updateOrderStatus(orderId,targetStatus);
4.方法内步骤2执行        addOrderChangeRecord(orderId,targetStatus);
5.方法内步骤3执行        notifyOnOrderComplete(orderId);
6.方法结束}// 可以简单理解成方法体结束的}位置
7.事务结束(提交或者回滚)由Spring通过AOP机制提交/回滚事务

现在想象一下,上面的逻辑执行到了步骤5之后,订单完成的消息已经发了出去,现在该执行步骤6,7了,此时发出去的消息已经被订阅方开始处理了,但是步骤7中的事务出现了提交失败,导致整个方法逻辑内的数据库操作必须要回滚。现在错误就很明确了 : 整个完成订单数据库操作被回滚,像是没事儿发生一样,但是又有一条消息发出声明订单完成了。

由此可见,如果要想在事务完成时发消息,必须不能在上面例子的方法体内发消息,必须想办法让消息的发送发生在事务提交之后。

4. 尽量不要从@Transactional方法内捕捉数据库操作异常

有的时候,你想捕捉数据库操作异常,然后根据异常进行相应的处理。比如你可能想在一个addXXX方法遇到主键重复抛出异常时捕获该异常然后给用户一个有好的提示,很有可能代码会写成这个样子:

@Transactional
public long addUser(String username,String password)
{
	try 	{
		// 添加用户的数据库操作
	}
	catch (DataIntegrityViolationException e)
	{
		// 输出日志,重新组织异常以便给用户提供一个友好易于理解的错误消息
	}
}

基于这段代码,通常是希望在添加用户时,如果遇到用户名重复,底层会抛出一个DataIntegrityViolationException异常,上面的catch逻辑对该异常进行捕获和转换处理,从而给用户一个易于理解的提示,比如"该用户名已经占用’。但实际上,这里 catch 中预期的语句并不会被执行。原因和上面提到的一样,真正的异常抛出会发生在方法体结束时其外壳代理方法中,此时已经超出了上面catch所关注的范围,所以上面的catch逻辑并不会如期执行。

参考文档

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值