@Transactional注解概述
@Transactional注解是Spring框架提供的一个用于声明式事务管理的注解。它可以应用在方法或类上,用于标识需要进行事务管理的方法或类
使用方法
作用于类上:表示所有该类的public方法都配置相同的事务属性信息。
作用于方法上:当应用在方法上时,该方法将被纳入事务管理,如果类配置了@Transactional,方法也配置了@Transactional,方法的事务会覆盖类的事务配置信息。
作用于接口上:不推荐这种使用方法,因为一旦标注在Interface上并且配置了Spring AOP 使用CGLib动态代理,将会导致@Transactional注解失效
属性值及其说明
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Transactional {
@AliasFor("transactionManager")
String value() default "";
/**
* 主要用来指定不同的事务管理器
* 满足在同一个系统中,存在不同的事务管理器
**/
@AliasFor("value")
String transactionManager() default "";
/**
*事务传播行为,有7种
* REQUIRED(默认) SUPPORTS MANDATORY REQUIRES_NEW NOT_SUPPORTED NEVER NESTED
**/
Propagation propagation() default Propagation.REQUIRED;
/**
* 事务隔离级别
* DEFAULT:使用底层数据库默认的隔离级别。
* READ_UNCOMMITTED:读未提交
* READ_COMMITTED:读已提交
* REPEATABLE_READ:可重复读
* SERIALIZABLE:串行化
**/
Isolation isolation() default Isolation.DEFAULT;
/**
* 事务超时时间
**/
int timeout() default -1;
/**
* 指定事务是否为只读事务,默认值为 false;为了忽略那些不需要事务的方法,比如读取数据,可以
* 设置 read-only 为 true。
**/
boolean readOnly() default false;
/**
* 导致事务回滚的异常类数组
**/
Class<? extends Throwable>[] rollbackFor() default {};
/**
* 导致事务回滚的异常类名字数组
**/
String[] rollbackForClassName() default {};
/**
* 不会导致事务回滚的异常类数组
**/
Class<? extends Throwable>[] noRollbackFor() default {};
/**
* 不会导致事务回滚的异常类名字数组
**/
String[] noRollbackForClassName() default {};
}
失效场景
@Transaction注解使用需要注意,在实际使用中很容易因为使用不当而导致注解失效,一般常见的失效场景如下:
- 应用在非 public 修饰的方法上:因为事务其实也算是增强方法,所以底层使用到的还是aop,而`的底层就是通过动态代理的方式,cglib动态代理方式使用的是继承,而jdk动态代理使用的是接口,如果存在私有方法,它们都无法直接用代理对象去调用,也就无法实现增强功能,所以必须是public的
- propagation 设置错误:当注解为PROPAGATION_NEVER以非事务方式运行,PROPAGATION_NOT_SUPPORTED以非事务方式运行,如果当前存在事务,则把当前事务挂起,PROPAGATION_SUPPORTS如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行。
- rollbackFor 设置错误:rollbackFor 可以指定能够触发事务回滚的异常类型,spring默认是抛出unchecked异常或者error,想要抛出其他特定的市场可以用这个属性去指定
- 异常被try catch给吞了:使用try-catch的话,可以在异常处理中使用TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
- 同一个类中调用:有个外围方法A,A里面调内围方法B。A、B两个方法在同一个类里面,A未注解,B注解且public修饰。当有其他方法调A方法时候,B方法注解是失效的。因为在外围方法A里面,调用B等同于this.B(),使用的是本地对象方法,没用使用到spring容器,也就无法触发aop,但是反过来,如果A是内围,B是外围则是可以的
- 数据库引擎不支持事务
传播特性及其场景
值 | 含义 |
REQUIR(默认) | 如果当前存在事务,则加入该事务; 如果当前不存在事务,则创建一个新的事务 |
SUPPORTS | 如果当前存在事务,则加入该事务; 如果当前不存在事务,则以非事务的方式继续运行; |
MANDATORY | 如果当前存在事务,则加入该事务; 如果当前不存在事务,则抛出异常; |
REQUIRES_NEW | 如果当前不存在事务,重新创建一个新的事务; 如果当前存在事务,则暂停当前的事务; |
NOT_SUPPORTED | 以非事务的方式运行 如果当前存在事务,则暂停当前的事务 |
NEVER | 以非事务的方式运行 如果当前存在事务,则抛出异常 |
NESTED | 如果当前存在事务,则在该事务内嵌套事务运行; 如果当前不存在事务,则创建一个新的事务; |
现在对场景进行测试,具体场景如下:
- 方法上使用@Transactional,对当前方法起作用,方法发生异常,User,Report均回滚
@Override @Transactional public String test01() { User user = new User(); user.setId(2); user.setUsername("小B"); user.setPassword("123456"); user.setRole(1); user.setPermission("0001"); userMapper.insertSelective(user); Report report = new Report(); report.setId(2); report.setTriggerDay(new Date()); report.setRunningCount(1); report.setFailCount(1); report.setSucCount(1); report.setUpdateTime(new Date()); reportMapper.insertSelective(report); int i = 1/0; return "111"; }
- 类上加@Transaction注解,对该类下所有public修饰的方法生效,方法内发生异常会事务回滚
- 方法类对异常代码进行try-catch,事务失效User、Report会提交
@Override @Transactional public String test03() { User user = new User(); user.setId(2); user.setUsername("小B"); user.setPassword("123456"); user.setRole(1); user.setPermission("0001"); userMapper.insertSelective(user); Report report = new Report(); report.setId(2); report.setTriggerDay(new Date()); report.setRunningCount(1); report.setFailCount(1); report.setSucCount(1); report.setUpdateTime(new Date()); reportMapper.insertSelective(report); try{ int i = 1/0; }catch (Exception e){ System.out.println("方法发生异常啦"); //采用下面这行代码才会事务回滚,或者 TransactionAspectSupport.currentTransactionStatus().setRollbackOnly(); } return "111"; }
4.同一个service类里面 ,外围方法A有注解,内围方法B无注解,事务生效,User,Report都回滚
@Override
@Transactional
public String testA() {
User user = new User();
user.setId(2);
user.setUsername("小B");
user.setPassword("123456");
user.setRole(1);
user.setPermission("0001");
userMapper.insertSelective(user);
tetsB();
//内部方法如果是private修饰的,事务也是生效的
//testC();
return "AAA";
}
@Override
public String tetsB() {
Report report = new Report();
report.setId(2);
report.setTriggerDay(new Date());
report.setRunningCount(1);
report.setFailCount(1);
report.setSucCount(1);
report.setUpdateTime(new Date());
reportMapper.insertSelective(report);
int i = 1/0;
return "BBB";
}
private String testC() {
User user = new User();
user.setId(3);
user.setUsername("小C");
user.setPassword("123456");
user.setRole(1);
user.setPermission("0001");
userMapper.insertSelective(user);
int i = 1 / 0;
return "CCC";
}
5.同一个service类里面 ,外围方法A无注解,内围方法B有注解,事务不生效,User,Report都提交
@Override
public String testA() {
User user = new User();
user.setId(2);
user.setUsername("小B");
user.setPassword("123456");
user.setRole(1);
user.setPermission("0001");
userMapper.insertSelective(user);
tetsB();
return "AAA";
}
@Override
@Transactional
public String tetsB() {
Report report = new Report();
report.setId(2);
report.setTriggerDay(new Date());
report.setRunningCount(1);
report.setFailCount(1);
report.setSucCount(1);
report.setUpdateTime(new Date());
reportMapper.insertSelective(report);
int i = 1/0;
return "BBB";
}
6.同一个Service内方法调用,当@Transactional 注解作用在类上时,事务起作用,数据回滚
7.不同类里面 ,外围方法method07有注解,内围方法addUser无注解,事务生效,User,Report都回滚8.不同类里面 ,外围方法method07无注解,内围方法addUser有注解,事务生效,User回滚,Report不回滚
扩展场景:如果有两个不同的类,由于业务的关系,需要两个类事务,可能在同一个事务内,可能不在同一个事务内。这时候就用到不同的传播特性了。由于代码较多,我就不一一贴代码了,就直接说对比结论吧。
- 当有两个方法,一个外围方法A,一个内围方法B的时候(A方法里面调用B),对A和B加上注解,A默认属性,当B为REQUIRED的时候,B发生异常,也会影响A回滚,当B为NESTED的时候,发生异常不会影响A回滚,它是一个事务里面的子事务
- NESTED和REQUIRES_NEW都可以做到内部方法事务回滚而不影响外围方法事务。但是因为NESTED是嵌套事务,所以外围方法回滚之后,作为外围方法事务的子事务也会被回滚。而REQUIRES_NEW是通过开启新的事务实现的,内部事务和外围事务是两个事务,外围事务回滚不会影响内部事务
- 当有两个方法,一个外围方法A,一个内围方法B的时候,如果想A,B是独立两个事务,出异常互相不影响,那么可以考虑NOT_SUPPORT
在我的项目中,有一个档案归档后需要自动装盒的这么一个功能,其中档案归档是外围方法,使用了REQUIRED,伪代码如下
@Service
public class ArchiveFlieServiceImpl implements ArchiveFlieService{
@Transactional
public void doFile(ArchiveFlie archiveFlie){
//业务代码
try {
archiveboxService.doAutoBox(Box box);
} catch (Exception e) {
}
//业务代码
}
//其他方法...
}
其中如果说自动归档的时候发生错了,那么装盒也是白装盒,自动归档数据回滚,装盒数据也应该回滚。如果自动归档没问题,装盒失败,那只回滚装盒的。这时候doAutoBox这个方法也要加上事务,并且传播属性为NESTED
@Service
public class ArchiveBoxServiceImpl implements ArchiveBoxService{
@Transactional(propagation = Propagation.NESTED)
public void doAutoBox(Box box){
//业务代码
try {
lifeLogService.addLog(LifeLog lifeLog);
} catch (Exception e) {
}
//业务代码
}
//其他方法.....
}
除此之外,在这流程中还要记录生命周期日志,但是日志可有可无,不能影响我归档或者装盒。日志添加失败也没问题不需要回滚,这时候就可以设置传播属性为NOT_SUPPORT
@Service
public class LifeLogServiceImpl implements LifeLogService{
@Transactional(propagation = Propagation.NOT_SUPPORTED)
public void addLog(LifeLog lifeLog){
//业务代码...
}
//其他方法...
}
redis扩展:我大佬问我一个问题,在事务注解里面加入redis的操作方法,事务里面一些操作数据库业务代码或者操作redis代码出错,会相互有啥影响呢?对此我进行了验证。结论是:
在使用 @Transactional
注解的方法中调用 Redis 的操作,如果方法内发生异常,不会影响 Redis 的操作回滚。@Transactional
注解主要用于控制数据库事务,在方法执行时,如果发生异常,会触发事务回滚,数据库的操作会被撤销。但是,Redis 并不是通过事务来进行数据操作和保证原子性的,所以在方法内发生异常时,Redis 的操作不会被回滚。
如果在发生异常时也能回滚 Redis 操作,可以使用 Redis 的事务功能。Redis 通过 MULTI
、EXEC
、DISCARD
和 WATCH
等命令来支持事务操作。在方法内使用 RedisTemplate 的 multi()
方法开启一个事务,然后在事务中执行 Redis 的命令,最后调用 exec()
提交事务,或者调用 discard()
放弃事务。这样可以保证 Redis 的操作和数据库操作在一个事务中,发生异常时可以一起回滚。
ok,上代码
@Override
@Transactional
public String testRedis() {
User user = new User();
user.setId(2);
user.setUsername("小B");
user.setPassword("123456");
user.setRole(1);
user.setPermission("0001");
userMapper.insertSelective(user);
//redis开启事务
//redisTemplate.setEnableTransactionSupport(true);
Object name ="dession";
redisTemplate.opsForValue().set("name",name);
//设置过期时间1天过期
redisTemplate.expire("name",1, TimeUnit.DAYS);
int i = 1/0;
return "BBB";
}
在上述代码中,发生异常后,User数据库会回滚,但是redis仍然会记录数据。方法里面错误不会影响redis。其次如果redis操作失败,比如redis异常,会导致User无法插入数据库。
但是如redis开始了事务,
redisTemplate.setEnableTransactionSupport(true);
那么结果就不一样了,发生异常后User数据库回滚,redis也会回滚的,这行代码体现的是redis对事务的一个支持。如果说testRedis()这个方法没有加@Transaction注解,那么这行代码也是白写。
最后,附上一个源码参考文章链接:Spring源码剖析-事务源码之@Transactionl解析_怎样查看 transactional 标签的源码-CSDN博客