咱们天天都在经过Spring和ORM框架简化咱们的开发工做,框架简化了咱们对事务处理的开发难度,我在工做中发现不少开发人员只会机械式的添加@Transactional注解,也无论是否真的执行了事务,以及事务的隔离级别,不一样异常处理对事务的影响(固然出现这种状况项目管理方面也是存在问题的)。我构思良久将本身的经验写成这篇博文,但愿与你们共勉。java
什么是事务和事务的特性等基础问题就不在此赘述,若是你还不知道那你应该反思了 面试
要搞清楚这个问题咱们,先看下什么是隔离 "隔离,指断绝接触;断绝往来。",这个含义很明显就是要断绝一个事务和外界的往来。在来看一下隔离级别,解决的业务场景——隔离级别解决的业务场景是并发和多线程。综上所述,事务的隔离级别就是咱们多个线程或者并发开启事务操做的时候,数据库要进行隔离操做,保证数据的准确性,它解决了数据脏读、不可重复读、幻读等问题。经过一个表解释这几种错误的含义。数据库
术语 | 含义 |
---|---|
脏读 | A事务读取到了B事务还未提交的数据,若是B未提交的事务回滚了,那么A事务读取的数据就是无效的,这就是数据脏读 |
不可重复读 | 在同一个事务中,屡次读取同一数据返回的结果不一致,这是因为读取事务在进行操做的过程当中,若是出现更新事务,它必须等待更新事务执行成功提交完成后才能继续读取数据,这就致使读取事务在先后读取的数据不一致的情况出现 |
幻读 | A事务读取了几行记录后,B事务插入了新数据,而且提交了插入操做,在后续操做中A事务就会多出几行本来不存在的数据,就像A事务出现幻觉,这就是幻读 |
隔离级别 | 含义 | 未解决的问题 | 解决的问题 |
---|---|---|---|
Read uncommitted | 容许一个事务读取另外一个事务未提交的数据 | 脏读 | 无 |
Read committed | 容许并发事务在已提交后读取(就是在并事务中一个事务要等待另外一个事务提交后才能读取) | 不可重复读、幻读 | 脏读 |
Repeatable read | 可重复读,对相同数据屡次读取是一致的,除非数据被这个事务自己修改,也就是说在读取事务开启后,不容许在提交事务,必须等读取事务结束 | 幻读 | 不可重复读 |
Serializable | 串行化事务,它是最高的隔离级别,可是也是全部隔离级别中最慢的 | 无 | 脏读、不可重复读、幻读 |
便于理解我写一个例子, 使用Spring Boot + Mybatis,表结构以下所示: 编程
/** * 模拟查询用户帐户信息 */ @Transactional(rollbackFor = Exception.class, isolation = Isolation.READ_UNCOMMITTED) public void findOne() throws InterruptedException { Thread.sleep(1000); Account account = accountMapper.findOne(1); System.out.println(account); } /** * 模拟更新用户余额 */ @Transactional(rollbackFor = Exception.class) public void updateAccount() throws InterruptedException { int modifyNum = accountMapper.updateAccount(1); Thread.sleep(1000); int i = 1 / 0; } 复制代码
<select id = "findOne" parameterType = "java.lang.Integer" resultMap = "baseColum">
select id, name, money from account where id = #{id}
</select>
<update id = "updateAccount">
update account set money = money - 100 where id = #{id}
</update>
复制代码
@Test public void readUnCommitTest() { CountDownLatch downLatch = new CountDownLatch(2); new Thread(() -> { try { accountService.updateAccount(); } catch (Exception e) { e.printStackTrace(); } downLatch.countDown(); }).start(); new Thread(() -> { try { accountService.findOne(); } catch (Exception e) { e.printStackTrace(); } downLatch.countDown(); }).start(); downLatch.await(6, TimeUnit.SECONDS); } 复制代码
这里我定义了两个方法,分别是一个查询方法和一个更新方法,同时建立了两个线程同时执行测试方法,经过定义不一样的隔离级别咱们分别来看一下,事务的执行状况。这里数据库Money的初始值均设置为1000。缓存
咱们将查询方法的隔离级别定义为Read uncommitted,模拟更新过程当中出现异常的状况,执行测试方法,控制台输出 Account{id=1, name='张三', money=900.0}, **查询方法查询到了更新方法还未提交的数据,可是更新方法遇到异常后执行了回滚操做,实际数据库数据并为发生改变,数据库发生了脏读。**若是这种状况发生在生产环境中,你的老板必定会砍死你的。 markdown
咱们首先更改一下查询方法的隔离级别(代码就不贴了),一样的模拟更新出现异常,执行测试方法,控制台输出 Account{id=1, name='张三', money=1000.0},虽然查询方法出现了异常回滚了数据,可是咱们查询方法查询的数据依旧是准确的,解决了脏读问题。多线程
咱们首先看看可重复读的效果,直接经过语句模拟: 架构
让咱们在看看设置隔离级别为,不可重复读取以后的效果:并发
和以前同样,搞清楚一个问题时咱们先要搞清楚它到底表明着什么,它的应用场景是什么,这样对于咱们理解是很是有好处的,那么到底啥是事务的传播行为了,传播确定是要两我的在一块儿才能够,一我的也无法传播呀(开车了)。事务的传播确定也是须要在两个或以上事务中进行的,因此事务的传播行为定义为在一个事务中调用另外一个事务,或者事务之间相互调用,事务如何传播即事务如何传递,它继续使用这个事务,仍是新建一个事务?,至于场景显而易见,一个事务方法被其余事务方法调用时使用。app
类型 | 解释 |
---|---|
PROPAGATION_REQUIRED | 支持当前事务,若是不存在则建立一个事务,这也是Spring 默认的传播行为 |
PROPAGATION_SUPPORTS | 支持当前事务,若是当前事务不存在,就不使用事务 |
PROPAGATION_MANDATORY | 支持当前事务,若是事务不存在则抛出异常 |
PROPAGATION_REQUIRES_NEW | 若是当前事务存在,则挂起当前事务,新建一个事务(会造成两个独立的事务,互不干涉 |
PROPAGATION_NOT_SUPPORTED | 以非事务方法运行,若是事务存在,则挂起事务,须要 JtaTransactionManager 的支持 |
PROPAGATION_NEVER | 以非事务的方式运行,若是有事务存在则抛出异常 |
PROPAGATION_NESTED | 若是当前事务存在则嵌套事务存在 |
咱们经过代码演示一下这种传播行为,修改以前的代码以下:
/** * 新增帐户信息 */ @Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRED) public void addAccount() { Account account = new Account(); account.setId(4); account.setName("赵六"); account.setMoney(1000); int i = accountMapper.save(account); updateAccount(); } /** * 模拟更新用户余额 */ @Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRED) public void updateAccount() { int modifyNum = accountMapper.updateAccount(1); System.out.println("update success"); //int i = 1 / 0; } 复制代码
xml:
<insert id = "save" parameterType="com.xiaoxiao.entity.Account"> insert into account(id, name, money) values(#{account.id}, #{account.name}, #{account.money}) </insert> 复制代码
单独执行 updateAccount 方法时会建立一个新的事务方法继续执行,当执行 addAccount 方法是建立了一个新的事务执行到更新方法时,就会加入这个事务,同时能够看到我在后面加了个by zero 的异常,若是更新方法发生异常,会致使两个方法同时回滚,由于他们自己就在同一个事务中。
将 updateAccount 传播行为更改成PROPAGATION_SUPPORTS,addAccount 则不变,当单独执行更新方法时它老是以非事务的方式执行,即便遇到错误也并未回滚,这是由于它是以非事务方式执行的,当咱们调用插入方法它则会加入到插入方法的事务中执行,这时遇到错误都会执行回滚操做。
将更新方法的传播行为修改成 PROPAGATION_MANDATORY, 当单独执行更新方法时,程序抛出 IllegalTransactionStateException 异常,当执行插入方法时更新方法加入到插入方法的事务中继续执行,遇到错误同时回滚。
这种传播行为须要 JtaTransactionManager 事务管理器,咱们修改一下代码
/** * 新增帐户信息 */ @Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRED) public void addAccount() { Account account = new Account(); account.setId(4); account.setName("赵六"); account.setMoney(1000); int i = accountMapper.save(account); updateAccount(); int num = 1 / 0; } /** * 模拟更新用户余额 */ @Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRES_NEW) public void updateAccount() { int modifyNum = accountMapper.updateAccount(1); System.out.println("update success"); } 复制代码
这里咱们在插入方法中人为的制造了一个异常,单独执行更新方法的时候它会新建立一个事务,执行插入方法的时候由于,这里已经存在一个事务了,因此在执行更新方法的时候它会将外层事务挂起,直到执行完毕,若是在执行更新方法结束后遇到异常,更新方法仍是会提交,外层事务则会回滚。
将更新方法的传播行为修改成 PROPAGATION_NESTED, 单独执行更新方法的时候由于上下文中并无事务存在,它会按照 PROPAGATION_REQUIRED 行为进行执行,若是调用插入方法的时候,它则会嵌套在插入事务中执行,若是插入方法后续执行过程当中出现异常,则插入更新方法也会回滚,这和 PROPAGATION_REQUIRES_NEW 行为正好相反。若是更新方法出现异常,它并不会回滚外层方法。
编程式事务和声明式事务在面试中常常会被提起,其实区分二者很简单,编程式事务就是须要咱们对事务进行手动管理,咱们须要手动编写事务处理代码的均可以被称为编程事务。 由于事务管理的代码多有重复,为了将其复用,利用了AOP的思想,经过对须要事务管理的方法进行先后拦截,来进行事务管理的,同时框架甚至帮助咱们简化了这部分代码,咱们只须要手动加注解的,这类都是声明式事务。 立刻咱们就能够知道,编程事务比较复杂,容易致使重复代码,代码侵入大,可是相对于声明式事务更为灵活,可定制性高。声明式事务简单,代码重复少,耦合低,可是灵活性较差。
虽然声明简化了咱们工做,可是你永远不知道,提需求的同窗的需求有多么惨绝人寰,我在实际工做中遇到过一个方法中部分须要事务管理,部分不须要事务管理,部分出错须要回滚,部分不让回滚的需求。
在进行事务编程过程当中,不一样的异常处理也会致使事务最终结果差别,下面我就具体讨论一下不一样异常处理对事务的影响。 若是你读过《阿里巴巴Java开发规约》,或者使用过规约插件的话,必定会发现规约中有一条以下:
最后插件还贴心的帮你准备了三个正例,为何阿里巴巴要求你这写,咱们首先看个例子:
/** * 反例1 */ @Transactional public void updateAccount { try { int modifyNum = accountMapper.updateAccount(1); int num = 1 / 0; } catch (Exception e) { e.printStackTrace(); } } 复制代码
有不少人处理异常的时候喜欢大而全,使用一个try块包含整个代码块,包括我刚开始工做的时候也是这样的,这样处理异常实际上是很是LOW的,并且它还会带来隐患,观察上面所述代码,你认为事务在碰到异常时真的会回滚吗,答案是是否认的,应为声明式事务是基于AOP来帮咱们处理异常的,你却在这里处理了异常,对于AOP来讲它是感知不到异常的,因此事务并不会回滚。
/** * 反例2 */ @Transactional public void updateAccount() throws FileNotFoundException { int modifyNum = accountMapper.updateAccount(1); //个人电脑并无这个文件 InputStream inputStream = new FileInputStream(new File("G://A.txt")); } 复制代码
前面提到了《阿里巴巴Java开发规约》要求咱们必须指定异常类型,为何要制定异常类型,由于**Spring框架的事务基础架构代码将默认只在抛出运行时和unchecked exceptions时才标识事务回滚。**这里首先咱们须要明白异常的分类:
**Java中的异常分为错误和异常,其中错误是咱们没法解决和处理的好比OOM,可是异常是咱们能够进行处理的,其中Excetion 又分为运行时异常和分运行时异常,RuntimeException 及其子类都属于运行时异常,其余则相反。异常又分检查异常和非检查异常,其中Exception中除RuntimeException都属于可查异常,不可查异常RuntimeException及其子类和Error都属于不可查异常。很好理解基本上咱们在平常工做中强制咱们捕获的都属于检查异常如 IOException,FileNotFoundException。**若是你对异常处理感兴趣,我接下来会写一篇关于异常处理的博文,能够继续关注哦。回到事务,上例的事务会成功回滚吗,不会由于Spring默认只回滚运行时异常和非检查异常,即便咱们抛出了FileNotFoundException,也不会回滚,由于他是检查异常,稍微修改一下代码。
/** * 正例 */ @Transactional(rollbackFor = Exception.class) public void updateAccount() throws FileNotFoundException { int modifyNum = accountMapper.updateAccount(1); //个人电脑并无这个文件 InputStream inputStream = new FileInputStream(new File("G://A.txt")); } 复制代码
或
@Transactional(rollbackFor = Exception.class) public void updateAccount() { try { int modifyNum = accountMapper.updateAccount(1); InputStream inputStream = new FileInputStream(new File("G://A.txt")); } catch (FileNotFoundException e) { throw new RuntimeException("文件不存在", e); } } 复制代码
让检查异常回滚:在整个方法前加上 @Transactional(rollbackFor=Exception.class)
让非检查异常不回滚: @Transactional(notRollbackFor=RunTimeException.class)
在嵌套方法中异常处理尤其重要,举个例子:
/** * 新增帐户信息 */ @Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRED) public void addAccount() { Account account = new Account(); account.setId(4); account.setName("赵六"); account.setMoney(1000); int i = accountMapper.save(account); updateAccount(); } /** * 模拟更新用户余额 */ @Transactional(rollbackFor = Exception.class) public void updateAccount() { try { int modifyNum = accountMapper.updateAccount(1); int num = 1 / 0; } catch (Exception e) { e.printStackTrace(); } } 复制代码
若是像上面这样,更新方法出现异常,添加方法并不会回滚,咱们是使用了 REQUIRED 隔离级别,显而易见咱们是想它们在同一个事务中的,当时异常捕获不当,就没法达到这种效果,这样的例子还有不少,在对事物作异常处理的时候必定要谨慎,多测试。对大段代码进行 try-catch,这是不负责任的表现。catch 时请分清稳定代码和非稳定代码,稳定代码指的是不管如何不会出错的代码。对于非稳定代码的 catch 尽量进行区分异常类型,再作对应的异常处理。阿里规范告诉咱们Transactional注解事务不要滥用。事务会影响数据库的QPS,另外使用事务的地方须要考虑各方面的回滚方案,包括缓存回滚、搜索引擎回滚、消息补偿、统计修正等。
事务处理在开发中绝对是重中之重,特别是对于金融银行类项目,若是出了问题确定是要卷铺盖走人的。若是你真正作跟钱相关的项目你必定会理解我说的。必定要带着敬畏的心情组织你的代码,同时必定要多测试、测试、测试,多思考多从开发过程当中总结经验。
很是感谢你的阅读,若是你以为不错的话就点个赞吧,若是你发现错误也能够在评论区给出批评。