今天分析spring 声明式事务 @Transactional :
事务管理在系统开发中是不可缺少的一部分,Spring提供了很好事务管理机制,主要分为编程式事务和声明式事务两种。
而声明式事务管理方法允许开发者配置的帮助下来管理事务,而不需要依赖底层API进行硬编码。开发者可以只使用注解或配置的XML 来管理。
A、@Transactional 注解直接添加到需要的方法和类上面
//默认配置下 Spring 只会回滚运行时、未检查的异常(继承自 RuntimeException 的异常)或者 Error。
// checked 的异常或数据库操作的异常不回滚
@Transactional
public void testTra() {
// 事务相关操作
}
B、xml方式配置开启:
<tx:annotation-driven />
<bean id="transactionManager"
class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource" />
</bean>
1、声明式事务的优点:
通过上面的例子,其实我们可以很容易的看出来,声明式事务帮助我们节省了很多代码,他会自动帮我们进行事务的开启、提交以及回滚等操作,把程序员从事务管理中解放出来。
声明式事务管理使用了 AOP 实现的,本质就是在目标方法执行前后进行拦截。 在目标方法执行前加入或创建一个事务,在执行方法执行后,根据实际情况选择提交或是回滚事务。
使用这种方式,对代码没有侵入性,方法内只需要写业务逻辑就可以了。
但是,声明式事务也有一定的弊端。
2、声明式事务的粒度问题:
首先,声明式事务有一个局限,那就是他的最小粒度要作用在方法上(也就是说,如果想要给一部分代码块增加事务的话,那就需要把这个部分代码块单独独立出来作为一个方法)。
因为声明式事务是通过注解的(有些时候还可以通过配置实现),这就会导致一个问题,那就是这个事务有可能被开发者忽略。
事务被忽略了有什么问题呢?
首先,如果开发者没有注意到一个方法是被事务嵌套的,那么就可能会再方法中加入一些如RPC远程调用、消息发送、缓存更新、文件写入等操作。
我们知道,这些操作如果被包在事务中,有两个问题:
1、这些操作自身是无法回滚的,这就会导致数据的不一致。可能RPC调用成功了,但是本地事务回滚了,可是PRC调用无法回滚了。
2、在事务中有远程调用,就会拉长整个事务。那么久会导致本事务的数据库连接一直被占用,那么如果类似操作过多,就会导致数据库连接池耗尽。
有些时候,即使没有在事务中进行远程操作,但是有些人还是可能会不经意的进行一些内存操作,如运算。或者如果遇到分库分表的情况,有可能不经意间进行跨库操作。
但是如果是编程式事务的话,业务代码中就会清清楚楚看到什么地方开启事务,什么地方提交,什么时候回滚。这样有人改这段代码的时候,就会强制他考虑要加的代码是否应该方法事务内。
3、因此,使用声明式事务时要特别小心,保证团队里的每个人都知道,并熟悉其原理,否则 可能会存在潜在的故障;同时 建议大家使用编程式事务,通过规则和机制保证系统的稳定性。
声明式事务用不对容易失效:
除了事务的粒度问题,还有一个问题那就是声明式事务虽然看上去帮我们简化了很多代码,但是如果没用对,也很容易导致事务失效。
如以下几种场景就可能导致声明式事务失效:
1、@Transactional 应用在非 public 修饰的方法上(只有@Transactional 注解应用到 public 方法,才能进行事务管理),错误事例如下:
// protected 或 private 修饰的方法,事务不生效
@Transactional(rollbackFor=Exception.class)
private void saveRelation(){
//批量保存接口
serviceDataRelationMapper.batchInsertServiceDataRelation(serviceDataRelations);
//单个保存接口
catalogInfoMapper.insertSelective(catalogInfo);
//抛出异常
if(serviceDataRelation.getServiceId()>25){
throw new RuntimeException("出现异常");
}
}
目前如果非 public 修饰的方法上加上事务, idea 有红线提示
原因:这是因为在使用 Spring AOP 代理时,Spring 在调用 TransactionInterceptor 在目标方法执行前后进行拦截之前,DynamicAdvisedInterceptor(CglibAopProxy 的内部类)的 intercept 方法或 JdkDynamicAopProxy 的 invoke 方法会间接调用 AbstractFallbackTransactionAttributeSource(Spring 通过这个类获取 @Transactional 注解的事务属性配置属性信息)的 computeTransactionAttribute 方法。
/**
* 这个方法会检查目标方法的修饰符是不是 public,若不是 public,就不会获取@Transactional 的属性配置
信息,最终会造成不会用 TransactionInterceptor 来拦截该目标方法进行事务管理。
*/
protected TransactionAttribute computeTransactionAttribute(Method method,
Class<?> targetClass) {
// Don't allow no-public methods as required.
if (allowPublicMethodsOnly() && !Modifier.isPublic(method.getModifiers())) {
return null;}
2、@Transactional 注解属性 propagation 设置错误,设置事务的传播属性:
事物传播行为介绍:
@Transactional(propagation=Propagation.REQUIRED) :如果有事务, 那么加入事务, 没有的话新建一个(默认情况下)
@Transactional(propagation=Propagation.NOT_SUPPORTED) :容器不为这个方法开启事务
@Transactional(propagation=Propagation.REQUIRES_NEW) :不管是否存在事务,都创建一个新的事务,原来的挂起,新的执行完毕,继续执行老的事务
@Transactional(propagation=Propagation.MANDATORY) :必须在一个已有的事务中执行,否则抛出异常
@Transactional(propagation=Propagation.NEVER) :必须在一个没有的事务中执行,否则抛出异常(与Propagation.MANDATORY相反)
@Transactional(propagation=Propagation.SUPPORTS) :如果其他bean调用这个方法,在其他bean中声明事务,那就用事务.如果其他bean没有声明事务,那就不用事务.
@Transactional 默认方式 Propagation.REQUIRED,如果设置错误,事务可能失效。
错误方式:
注意下面三种 propagation 可以不启动事务。本来期望目标方法进行事务管理,但若是错误的配置这三种 propagation,事务将不会发生回滚。
- TransactionDefinition.PROPAGATION_SUPPORTS:如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行。
- TransactionDefinition.PROPAGATION_NOT_SUPPORTED:以非事务方式运行,如果当前存在事务,则把当前事务挂起。
- TransactionDefinition.PROPAGATION_NEVER:以非事务方式运行,如果当前存在事务,则抛出异常。
3、@Transactional 注解属性 rollbackFor 设置错误,某些场景错误事例
/**
* @Transactional 默认回滚设置是 (rollbackFor=RuntimeException.class),如果出现的不是RuntimeException或其子类类型的异常,则不会抛出。
/
@Transactional
pubilc void saveRelation(){
//批量保存接口...省略其他业务代码
serviceDataRelationMapper.batchInsertServiceDataRelation(serviceDataRelations);
//单个保存接口...省略其他业务代码
catalogInfoMapper.insertSelective(catalogInfo);
}
4、同一个类中方法调用,导致@Transactional失效,多种情况均分析一下:
A、生效情况:
//在方法入口处加事务(必须加)
@Transactional(rollbackFor=Exception.class)
@Override
public void saveCatalogsAndDataRelations(ServiceSetCatalogModel setCatalogModel) {
//调用其他方法操作
saveRelation(setCatalogModel);
}
public的方法如下:
//这里不加事务(加上也没有副作用)
pubilc void saveRelation(){
//批量保存接口...省略其他业务代码
serviceDataRelationMapper.batchInsertServiceDataRelation(serviceDataRelations);
//单个保存接口...省略其他业务代码
catalogInfoMapper.insertSelective(catalogInfo);
}
B、生效情况:
//在方法入口处加事务(必须加)
@Transactional(rollbackFor=Exception.class)
@Override
public void saveCatalogsAndDataRelations(ServiceSetCatalogModel setCatalogModel) {
//批量保存接口...省略其他业务代码
serviceDataRelationMapper.batchInsertServiceDataRelation(serviceDataRelations);
//调用其他方法操作
saveRelation(setCatalogModel);
}
public 方法:
//这里不加事务(加上也没有副作用)
pubilc void saveRelation(){
//单个保存接口...省略其他业务代码
catalogInfoMapper.insertSelective(catalogInfo);
}
C、注意:不生效情况:
//在方法入口处没有加事务
@Override
public void saveCatalogsAndDataRelations(ServiceSetCatalogModel setCatalogModel) {
//调用其他方法操作
saveRelation(setCatalogModel);
}
public的方法如下:
//这里加事务(此处加不生效)
@Transactional(rollbackFor=Exception.class)
pubilc void saveRelation(){
//批量保存接口...省略其他业务代码
serviceDataRelationMapper.batchInsertServiceDataRelation(serviceDataRelations);
//单个保存接口...省略其他业务代码
catalogInfoMapper.insertSelective(catalogInfo);
}
原因:@Transactional注解底层使用的是动态代理来进行实现的,如果在调用本类中的方法,此时不添加@Transactional注解,而是在调用类中使用thisi调用本方法中的另外一个方法添加了@Transactional注解,此时调用的方法上的@Transactional注解是不起作用的。
5、异常被catch捕获导致@Transactional失效
A、调用字方法 失效场景:
//在方法入口处加事务(必须加)
@Transactional(rollbackFor=Exception.class)
@Override
public void saveCatalogsAndDataRelations(ServiceSetCatalogModel setCatalogModel) {
//调用其他方法操作
saveRelation(setCatalogModel);
}
public方法:
//这里业务有加 catch
pubilc void saveRelation(){
//批量保存接口...省略其他业务代码
serviceDataRelationMapper.batchInsertServiceDataRelation(serviceDataRelations);
//单个保存接口...省略其他业务代码
try{
catalogInfoMapper.insertSelective(catalogInfo);
int a = 1/0;//抛异常的点
}catch (Exception e){
e.printStackTrace();
}
}
B、本方法失效场景:
//在方法入口处加事务
@Transactional(rollbackFor=Exception.class)
@Override
public void saveCatalogsAndDataRelations(ServiceSetCatalogModel setCatalogModel) {
//批量保存接口...省略其他业务代码
serviceDataRelationMapper.batchInsertServiceDataRelation(serviceDataRelations);
//单个保存接口...省略其他业务代码
try{
catalogInfoMapper.insertSelective(catalogInfo);
int a = 1/0;//抛异常的点
}catch (Exception e){
e.printStackTrace();
}
}
分析:事务的回滚是方法发生异常时进行的,在aop的异常通知中进行拦截,回滚;如果方法中捕获了异常,是不会被aop的异常通知拦截到的。
如果使用了try catch捕获异常,需要在catch中抛出一个异常或者在catch中通过
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly() // 设置手动回滚。
另外,如果你系统做了异常的统一捕获(自定义切面异常),那么就禁止使用 声明式事务吧!
6、数据库引擎不支持事务
有些数据库不支持事务,那么无论你怎么加都不会生效。而以上几个事例是采用的mysql 数据库,肯定是支持事务的。
但是我还是那句话,我们确实无法保证所有人的能力都很高,也无法要求所有开发者都能不出错。我们能做的就是,尽量可以通过机制或者规范,来避免或者降低这些问题发生的概率。
其实,如果大家有认真看过阿里巴巴出的那份Java开发手册的话,其实就能发现,其中的很多规约并不是完完全全容易被人理解,有些也比较生硬,但是其实,这些规范都是从无数个坑里爬出来的开发者们总结出来的。本文只是建议大家日后在使用事务的时候,能够考虑到本文中提到的观点,然后自行选择。