什么是事务?事务就是把一系列的动作当成一个独立的工作单元,这些动作要么全部完成,要么全部不起作用,关乎数据准确性的地方我们一定要用到事务,防止业务逻辑出错。
什么是事务管理,事务管理对于企业应用而言至关重要。它保证了用户的每一次操作都是可靠的,即便出现了异常的访问情况,也不至于破坏后台数据的完整性。就像银行的自助取款机,通常都能正常为客户服务,但是也难免遇到操作过程中机器突然出故障的情况,此时,事务就必须确保出故障前对账户的操作不生效,就像用户刚才完全没有使用过取款机一样,以保证用户和银行的利益都不受损失
关于事务及事务管理其实我们在MySQL以及Redis部分学习的时候已经深入了解过了,在我的这篇博客:【MySQL数据库原理 七】MySQL数据库事务及锁机制里详细介绍了MySql的事务实现方式,并且在【Java Web编程 十三】深入理解JDBC规范中提及了JDBC是如何使用事务的,在【MyBatis学习笔记 二】MyBatis基本操作CRUD及配置解析中提及了MyBatis事务提交的方法,在另一篇博客:【Redis核心知识 三】Redis的事务机制里详细介绍了Redis的事务实现机制,今天这篇Blog从Spring的角度出发和实现方式去讨论下Spring是如何进行事务管理的,又是通过什么机制来实现事务管理的。
事务特性
在学习MySql中就学习过事务的四大特性,也可以理解为事务的四大原则,也就是我们常说的ACID:
- 原子性(Atomicity),事务是最⼩的执⾏单位,不允许分割。事务包含的所有操作要么全部成功,要么全部失败回滚,因此事务的操作如果成功就必须要完全应用到数据库,如果操作失败则不能对数据库有任何影响。
- 一致性(Consistency),是指事务必须使数据库从一个一致性状态变换到另一个一致性状态,也就是说一个事务执行之前和执行之后都必须处于一致性状态。拿转账来说,假设用户A和用户B两者的钱加起来一共是5000,那么不管A和B之间如何转账,转几次账,事务结束后两个用户的钱相加起来应该还得是5000,这就是事务的一致性。
- 隔离性(Isolation),是当多个用户并发访问数据库时,比如操作同一张表时,数据库为每一个用户开启的事务,不能被其他事务的操作所干扰,多个并发事务之间要相互隔离,要串行执行。即要达到这么一种效果:对于任意两个并发的事务T1和T2,在事务T1看来,T2要么在T1开始之前就已经结束,要么在T1结束之后才开始,这样每个事务都感觉不到有其他事务在并发地执行。关于事务的隔离性数据库提供了多种隔离级别,稍后会介绍到。
- 持久性(Durability),是指一个事务一旦被提交了,那么对数据库中的数据的改变就是永久性的,即便是在数据库系统遇到故障的情况下也不会丢失提交事务的操作。例如我们在使用JDBC操作数据库时,在提交事务方法后,提示用户事务操作完成,当我们程序执行完成直到看到提示后,就可以认定事务以及正确提交,即使这时候数据库出现了问题,也必须要将我们的事务完全执行完成,否则就会造成我们看到提示事务处理完毕,导致数据库因为故障而没有执行事务的重大错误。
满足这四个基本原则或者特性,我们就说这个事务是有效的。
事务分类
事务的分类方式有好几种,分别安装事务生效范围、实现方式以及编程方式进行划分。
本地事务和全局(分布式)事务
按照事务的生效范围,事务可以分类为:本地事务和全局(分布式)事务
- 本地事务:普通事务,独立一个数据库,能保证在该数据库上操作的ACID;
- 全局(分布式)事务:涉及两个或多个数据库源的事务,即跨越多台同类或异类数据库的事务(由每台数据库的本地事务组成),分布式事务旨在保证这些本地事务的所有操作的ACID,使事务可以跨越多台数据库;
JDBC事务和JTA事务
按照Java事务类型实现方式,事务可以分类为:JDBC事务和JTA事务:
- JDBC事务:即为上面说的数据库事务中的本地事务,通过connection对象控制管理
- JTA事务:指Java事务API(Java Transaction API),是Java EE数据库事务规范,JTA只提供了事务管理接口,由应用程序服务器厂商(如WebSphere Application Server)提供实现,JTA事务比JDBC更强大,支持分布式事务
声明式事务和编程式事务
按照是否通过编程或是否对业务代码有侵入性分为声明式事务和编程式事务
- 编程式事务:通过编程代码在业务逻辑时需要时自行实现,粒度更小,编程式事务是侵入性事务管理,在代码中直接使用底层的
PlatformTransactionManager
、使用TransactionTemplate
。我们需要在代码中调用beginTransaction()、commit()、rollback()
等事务管理相关的方法 - 声明式事务:通过注解或XML配置实现,粒度大一些,但对业务无侵入性
我们通过Spring可以实现全局的JTA类型的声明式事务,这也是我们最常用的一种事务类型组合
Spring事务实现原理
Spring 框架中最重要的事务相关API有三个:TransactionDefinition、PlatformTransactionManager、TransactionStatus
。所谓事务管理,其实就是按照给定的事务规则来执行提交或者回滚操作。
- 给定的事务规则就是用
TransactionDefinition
表示的 - 按照规则执行提交或者回滚操作则是用
PlatformTransactionManager
来表示
TransactionStatus
的作用则是用于表示一个运行着的事务的状态,也就是事务操作的结果或进度。
TransactionDefinition接口功能
TransactionDefinition
接口管理了一个事务具体的定义和全部属性,的主要方法如下
// // Source code recreated from a .class file by IntelliJ IDEA // (powered by FernFlower decompiler) // package org.springframework.transaction; import org.springframework.lang.Nullable; public interface TransactionDefinition { int PROPAGATION_REQUIRED = 0; int PROPAGATION_SUPPORTS = 1; int PROPAGATION_MANDATORY = 2; int PROPAGATION_REQUIRES_NEW = 3; int PROPAGATION_NOT_SUPPORTED = 4; int PROPAGATION_NEVER = 5; int PROPAGATION_NESTED = 6; int ISOLATION_DEFAULT = -1; int ISOLATION_READ_UNCOMMITTED = 1; int ISOLATION_READ_COMMITTED = 2; int ISOLATION_REPEATABLE_READ = 4; int ISOLATION_SERIALIZABLE = 8; int TIMEOUT_DEFAULT = -1; //返回事务的传播行为,由是否有一个活动的事务来决定一个事务调用 default int getPropagationBehavior() { return 0; } //返回事务的隔离级别,事务管理器依据它来控制另外一个事务能够看到本事务内的哪些数据 default int getIsolationLevel() { return -1; } //返回事务必须在多少秒内完毕 default int getTimeout() { return -1; } //事务是否仅仅读,事务管理器可以依据这个返回值进行优化。确保事务是仅仅读的 default boolean isReadOnly() { return false; } @Nullable default String getName() { return null; } static TransactionDefinition withDefaults() { return StaticTransactionDefinition.INSTANCE; } }
1 事务隔离级别
指若干个并发的事务之间的隔离程度,TransactionDefinition接口中定义了5个表示隔离级别的常量
int ISOLATION_DEFAULT = -1; int ISOLATION_READ_UNCOMMITTED = 1; int ISOLATION_READ_COMMITTED = 2; int ISOLATION_REPEATABLE_READ = 4; int ISOLATION_SERIALIZABLE = 8;
各个隔离级别解释如下:
ISOLATION_DEFAULT
:默认值-1,表示使用底层数据库的默认隔离级别,对大部分数据库而言,通常这值就是ISOLATION_READ_COMMITTED
,也就是读已提交ISOLATION_READ_UNCOMMITTED
:读未提交,该隔离级别表示一个事务可以读取另一个事务修改但还没有提交的数据,该级别可能导致脏读、不可重复读和幻读,因此很少使用该隔离级别,比如PostgreSQL实际上并没有此级别ISOLATION_READ_COMMITTED
ISOLATION_READ_COMMITTED
:读已提交,(Oracle默认级别)该隔离级别表示一个事务只能读取另一个事务已经提交的数据,即允许从已经提交的并发事务读取,该级别可以防止脏读,但幻读和不可重复读仍可能会发生;ISOLATION_REPEATABLE_READ
,可重复读,(MySQL默认级别)该隔离级别表示一个事务在整个过程中可以多次重复执行某个查询,并且每次返回的记录都相同,即对相同字段的多次读取的结果是一致的,除非数据被当前事务本事改变。该级别可以防止脏读和不可重复读,但幻读仍可能发生;ISOLATION_SERIALIZABLE
,串行化,(完全服从ACID的隔离级别)所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,该级别可以防止脏读、不可重复读和幻读,但严重影响程序的性能,因为它通常是通过完全锁定当前事务所涉及的数据表来完成的
关于脏读、幻读和不可重复读的一些概念,可以参照我这篇Blog:【MySQL数据库原理 七】MySQL数据库事务及锁机制这里不再赘述。
2 事务传播机制
事务的传播性一般用在事务嵌套的场景,比如一个事务方法里面调用了另外一个事务方法,那么两个方法是各自作为独立的方法提交还是内层的事务合并到外层的事务一起提交,这就需要事务传播机制的配置来确定怎么样执行;在TransactionDefinition接口中定义了以下几个表示传播机制的常量,值为0~6:
int PROPAGATION_REQUIRED = 0; int PROPAGATION_SUPPORTS = 1; int PROPAGATION_MANDATORY = 2; int PROPAGATION_REQUIRES_NEW = 3; int PROPAGATION_NOT_SUPPORTED = 4; int PROPAGATION_NEVER = 5; int PROPAGATION_NESTED = 6;
各个传播性配置解释如下:
PROPAGATION_REQUIRED
:默认值,能满足绝大部分业务需求,如果外层有事务,则当前事务加入到外层事务,一块提交,一块回滚。如果外层没有事务,新建一个事务执行,假设ServiceX#methodX()
都工作在事务环境下(即都被 Spring 事务增强了),假设程序中存在如下的调用链:Service1#method1()->Service2#method2()->Service3#method3(),
那么这 3 个服务类的 3 个方法通过 Spring 的事务传播机制都工作在同一个事务中,也就是我们刚才的几个方法存在调用,所以会被放在一组事务当中!PROPAGATION_SUPPORTS
:如果外层有事务,则加入外层事务;如果外层没有事务,则直接以非事务的方式继续运行。完全依赖外层的事务PROPAGATION_MANDATORY
:如果外层有事务,则加入外层事务,如果外层没有事务,则抛出异常PROPAGATION_REQUIRES_NEW
:该事务传播机制是每次都会新开启一个事务,同时把外层事务挂起,当当前事务执行完毕,恢复外层事务的执行。如果外层没有事务,执行当前新开启的事务即可,挂起事务指的是将当前事务的属性如事务名称,隔离级别等属性保存在一个变量中,同时将当前线程中所有和事务相关的ThreadLocal变量设置为从未开启过线程一样。Spring维护着一个当前线程的事务状态,用来判断当前线程是否在一个事务中以及在一个什么样的事务中,挂起事务后,当前线程的事务状态就好像没有事务PROPAGATION_NOT_SUPPORTED
:该传播机制不支持事务,如果外层存在事务则挂起,执行完当前代码,则恢复外层事务,无论是否异常都不会回滚当前的代码PROPAGATION_NEVER
,该传播机制不支持外层事务,即如果外层有事务就抛出异常PROPAGATION_NESTED
:该传播机制的特点是可以保存状态保存点,当前事务回滚到某一个点,从而避免所有的嵌套事务都回滚,即各自回滚各自的,如果子事务没有把异常吃掉,基本还是会引起全部回滚的,前面的六种事务传播行为是 Spring 从 EJB 中引入的,他们共享相同的概念。而PROPAGATION_NESTED
是 Spring 所特有的。以PROPAGATION_NESTED
启动的事务内嵌于外部事务中(如果存在外部事务的话),此时,内嵌事务并不是一个独立的事务,它依赖于外部事务的存在,只有通过外部的事务提交,才能引起内部事务的提交,嵌套的子事务不能单独提交。如果熟悉 JDBC 中的保存点(SavePoint)的概念,那嵌套事务就很容易理解了,其实嵌套的子事务就是保存点的一个应用,一个事务中可以包括多个保存点,每一个嵌套子事务。另外,外部事务的回滚也会导致嵌套子事务的回滚
传播机制回答了这样一个问题:一个新的事务应该被启动还是被挂起,或者是一个方法是否应该在事务性上下文中运行
3 只读
如果一个事务只对数据库执行读操作,那么该数据库就可能利用事务的只读特性采取优化措施。通过把一个事务声明为只读,可以给数据库一个机会来应用那些它认为合适的优化措施。由于只读的优化措施是在一个事务启动时由后端数据库实施的,因此,只有对于那些具有可能启动一个新事务的传播行为(PROPAGATION_REQUIRES_NEW、PROPAGATION_REQUIRED、 ROPAGATION_NESTED
)的方法来说,将事务声明为只读才有意义,在 TransactionDefinition 中以 boolean 类型来表示该事务是否只读
//事务是否仅仅读,事务管理器可以依据这个返回值进行优化。确保事务是仅仅读的 default boolean isReadOnly() { return false; }
4 事务超时
指一个事务所允许执行的最长时间,如果超过该时间限制但事务还没有完成,则自动回滚事务,在TransactionDefinition
中以int
的值来表示超时时间,其单位是秒;默认设置为底层事务系统的超时值,如果底层数据库事务系统没有设置超时值,那么就是none,没有超时限制
//返回事务必须在多少秒内完毕 default int getTimeout() { return -1; }
5 Spring事务回滚规则
默认配置下,Spring只有在抛出的异常为运行时异常(runtime exception
)时才回滚该事务,也就是抛出的异常为RuntimeException
的子类(Error也会导致事务回滚),而抛出受检查异常(checked exception
)则不会导致事务回滚,除此之外:
- 可以声明在抛出哪些异常时回滚事务,包括
checked
异常 - 可以声明哪些异常抛出时不回滚事务,即使异常是运行时异常
- 可以编程性的通过
setRollbackOnly()
方法来指示一个事务必须回滚,在调用完setRollbackOnly()后你所能执行的唯一操作就是回滚
也就是说比较灵活,我们一般使用默认的方式就可以了
PlatformTransactionManager接口功能
Spring所有的事务管理策略类都继承自org.springframework.transaction.PlatformTransactionManager
接口,用于执行具体的事务操作。PlatformTransactionManager
接口中定义的主要方法如下
package org.springframework.transaction; import org.springframework.lang.Nullable; public interface PlatformTransactionManager extends TransactionManager { //获得当前事务状态 TransactionStatus getTransaction(@Nullable TransactionDefinition var1) throws TransactionException; //提交事务 void commit(TransactionStatus var1) throws TransactionException; //回滚事务 void rollback(TransactionStatus var1) throws TransactionException; }
根据底层所使用的不同的持久化 API 或框架,PlatformTransactionManager
的主要实现类大致如下:
- DataSourceTransactionManager:适用于使用JDBC和iBatis进行数据持久化操作的情况,因为我们的Spring框架整合的是MyBatis,所以我们重点讨论这种情况
- HibernateTransactionManager:适用于使用Hibernate进行数据持久化操作的情况。
- JpaTransactionManager:适用于使用JPA进行数据持久化操作的情况。
TransactionStatus接口功能
TransactionStatus 表示事务状态, TransactionStatus getTransaction(@Nullable TransactionDefinition var1) throws TransactionException;
方法返回一个 TransactionStatus 对象,表示一个事务的状态,部分方法如下:
package org.springframework.transaction; import java.io.Flushable; public interface TransactionStatus extends TransactionExecution, SavepointManager, Flushable { boolean hasSavepoint(); void flush(); }
其实现接口实现的一些判断方法如下:
//判断事务是否只读 boolean isReadOnly() ; //是否是一个新的事务 boolean isNewTransaction(); //判断是否有回滚点 boolean hasSavepoint(); //将一个事务标识为不可提交的。在调用完setRollbackOnly()后只能被回滚 //在大多数情况下,事务管理器会检测到这一点,在它发现事务要提交时会立刻结束事务。 //调用完setRollbackOnly()后,数数据库可以继续执行select,但不允许执行update语句,因为事务只可以进行读取操作,任何修改都不会被提交。 void setRollbackOnly(); boolean isRollbackOnly(); void flush(); //判断事务是否已经完成 boolean isCompleted(); Object createSavepoint() throws TransactionException; void rollbackToSavepoint(Object var1) throws TransactionException; void releaseSavepoint(Object var1) throws TransactionException;