缺省情况下,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
逻辑并不会如期执行。