事务的理解
注解 开启事务的方式
启动类 增加 @enableTransactionManagement
在想要开启事务的方法上面增加 @Transactional
事务其实就是针对数据库的操作,也就是在一个数据库当中开启一个事务后,该事务遵从acid的原则、存在四种隔离级别(存在事务的传播性,这个是在框架整合mysql应用当中的需求)。
今天这篇文章主要是针对 原子性 即要么操作全部成功(提交)要么全部失败(回撤)的介绍,通常文章在讲事务的时候,只是根据框架本身来解释,本篇文章结合了,事务在数据库中的表现来讲解。下面以mysql为例。
事务在mysql中的表现
上图是在mysql当中去开启一个事务,begin之后,就开启了一个线程,直到去执行commit;该事务将一直存在。
并且在该事务提交之前,其他事务是没法看到本次事务的所有操作的。因为mysql的隔离级别是可重复读的级别。而如果在该事务提交之前,执行rollback操作,则把之前所有的操作全部取消,如下图:
可以看到rollback之后,重新执行查找操作,之前的修改和插入操作都取消了。
commit 之后所有的操作都执行成功 完成持久化。
事务在spring框架中的体现
当然了,刚才在mysql中开启事务,只是一个实例,在实际的开发过程中,是不会用到的。而是是要在spring整合mysql过程中,结合具体的业务来做事务的开启、回撤、提交,当然了此时也存在传播性的。
以下做具体介绍
网上大部分的过于spring开启事务的实例都是在 serviceImp当中的方法去开启的,但是其实放在哪个方法都可以,所以本篇文章把线程的开启放在controller的方法当中。
这里做一下解释为什么之前要介绍mysql当中如何开启和结束事务,又为什么这里要把开启事务的注解不放在常规的serviceImp上面而是放在controller 的方法上面?
主要是想向广大读者解释,事务的本质到底是什么,本质上一个事务是:
1、应用跟数据库的一个连接,该连接就是给数据库一个指令,就是begin指令,让一个事务在mysql当中开启。
2、无论开始事务的注解指令(@Transactional)放在哪里,该方法下的所有跟数据库交互的sql执行,都是在一个事务当中的,(当然除非有新的传播隔离出现,这里先不讨论这种情况),直到该方法执行完毕,该事务就被提交。
事务demo的解析,结合mysql事务
还是用我的一个简单demo来分析事务,首先我把@Transactional放在了controller的某个方法上,那么根据上面的理论,该方法里面所有的操作都是在一个事务当中的,直到该方法被执行完毕,则自动commit。
先把所有的跟数据库交互找到:
![在这里插入图片描述](https://img-blog.csdnimg.cn/56fab2df015c4b829f30f373ce6e46ac.png忽略所有的查找语句,就盯着增删改语句,会发现一共有三条sql,分别是
update sys_user set username= ‘1234’ where user_id=4;
update sys_user set username= ‘12’ where user_id=2;
update sys_user set username= ‘11111111111111111111111’ where user_id=1;
因为事务说到底是跟数据库的交互,所以这里要在mysql中开启线程来把该方法的sql演绎出来,如下图:
事务当中的sql语句是根据 事务开启的方法的自然顺序执行下来的,可以看到最后一条语句,由于设置的value的长度已经超过该字段的最大长度,所以发生了运行时异常报错。根据事务的原则,一个事务要么全部成功要么全部失败,所以这里报错之后,会给mysql一个rollback操作,随后该事务就直接执行commit,本事务就完全结束了。
至此一个事务在mysql当中的完整执行就结束了,总结一下就是出现异常就会rollback,随后直接commit。没有异常就是正常的 commit。
事务何时需要rollback
那么根据上述实例,在实际的开发当中,我们需要把握rollback的时机,才能保证事务按照我们业务的要求来执行。
1、首先 默认只在遇到运行时异常和Error时才会回滚rollback,当然运行时异常,RuntimeException是一个大类,具体的异常分类如下图:
先回忆下java的异常模型,Throwable是最顶层的父类,有Error和Exception两个子类。
Error表示严重的错误(如OOM等);
Exception可以分为运行时异常(RuntimeException及其子类)和非运行时异常(Exception的子类中,除了RuntimeException及其子类之外的类)。
非运行时异常是检查异常(checked exceptions),一定要try catch,因为这类异常是可预料的,编译阶段就检查的出来;
Error和运行时异常是非检查异常(unchecked exceptions),不需要try catch,因为这类异常是不可预料的,编辑阶段不会检查,没必要检查,也检查不出来。
下面的demo是对 抛出非运行时异常 事务不会回滚的示例:
可以根据理论得出结论,由于捕获到的异常是IOException,所以除了执行失败的最后一条sql之外,其余两条的sql都执行成功,完成了持久化(上面的图给错了,应该是要抛出IOException)。
可以看到,只有在发生RuntimeException 或者其下面的子类的异常的时候才会发生rollback,那如果发生了其他的非运行时异常怎么办呢?解决方法如下图:
在@Transational 增加限制rollbackFor = Exception.class ,此时Exception下面所有的异常,即运行时异常和非运行时异常的抛出都会进行回滚。
事务的传播
刚才我们一直是在一个事务同一个传播级别下执行的三条sql,还是利用刚才的demo,因为抛出了异常,所以会导致整个事务回滚,也就是所有的sql执行都无效。那么如果在实际的业务中,某个sql不依赖其他sql是否执行成功的话,那么就需要设置新的隔离级别了。
我们这时候给在第一条sql执行之前设置 REQUIRES_NEW的隔离级别,也就是无论之前的方法是否存在事务我都开启一个新的事务来执行,如下图:
此时,虽然第三条sql语句的执行抛出异常,但是因为第一条sql语句的执行被分配到了新的事务当中,所以第一条sql执行成功,当然后两条依然会回滚。