spring事务在实际项目开发中的使用
一, 事务的一些基础知识简单回顾一下,讲的不是很深入,网上博客很多。
1,关于事务的四大特性:原子性、隔离性、一致性、持久性 本文不再赘述;
2,事务的隔离级别:读未提交,读已提交,可重复读,串行化(这里应该深入了解各个级别会出现什么问题,比如脏读,不可重复读,幻读)
3,事务的传播行为:事务传播行为指的就是当一个事务方法被另一个事务方法调用时,这个事务方法应该如何进行。 例如:methodA事务方法调用methodB事务方法时,methodB是继续在调用者methodA的事务中运行呢,还是为自己开启一个新事务运行,这就是由methodB的事务传播行为决定的。默认采用:PROPAGATION_REQUIRED
二,接下来我们简单回顾一下java的异常体系:
Throwable 是 Java 语言中所有错误或异常的超类,在 Java 中只有 Throwable 类型的实例才可以被抛出(throw)或者捕获(catch),它是异常处理机制的基本组成类型。
实例分为 Error 和 Exception 两种。
Error 类是指 java 运行时系统的内部错误和资源耗尽错误。应用程序不会抛出该类对象。如果
出现了这样的错误,除了告知用户,剩下的就是尽力使程序安全的终止。
Exception 又有两个分支 , 一个是运行时异常 RuntimeException , 一 个是检查异常 CheckedException。
RuntimeException 如 :NullPointerException 、 ClassCastException ;
CheckedException 如: I/O 错误导致的 IOException、SQLException。
RuntimeException 是那些可能在 Java 虚拟机正常运行期间抛出的异常的超类。 如果出现 RuntimeException,那么一
定是程序员代码书写导致的错误.
CheckedException:一般是外部错误,这种异常都发生在编译阶段,Java 编译器会强
制程序去捕获此类异常,即会出现要求你把这段可能出现异常的程序进行 try catch
三,言归正传,项目中事务的使用
springboot自动配置默认为我们配置好了事务管理器,参考我们另一篇文章https://www.cnblogs.com/enchaolee/p/11364025.html
我们在项目的实际开发中,对于事务的处理,在编码中无外乎使用@Transactional这个注解,以及对于异常的处理,下面我们详细说一下。
1,方法上加入@Transactional注解后,这个方法就成为了事务方法,如果对于注解的一些属性不做特殊配置的话,方法中如果出现了RuntimeException(运行时异常),事务会进行回滚,如果出现了checkedException(编译时异常),则不会回滚。那么对于这类异常,我们想让他在抛出的时候也进行回滚怎么办呢,当我们点进去@Transactional注解后,看下图:
我们可以看到,图中有标红的两个注解中的属性,我们可以在这里进行手动配置,以实现方法抛出具体异常进行回滚的逻辑。例如我们可以这样配置:@Transactional(rollbackFor = Exception.class);
当然还有两个字段可以设置抛出某某异常不进行回滚:noRollbackFor,noRollbackForClassName;
2,对于异常捕获,事务如何处理
如果我们在事务方法中,手动捕获了异常,并没有让事务抛出去,也没有手动指定需要回滚,那么事务方法即使出现异常,也会提交事务。
比如我们这样去做,只是记录了日志,即使使用rollbackfor=Exception.class制定了需要回滚的逻辑,但是事务仍然会提交,除非加上下面这一行:
有这句代码就不需要再手动抛出运行时异常了,但是不建议这样做,因为这样做代码中会多出很多事务回滚的代码,不利于维护,还是交得spring去处理更妥当。3,的
3,我们该怎么样处理异常回滚呢?
如图所示,CommonException是我们定义的全局异常类,我们可以把catch到的异常统一按照一定的格式进行抛出。下面是CommonException相关代码
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 public class CommonException extends RuntimeException { 2 3 protected String errMsg; // 错误提示信息,显示给用户 4 protected String detailMsg; // 错误的具体信息,可能包含一些参数ID等信息 5 protected CommonErrorCode error; // 错误码 6 protected Object data; // 返回内容 7 8 public CommonException(CommonErrorCode error) { 9 super(error.getDesc()); 10 this.error = error; 11 this.errMsg = error.getDesc(); 12 this.detailMsg = error.getDesc(); 13 } 14 15 public CommonException(CommonErrorCode error, String errMsg) { 16 super(errMsg); 17 this.error = error; 18 this.errMsg = errMsg; 19 this.detailMsg = errMsg; 20 } 21 22 public CommonException(CommonErrorCode error, String errMsg, String detailMsg) { 23 super(StringUtils.isEmpty(detailMsg) ? errMsg : detailMsg); 24 this.error = error; 25 this.errMsg = errMsg; 26 this.detailMsg = detailMsg; 27 } 28 29 public CommonException(CommonErrorCode error, String errMsg, Throwable cause) { 30 super(errMsg, cause); 31 this.error = error; 32 this.errMsg = errMsg; 33 this.detailMsg = errMsg; 34 } 35 36 public CommonException(CommonErrorCode error, String errMsg, String detailMsg, Throwable cause) { 37 super(StringUtils.isEmpty(detailMsg) ? errMsg : detailMsg, cause); 38 this.error = error; 39 this.errMsg = errMsg; 40 this.detailMsg = detailMsg; 41 } 42 43 public CommonException(CommonErrorCode error, Throwable cause) { 44 super(error.getDesc(), cause); 45 this.error = error; 46 this.errMsg = error.getDesc(); 47 this.detailMsg = error.getDesc(); 48 } 49 50 public String getErrMsg() { 51 return errMsg; 52 } 53 54 public CommonErrorCode getError() { 55 return error; 56 } 57 58 public String getDetailMsg() { 59 return detailMsg; 60 } 61 62 public void setDetailMsg(String detailMsg) { 63 this.detailMsg = detailMsg; 64 } 65 66 public void setErrMsg(String errMsg) { 67 this.errMsg = errMsg; 68 } 69 70 public Object getData() { 71 return data; 72 } 73 74 public CommonException setData(Object data) { 75 this.data = data; 76 return this; 77 } 78 }
4,事务方法调用事务方法,怎么去处理
首先我们先确认一个前提:事务的传播行为我们使用默认的即:required,并且我们假设有两个事务方法a,b;a调用b。
(1)根据上面我们讲过的required的特性,我们知道spring对于这种事务方法间的调用,会默认把它当做一个事务;我们假设如果b中抛出了NullpointException,并且a,b都没有做异常的处理,那么由a,b组成的整个事务肯定都会进行回滚,这是毋庸置疑的。
(2)如果b出现异常,a catch了异常,并且没有抛出去,就像我们上面例子讲的只是记录了日志,我们会发现这个异常,思考一下为什么呢?
我们知道spring 事务管理,启用事务的方法,调用另一个事务方法时,会进行事务传播。注解@Transactional默认的传播机制是PROPAGATION_REQUIRED。再来回顾一下required特性:表示当前方法必须运行在事务中。如果当前事务存在,方法将会在该事务中运行。否则,会启动一个新的事务
所以是补齐方法运行的时候,同步的事务合并到了补齐的事务里面。当同步发票发生异常后,被try catch 捕获,没有抛出来。但是事务还是会进行回滚,回滚执行到 DataSourceTransactionManager 类的 doSetRollbackOnly 方法时,设置了rollbackOnly = true;
由于异常被catch, 不阻断整个事务执行。整个事务执行完后,执行commit 提交,我们打开这个抽象类AbstractPlatformTransactionManager看一下commit的逻辑
这里我标红了这个方法,我们继续往下看,我们打开DefaultTransactionStatus这个类,可以看到标红方法的具体实现。
在执行commit 提交逻辑时,执行到 DefaultTransactionStatus 类的 isGlobalRollbackOnly方法时,判断rollbackOnly 为true, 则执行回滚,并且打出那句报错的日志”Transaction rolled back because it has been marked as rollback-only”。
注意:想要复现这个问题,a,b要在不同的类中才行,因为事务是基于aop动态代理实现的,同一个类不能复现问题。如果事务方法a调用b方法。然后想让b出异常后,a catch住不影响主流程,其实可以完全把b方法的传播机制设置为requeired_NEW,这样a,b方法就是两个不同的事务,互不影响。
参考:https://blog.csdn.net/qq_38478903/article/details/88929207
5,需要注意的一些点
(1)事务方法需要标记为public的
(2)@Transactional注解只能写在service中,不能再controller中,否则会报404错误
(3)如果在使用事务的情况下,所有操作都是读操作,那建议把事务设置成只读事务,当事务被标识为只读事务时,Spring可以对某些可以针对只读事务进行优化的资源就可以执行相应的优化措施,需要手动设置成true。但是方法再执行增删改回抛异常。