基于Spring中的事务@Transactional细节与易错点、幻读

目录
  • 为什么要使用事务?
  • 如何使用事务?
  • 事务的传播带来的几种结果
    • 两个特例
  • 事务传播属性propagation
  • 数据库隔离级别
    • 1、未提交读(会有脏读的现象)
    • 2、已提交读
    • 3、可重复读 (有可能覆盖掉其他事务的操作)
    • 4、串行化(没有并发操作)
    • Spring事务隔离级别比数据库事务隔离级别多一个default

ACID,事务内的一组操作具有 原子性 、一致性、隔离性、持久性

  • Atomicity(原子性):一个事务(transaction)中的所有操作,要么全部完成,要么全部不完成,不会结束在中间某个环节。事务在执行过程中发生错误,会被恢复(Rollback)到事务开始前的状态,就像这个事务从来没有执行过一样。
  • Consistency(一致性):在事务开始之前和事务结束以后,数据库的完整性没有被破坏。这表示写入的资料必须完全符合所有的预设规则,这包含资料的精确度、串联性以及后续数据库可以自发性地完成预定的工作。
  • Isolation(隔离性):数据库允许多个并发事务同时对其数据进行读写和修改的能力,隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致。事务隔离分为不同级别,包括读未提交(Read uncommitted)、读提交(read committed)、可重复读(repeatable read)和串行化(Serializable)。
  • Durability(持久性):事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失。

为什么要使用事务?

就是一组操作中,存在着多个更新修改操作,并且要满足事务的相关要求,所以就需要使用到事务。最常见的例子就是银行两个账户间的转账,包含的A扣款 、B到账等多个操作,这些个操作需要具备事务的特性。比如说,要么A成功扣款,B也成功到账;不能出现A扣款了,B没到账(原子性);也不能出现现在AB都处理成功了,后续又出现A账户的钱又增多了(持久性);也不能出现A账号初始余额充足,两个并发处理,导致出现余额为负的情况(隔离性)。

如何使用事务?

在spring中可以使用声明性的注解事务,即在有需要使用的方法、类上,用@Transactional

修饰即可。修饰的方法、类就是这个事务的包裹区域。出现了对应的异常就会在AOP中触发回滚。

默认的回滚是错误与运行异常,不包括检验异常。

rollbackFor参数支持用户自行设置,例如可定义异常跟运行异常,如下所示;也支持自定义异常类

    @Transactional(rollbackFor = { Exception.class, RuntimeException.class })

默认的事务传播机制是Propagation.REQUIRED

事务的传播本质确定好事务的限制区域,即哪些代码是受到事务保护的,出现异常可以回滚。

细节点:

  • 代码出现事务配置的异常,在事务内的会自动回滚;如果在对应的方法体内使用了try catch捕获异常,异常没有抛出去,那就不会回滚,需要手动回滚了。在catch语句中增加手动回滚的TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();语句
  • public 方法的事务才生效

事务的传播带来的几种结果

  • 外层没有事务的话,内在的子方法,没有的就没有;有事务的就会有事务,有事务的效果形同Propagation.REQUIRES_NEW,互相独立,每个都是一个不同的新事务
  • 外层有事务的话,那这个整个方法都在一个事务的区域范围内,内外任何一处回滚,都是整个回滚。但是Propagation.NESTED修饰的内部方法,可以单独回滚掉自己这个内部方法,作为一个嵌入子事务所具有的独特性。
  • 外层有还是没有事务,Propagation.REQUIRES_NEW修饰的方法都是作为一个独立的事务,自己独立控制回滚与提交,与外层事务无关联。

此处举例的事务,指的是 默认值为 Propagation.REQUIRED的传播行为,以及Propagation.NESTED的传播行为。

两个特例

  • 同一个类中有A、B两个方法,A调用B方法。A没事务,B有事务,B有异常时,回滚失败
A没事务 A有事务
B没事务 没有事务的效果 不分析 事务生效,B的异常,可以让整个A回滚
B有事务 事务失效 事务生效,同上 不分析

若是A/B在同一个类中,A方法有事务,B方法没有事务,这个时候事务会生效,原因是异常传导到了A方法中;

A方法没事务,B方法有事务,A调用B方法。若是A/B在同一个类中,B方法事务失效。A/B在不同的类,B方法有事务效果。

原因分析:这是动态代理导致的,当要执行B方法的回滚时,此时A调用的B方法,不是动态代理的那个类,无法进行回滚。

  • A方法循环调用B方法,A方法有事务,B方法启用新事务,B方法处理成功一条提交一条的数据;B方法遇到异常,有异常的那条回滚,不影响之前处理成功提交的数据。

从之前的推断来看,Propagation.REQUIRES_NEW修饰的内部方法独立一个新事务,跟外层没有关系,其实是两个事务了,外层事务回滚内存的也不会回滚;内层回滚也不影响外层事务。

但是实际结果还是有点不太一样,若是A/B在不同类中,可以达到这个效果;同一个类的话,就会回滚失败。跟上面AB方法调用的结果类似。

究其原因,还是由于使用了动态代理来进行事务AOP的,此时的B方法一旦触发回滚就是事务回滚异常了。那么要想一个类中两个方法间调用达到部分提交的效果,需要使用ApplicationContext 上下文对象获取当前类对象,再进行调用;

// 使用 ApplicationContext 上下文对象获取该对象;
@Autowired
private ApplicationContext applicationContext;
CurrentClass classService = applicationContext.getBean(CurrentClass.class);
//再用这个对象去调用同类的其他方法
classService.b();

总结: 事务的实现依赖于动态代理,因此在同一个类中使用了类的其他方法时,就需要额外注意了,只有使用动态代理的对象去调用方法时,才会有事务回滚的操作。

事务传播属性propagation

propagation 代表事务的传播行为,默认值为 Propagation.REQUIRED,总共的属性信息如下:

  • Propagation.REQUIRED:如果当前存在事务,则加入该事务,如果当前不存在事务,则创建一个新的事务。(默认传播行为,一定会有一个事务)

( 也就是说如果A方法和B方法都添加了注解,在默认传播模式下,A方法内部调用B方法,会把两个方法的事务合并为一个事务 )

  • Propagation.SUPPORTS:如果当前存在事务,则加入该事务;如果当前不存在事务,则以非事务的方式继续运行。(以当前是否有事务为标准,可以有事务,也可以没有事务)
  • Propagation.MANDATORY:如果当前存在事务,则加入该事务;如果当前不存在事务,则抛出异常。(要求当前有事务,就能运行;没有就会异常)
  • Propagation.REQUIRES_NEW:重新创建一个新的事务,如果当前存在事务,暂停当前的事务。

( 当类A中的 a 方法用默认Propagation.REQUIRED模式,类B中的 b方法加上采用 Propagation.REQUIRES_NEW模式,然后在 a 方法中调用 b方法操作数据库,然而 a方法抛出异常后,b方法并没有进行回滚,因为Propagation.REQUIRES_NEW会暂停 a方法的事务 )

  • Propagation.NOT_SUPPORTED:以非事务的方式运行,如果当前存在事务,暂停当前的事务。(以非事务的方式运行,当前有不报错)
  • Propagation.NEVER:以非事务的方式运行,如果当前存在事务,则抛出异常。
  • Propagation.NESTED :如果当前存在事务,则嵌套事务内执行,如果不存在事务和 Propagation.REQUIRED 效果一样。

( 也就是说如果A方法和B方法都添加了注解,在A默认传播模式下,B方法加上采用 Propagation.NESTED模式,A方法内部调用B方法,A回滚,B也会回滚;但B回滚,A不会回滚

数据库隔离级别

事务的隔离级别依赖于数据库的隔离级别,mysql的默认隔离级别是可重复读(repeatable read),对应的效果是在一个事务内重复读取一个表中的数据,一直会是一样的,并不会读取到那些个此事务范围内其他事务 未提交(脏读)、已提交(不可重复读)的修改记录。

在对数据进行测试隔离级别时,需要先对数据库进行一系列的设置,包括关闭自动提交、查看当前的隔离级别,相关命令如下所示:

//由于变量autocommit分会话系统变量与全局系统变量, Value的值为ON,表示autocommit开启。
OFF表示autocommit关闭。
show session variables like 'autocommit';
show global variables like 'autocommit';

//关闭当前会话的自动提交
set session autocommit=0;

//开启一个事务
start transaction;
begin;   

//回滚
rollback;
//提交事务
commot; 
//查看当前的隔离级别   查看全局、当前会话的隔离级别
select @@tx_isolation;
SELECT @@global.tx_isolation, @@session.tx_isolation;

//设置当前会话的隔离级别为read uncommitted级别:
set session transaction isolation level read uncommitted;

//设置当前会话的隔离级别为read committed级别:
set session transaction isolation level read committed;

//设置当前会话的隔离级别为repeatable read级别:
set session transaction isolation level repeatable read;

//设置当前会话的隔离级别为serializable级别:
set session transaction isolation level serializable;

//展示连接id
select connection_id();

//数据库超时设置查询
show session variables like '%timeout';

事务的隔离级别总共分为:未提交读(read uncommitted)、已提交读(read committed)、可重复读(repeatable read)、串行化(serializable)。

下面将对这四种一一展开说明:

1、未提交读(会有脏读的现象)

A事务已执行,但未提交;B事务查询到A事务的更新后数据;A事务回滚;那么之前读取到的A事务为提交的数据就是脏数据了。最低的隔离级别,很少会使用到。

---脏读,读取到了未提交的数据(新增、修改和删除); 除此之外,还有会不可重复读、幻读的现象。

事务1设置如上所示,隔离级别为读未提交,关闭了自动提交。此时开始一个事务,看到的有两条数据;

再打开一个窗口,关闭自动提交,然后进行新增改的操作

最后的结果如上所示,事务1读取到了另一事务未提交时的新增、修改跟删除的数据。

2、已提交读

(会有不能重复读的现象,因为每次读取都是读最新的,那就可能前后两次会有差异了)

会读取这一段时间内其他事务对这些数据的变更操作,A事务执行更新;B事务查询;A事务又执行更新;B事务再次查询时,B事务前后两次查询到的数据不一致;例如事务B要更新状态,因此先进行一次查询,此时状态为1,一系列操作后,马上就要更新了,此时再次查询,第二次查询出来的状态变成了2。

---不可重复读,一般指的是删除、更新、新增;还会有幻读的现象

不可重复读的结果就是事务1能够读取到另外事务的 新增、修改、删除操作,与脏读的区别在于,一个是提交后才能读取到,一个是未提交的实时操作就能读取到。

3、可重复读 (有可能覆盖掉其他事务的操作)

可重复读是mysql数据的默认隔离级别,也是使用的较多的一种隔离级别,下面重点对其分析分析。

A事务无论执行多少次,只要不提交,在这事务内同一个SQL的查询值永远都不变;可以理解成A事务内的所有查询都是 查询A事务开始时那一瞬间的数据快照;

幻读: 由于互相隔离,以及可重复读的特性,另一个事务也同时在处理同一数据的话,就会有一种空幻的现象,好像少了点什么。例如,两个事务都带id去插入同一数据,那么后插入的数据会加锁执行失败(另一事务未提交)或者主键冲突(另一事务已提交),而插入失败后再去查询,又会发现并没有找到重复那条数据的,就会有种读到了空白的感觉,少读取到了内容。 幻读不仅是插入,更新、删除也会有这样的现象的。

现实的一个例子,就是离银行还款日期之前,A去查看账单表,获取到了此次的账单数据,求得了总和,根据账单综合就将账单还清了,并且还再次查询,显示已经还清了。此时,A将本次的查询,还款操作提交到数据库,在开开心心下班前,突然心血来潮再次进行了查询账单操作,突然多了几条消费记录了,需要再次还款。A就感觉 提交事务前的查询有点幻读了,少了几条数据。

事务1在另一个事务提交后,再对同样的数据做修改 删除 新增操作。

---幻读,一般值的是新增;就是明明查询不到这条数据,去新增时会报错。

其实更新、删除也会有的,例如更新同一条用id+status去更新是,后提交的会更新失败,这一特性也可用来加锁,即CAS来更新数据,这样后操作的肯定就不会覆盖前面的数据了。

已经被删除的数据,此时去更新,也不会生效了,在这个事务内再次查询还是删除前的那个数据快照。

如果更新数据时只用id,存在并发修改的情况,那么后提交的必定覆盖之前事务的更新操作。比如本来数据的状态是1,事务2将数据状态有1->2,而事务1看的的状态还是1,事务1直接使用id更新的话,将数据的状态变成了3。事务1以为是1->3 ,其实是由 2->3,中间的状态2直接就被覆盖了。因此高并发的更新,需要慎重。

幻读总结: 很多对幻读的解释是,一个事务在查询的同时,另一个事务插入了数据,然后前一个事务再次查询就会发现多了几条数据,这个现象是不存在的,如果出现了,那说明当前的隔离级别是读已提交了。

可重复读中的就是说同一个事务了多次查询返回的数据肯定是一样的,这是毋庸置疑的,这也是与读已提交的区别。因此,只有在当前事务提交后,再次查询才会刷新到另一事务的改变。

那么我理解的幻读就是,在另一事务新增数据并提交后,此时的事务去新增同样一条数据,会报错的,而此时再去查询又是查无数据,这种现象才是幻读。

更新数据层面就是,事务2已经将数据的状态改变提交了,事务1用旧的状态作为条件去更新,影响行数会是0,这也是一种幻读。 更新已经被删除的数据,也是影响行数为0。

数据库最终的执行还是串行的,只是在前置的一些操作可以并发,最终更新到数据库,只能是有一条成功,由于一些规则的设置,就会出现上述的现象了。

4、串行化(没有并发操作)

串行化是最高的隔离级别,即事务排队串行执行了,没有了并发操作,也不会发生上述所说的脏读、不可重复读、幻读的现象,这个的使用场景不多,理解起来也较为的简单。

总结: 数据库的隔离级别就是一个事务内,对于另一事务的并发操作会有怎么样的效果;

  • 另一事务操作时就能看到修改后的数据,就是读未提交
  • 另一事务操作并提交后 能看到修改后的数据,就是读已提交
  • 另一事务操作提交后,当前事务依旧看不到相应的修改,事务开始什么数据,事务结束也是读取到同样的数据,就是可重复读

所有的事务都排队依次执行了,一次只能有一个进行修改,没有了并行,就是串行化

Spring事务隔离级别比数据库事务隔离级别多一个default

除了上述的四个隔离级别,多出来 DEFAULT (默认)这是一个PlatfromTransactionManager默认的隔离级别,即使用数据库默认的事务隔离级别。另外四个与JDBC的隔离级别相对应,可以显性去指定其隔离级别。

以上为个人经验,希望能给大家一个参考,也希望大家多多支持我们。

(0)

相关推荐

  • 一文搞懂spring boot本地事务@Transactional参数

    目录 1. 本地事务 1.1. 基本概念 1.2. 隔离级别 1.3. 相关命令 1.4. 传播行为 1.4.1. 伪代码练习 1.4.2. 改造商品新增代码 1.4.3. 测试1:同一service + requires_new 1.4.4. 测试2:不同service + requires_new 1.4.5. 在同一个service中使用传播行为 1.5. 回滚策略 1.5.1. 测试编译时异常不回滚 1.5.2. 定制回滚策略 1.6. 超时事务 1.7. 只读事务 1. 本地事务 商品

  • spring声明式事务 @Transactional 不回滚的多种情况以及解决方案

    目录 一. spring 事务原理 问题一.@Transactional 应该加到什么地方,如果加到Controller会回滚吗? 问题二. @Transactional 注解中用不用加rollbackFor = Exception.class 这个属性值 问题三:事务调用嵌套问题具体结果如下代码: 四.总结 五. 参考链接 本文是基于springboot完成测试测试代码地址如下: https://github.com/Dr-Water/springboot-action/tree/master

  • 解析spring事务管理@Transactional为什么要添加rollbackFor=Exception.class

    spring中事务处理原理 利用aop生成代理对象执行带有Transactional事务注解的方法业务逻辑.项目启动过程中会生成代理对象并将Transactional注解中的属性进行解析加载处理.在方法执行过程中如果出现异常,会根据注解配置决定是进入到事务回滚处理还是事务提交处理逻辑中,事务回滚处理逻辑中最终还是基于数据库的事务回滚处理. 异常的分类 案例说明 以自定义异常为例说明一下@Transactional中是否指定rollbackFor=Exception.class的区别     未指

  • 聊聊spring @Transactional 事务无法使用的可能原因

    spring transaction 建议 Spring团队的建议是你在具体的类(或类的方法)上使用 @Transactional 注解, 而不要使用在类所要实现的任何接口上.你当然可以在接口上使用 @Transactional 注解, 但是这将只能当你设置了基于接口的代理时它才生效. 因为注解是不能继承的, 这就意味着如果你正在使用基于类的代理时,那么事务的设置将不能被基于类的代理所识别, 而且对象也将不会被事务代理所包装(将被确认为严重的). 因此请接受Spring团队的建议并且在具体的类上

  • 基于Spring中的事务@Transactional细节与易错点、幻读

    目录 为什么要使用事务? 如何使用事务? 事务的传播带来的几种结果 两个特例 事务传播属性propagation 数据库隔离级别 1.未提交读(会有脏读的现象) 2.已提交读 3.可重复读 (有可能覆盖掉其他事务的操作) 4.串行化(没有并发操作) Spring事务隔离级别比数据库事务隔离级别多一个default ACID,事务内的一组操作具有 原子性 .一致性.隔离性.持久性. Atomicity(原子性):一个事务(transaction)中的所有操作,要么全部完成,要么全部不完成,不会结束

  • spring声明式事务@Transactional底层工作原理

    目录 引言 工作机制简述 事务AOP核心类释义 @Transactional TransactionAttribute SpringTransactionAnnotationParser AnnotationTransactionAttributeSource TransactionAttributeSourcePointcut TransactionInterceptor BeanFactoryTransactionAttributeSourceAdvisor ProxyTransaction

  • Spring中的事务控制知识总结

    一.环境准备 为了演示 Spring 中的事务控制,我们创建一个空项目,项目目录如下: 导入依赖: <dependencies> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>5.0.2.RELEASE</version> </dependency&g

  • spring中12种@Transactional的失效场景(小结)

    目录 一.失效场景集一:代理不生效 二.失效场景集二:框架或底层不支持的功能 三.失效场景集三:错误使用@Transactional 四.总结 数据库事务是后端开发中不可缺少的一块知识点.Spring为了更好的支撑我们进行数据库操作,在框架中支持了两种事务管理的方式: 编程式事务声明式事务 日常我们进行业务开发时,基本上使用的都是声明式事务,即为使用@Transactional注解的方式. 常规使用时,Spring能帮我们很好的实现数据库的ACID (这里需要注意哦,Spring只是进行了编程上

  • Spring中的事务管理实例详解

    本文实例讲述了Spring中的事务管理.分享给大家供大家参考.具体分析如下: 事务简介: 事务管理是企业级应用程序开发中必不可少的技术,用来确保数据的完整性和一致性 事务就是一系列的动作,它们被当作一个单独的工作单元.这些动作要么全部完成,要么全部不起作用 事务的四个关键属性(ACID) ① 原子性(atomicity):事务室一个原子操作,有一系列动作组成.事务的原子性确保动作要么全部完成,要么完全不起作用 ② 一致性(consistency):一旦所有事务动作完成,事务就被提交.数据和资源就

  • 基于Spring中各个jar包的作用及依赖(详解)

    先附spring各版本jar包下载链接http://repo.spring.io/release/org/springframework/spring/ spring.jar 是包含有完整发布模块的单个jar 包.但是不包括mock.jar, aspects.jar, spring-portlet.jar, and spring-hibernate2.jar 示例图片为Spring-2.5.6.jar的包目录 下面讲解各个jar包的作用: 1.org.springframework.aop或sp

  • 简单了解Spring中的事务控制

    1.事务的概念 事务是一组操作的执行单元,相对于数据库的单条操作而言,事务管理的是一组SQL指令,如增删改查等,事务的特性体现在事务内包含的SQL指令必须全部执行成功,如果其中一条指令发生错误,那么整个事务内的一组操作都要进行回滚. 事务有四个特性: 原子性 Atomic ,事务是一个不可再拆分的最小单位,要么整个执行,要么整个回滚. 一致性 Consistent,事务要保证数据库整体数据的完整性和业务的数据的一致性,事务成功提交整体数据修改,事务错误则回滚到数据回到原来的状态. 隔离性 Iso

  • Spring中的事务操作、注解及XML配置详解

    事务 事务全称叫数据库事务,是数据库并发控制时的基本单位,它是一个操作集合,这些操作要么不执行,要么都执行,不可分割.例如我们的转账这个业务,就需要进行数据库事务的处理. 转账中至少会涉及到两条 SQL 语句: update Acoount set balance = balance - money where id = 'A'; update Acoount set balance = balance + money where id = 'B' 上面这两条 SQL 就可以要看成是一个事务,必

  • Spring中的事务管理如何配置

    这篇文章主要介绍了spring中的事务管理如何配置,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 在springboot中,使用事务非常的简单,因为springboot已经帮我们配置好了,只需要加上注解@Transactional即可 在spring中我们需要做一些配置:主要有三点: @Transactional:在相应的方法上加上这个注解 @EnableTransactionManagement:在配置类中加上,开启事务管理 需要在配置类中加

随机推荐