Spring 事务详解

前言:

​ 在了解Spring事务之前我们先得知道什么是事务?

1、事务的基本概念

事务

​ 相信大家都在ATM机取过钱,但是是否有人考虑过它的流程是怎样的呢?

​ 我们都知道,假如我们取300块钱,那么当这三百块钱从ATM机出来时,我们的账户相应的会减少300。这两个过程一定是要同时成功才算成功的。否则就会出现账户少了300.但是钱没出来,对于我们来说,我们损失了300.而如果钱出来了但是账户钱没扣掉的话,银行就会损失300.这两种情况都是不可取的。所以就需要保证要么大家一起成功,有一个失败即表示这个过程失败了,就需要还原现场(钱没出来,就需要把账户扣掉的钱给补上)。这,就是事务。

定义:

​ 事务必须服从ACID原则。ACID指的是原子性(atomicity)、一致性(consistency)、隔离性(isolation)和持久性(durability)。 通俗理解,事务其实就是一系列指令的集合。

事务的特性

  • 原子性(Atomicity):事务是一个原子操作,由一系列动作组成。事务的原子性确保动作要么全部完成,要么完全不起作用。
  • 一致性(Consistency):一旦事务完成(不管成功还是失败),系统必须确保它所建模的业务处于一致的状态,而不会是部分完成部分失败。在现实中的数据不应该被破坏。
  • 隔离性(Isolation):可能有许多事务会同时处理相同的数据,因此每个事务都应该与其他事务隔离开来,防止数据损坏。
  • 持久性(Durability):一旦事务完成,无论发生什么系统错误,它的结果都不应该受到影响,这样就能从任何系统崩溃中恢复过来。通常情况下,事务的结果被写到持久化存储器中。

2、Spring事务的基本原理

​ Spring事务的本质其实就是数据库对事务的支持,没有数据库的事务支持,spring是无法提供事务功能的。对于纯JDBC操作数据库,想要用到事务,可以按照以下步骤进行:

  1. 获取连接 Connection con = DriverManager.getConnection()
  2. 开启事务con.setAutoCommit(true/false);
  3. 执行CRUD
  4. 提交事务/回滚事务 con.commit() / con.rollback();
  5. 关闭连接 conn.close();

​ 使用Spring的事务管理功能后,我们可以不再写步骤 2 和 4 的代码,而是由Spirng 自动完成。那么Spring是如何在我们书写的 CRUD 之前和之后开启事务和关闭事务的呢?解决这个问题,也就可以从整体上理解Spring的事务管理实现原理了。

下面简单地介绍下,注解方式为例子

  1. 配置文件开启注解驱动,在相关的类和方法上通过注解@Transactional标识。
  2. spring 在启动的时候会去解析生成相关的bean,这时候会查看拥有相关注解的类和方法,并且为这些类和方法生成代理,并根据@Transaction的相关参数进行相关配置注入,这样就在代理中为我们把相关的事务处理掉了(开启正常提交事务,异常回滚事务)。
  3. 真正的数据库层的事务提交和回滚是通过binlog或者redo log实现的。

3、Spring事务的配置方式

​ Spring支持编程式事务管理以及声明式事务管理两种方式。

2.1 编程式事务管理

​ 编程式事务管理是侵入性事务管理,使用TransactionTemplate或者直接使用PlatformTransactionManager,对于编程式事务管理,Spring推荐使用TransactionTemplate。

2.2 声明式事务管理

​ 声明式事务管理建立在AOP之上,其本质是对方法前后进行拦截,然后在目标方法开始之前创建或者加入一个事务,执行完目标方法之后根据执行的情况提交或者回滚。
​ 编程式事务每次实现都要单独实现,但业务量大功能复杂时,使用编程式事务无疑是痛苦的,而声明式事务不同,声明式事务属于无侵入式,不会影响业务逻辑的实现,只需要在配置文件中做相关的事务规则声明或者通过注解的方式,便可以将事务规则应用到业务逻辑中。
​ 显然声明式事务管理要优于编程式事务管理,这正是Spring倡导的非侵入式的编程方式。唯一不足的地方就是声明式事务管理的粒度是方法级别,而编程式事务管理是可以到代码块的,但是可以通过提取方法的方式完成声明式事务管理的配置。

4、事务的传播机制

​ 主要就是解决多个事务嵌套时,新事务和旧事务该如何配合。

常用的事务传播机制如下:

常量名称常量解释
PROPAGATION_REQUIREDSpring默认的传播机制,能满足绝大部分业务需求,如果外层有事务,则当前事务加入到外层事务,一块提交,一块回滚。如果外层没有事务,新建一个事务执行
PROPAGATION_REQUES_NEW该事务传播机制是每次都会新开启一个事务,同时把外层事务挂起,当当前事务执行完毕,恢复上层事务的执行。如果外层没有事务,执行当前新开启的事务即可
PROPAGATION_SUPPORT如果外层有事务,则加入外层事务,如果外层没有事务,则直接使用非事务方式执行。完全依赖外层的事务
PROPAGATION_NOT_SUPPORT该传播机制不支持事务,如果外层存在事务则挂起,执行完当前代码,则恢复外层事务,无论是否异常都不会回滚当前的代码
PROPAGATION_NEVER该传播机制不支持外层事务,即如果外层有事务就抛出异常
PROPAGATION_MANDATORY与NEVER相反,如果外层没有事务,则抛出异常
PROPAGATION_NESTED该传播机制的特点是可以保存状态保存点,当前事务回滚到某一个点,从而避免所有的嵌套事务都回滚,即各自回滚各自的,如果子事务没有把异常吃掉,基本还是会引起全部回滚的。

5、事务的隔离机制

​ 在前面我们了解到,Spring事务的本质其实就是数据库对事务的支持。所以我们先了解一下数据库事务的隔离级别。

数据库事务的隔离级别

​ 数据库事务的隔离级别有4个,由低到高依次为Read uncommitted、Read committed、Repeatable read、Serializable,这四个级别可以逐个解决脏读、不可重复读、幻读这几类问题。

四个级别分别是:

  • ISOLATION_READ_UNCOMMITTED:这是事务最低的隔离级别,它充许令外一个事务可以看到这个事务未提交的数据。这种隔离级别会产生脏读,不可重复读和幻像读。
  • 2. ISOLATION_READ_COMMITTED:保证一个事务修改的数据提交后才能被另外一个事务读取。另外一个事务不能读取该事务未提交的数据
  • ISOLATION_REPEATABLE_READ:这种事务隔离级别可以防止脏读,不可重复读。但是可能出现幻像读。它除了保证一个事务不能读取另一个事务未提交的数据外,还保证了避免下面的情况产生(不可重复读)。
  • ISOLATION_SERIALIZABLE:这是花费最高代价但是最可靠的事务隔离级别。事务被处理为顺序执行。
    除了防止脏读,不可重复读外,还避免了幻像读。

对于脏读,不可重复读,幻读可以借助以下内容理解:

  • 脏读:一个事务读取到另一个事务未提交的更新数据。

小明的银行卡余额里有100元。现在他打算用手机点一个外卖饮料,需要付款10元。但是这个时候,他的女朋友看中了一件衣服95元,她正在使用小明的银行卡付款。于是小明在付款的时候,程序后台读取到他的余额只有5块钱了,根本不够10元,所以系统拒绝了他的交易,告诉余额不足。但是小明的女朋友最后因为密码错误,无法进行交易。小明非常郁闷,明明银行卡里还有100元,怎么会余额不足呢?(他女朋友更郁闷。。。)

  • 幻读也叫虚读:一个事务执行两次查询,第二次结果集包含第一次中没有或某些行已经被删除的数据,造成两次结果不一致,只是另一个事务在这两次查询中间插入或删除了数据造成的。

公司财务A在进行员工薪资核算业务,需要对小明的工资进行计算并录入系统,必须查询两次明细信息,然后将后一次的明细信息计算总数出来。财务在第一次明细查询时,基本工资2000元,全勤奖1000元,提成2000元,共计5000元。在进行第二次计算时,财务B突然接到通知,需要把下个月的节日福利也在这个月的工资中发放,每人100元。于是财务B在每个人的工资明细中又加了一条节日福利100元。而此时财务A获得第二次查询小明的工资明细后,发现工资明细变成了4条数据,总数是5100元。两次计算结果相差100元,财务A奇怪这多出来的一条明细100元是哪里来的呢?(都怪财务B不告诉A。。。)

  • 不可重复读:一个事务两次读取同一行的数据,结果得到不同状态的结果,中间正好另一个事务更新了该数据,两次结果相异,不可被信任。

小明在手机上购买起购价为1W元理财产品。系统首先要判断他的余额够不够购买理财产品,如果足够再获取当前的余额,进行申请。系统第一次读取到小明的余额还剩1W元,刚好足够购买产品。但是这个时候刚好他女朋友又看中了一个包包5000元,这次密码终于不会再错误的女朋友毫不犹豫刷了小明的银行卡买下了这个包包。但是这个系统刚好在进行第二次确认,发现小明的余额上只有5000元,根本不够购买。于是系统很生气,拒绝了小明的购买行为,告诉他,你真是个骗子!!!

该故事借鉴与该篇博客,感谢

Spring中的隔离级别

隔离级别含义
ISOLATION_DEFAULT使用后端数据库默认的隔离级别
ISOLATION_READ_UNCOMMITTED允许读取尚未提交的更改。可能导致脏读、幻读或不可重复读。
ISOLATION_READ_COMMITTED(Oracle 默认级别)允许从已经提交的并发事务读取。可防止脏读,但幻读和不可重复读仍可能会发生。
ISOLATION_REPEATABLE_READ(MYSQL默认级别)对相同字段的多次读取的结果是一致的,除非数据被当前事务本身改变。可防止脏读和不可重复读,但幻读仍可能发生。
ISOLATION_SERIALIZABLE完全服从ACID的隔离级别,确保不发生脏读、不可重复读和幻影读。这在所有隔离级别中也是最慢的,因为它通常是通过完全锁定当前事务所涉及的数据表来完成的。

只读

如果一个事务只对数据库执行读操作,那么该数据库就可能利用那个事务的只读特性,采取某些优化措施。通过把一个事务声明为只读,可以给后端数据库一个机会来应用那些它认为合适的优化措施。由于只读的优化措施是在一个事务启动时由后端数据库实施的, 因此,只有对于那些具有可能启动一个新事务的传播行为(PROPAGATION_REQUIRES_NEW、PROPAGATION_REQUIRED、 ROPAGATION_NESTED)的方法来说,将事务声明为只读才有意义。

事务超时

为了使一个应用程序很好地执行,它的事务不能运行太长时间。因此,声明式事务的下一个特性就是它的超时。

假设事务的运行时间变得格外的长,由于事务可能涉及对数据库的锁定,所以长时间运行的事务会不必要地占用数据库资源。这时就可以声明一个事务在特定秒数后自动回滚,不必等它自己结束。

由于超时时钟在一个事务启动的时候开始的,因此,只有对于那些具有可能启动一个新事务的传播行为(PROPAGATION_REQUIRES_NEW、PROPAGATION_REQUIRED、ROPAGATION_NESTED)的方法来说,声明事务超时才有意义。

回滚规则

在默认设置下,事务只在出现运行时异常(runtime exception)时回滚,而在出现受检查异常(checked exception)时不回滚(这一行为和EJB中的回滚行为是一致的)。
不过,可以声明在出现特定受检查异常时像运行时异常一样回滚。同样,也可以声明一个事务在出现特定的异常时不回滚,即使特定的异常是运行时异常。

6、Spring声明式事务配置参考

事物配置中有哪些属性可以配置?以下只是简单的使用参考

  • 事务的传播性:
    @Transactional(propagation=Propagation.REQUIRED)

  • 事务的隔离级别:
    @Transactional(isolation = Isolation.READ_UNCOMMITTED)

注:读取未提交数据(会出现脏读, 不可重复读) 基本不使用

  • 只读:
    @Transactional(readOnly=true)
    该属性用于设置当前事务是否为只读事务,设置为true表示只读,false则表示可读写,默认值为false。

  • 事务的超时性:
    @Transactional(timeout=30)

  • 回滚:
    指定单一异常类:@Transactional(rollbackFor=RuntimeException.class)
    指定多个异常类:@Transactional(rollbackFor={RuntimeException.class, Exception.class})

参考文档:

关于Spring事务,开这一篇就够了

Spring事务管理(一)什么是事务?

深入理解 Spring 事务原理

Q.E.D.