一个数据库事务是一个被视为单一的工作单元的操作序列。这些操作应该要么完整地执行,要么完全不执行。事务管理是一个重要组成部分,RDBMS 面向企业应用程序,以确保数据完整性和一致性。事务的概念可以描述为具有以下四个关键属性说成是 ACID:
-
原子性:事务应该当作一个单独单元的操作,这意味着整个序列操作要么是成功,要么是失败的。
-
一致性:这表示数据库的引用完整性的一致性,表中唯一的主键等。
-
隔离性:可能同时处理很多有相同的数据集的事务,每个事务应该与其他事务隔离,以防止数据损坏。
- 持久性:一个事务一旦完成全部操作后,这个事务的结果必须是永久性的,不能因系统故障而从数据库中删除。
一个真正的 RDBMS 数据库系统将为每个事务保证所有的四个属性。使用 SQL 发布到数据库中的事务的简单视图如下:
-
使用 begin transaction 命令开始事务。
-
使用 SQL 查询语句执行各种删除、更新或插入操作。
- 如果所有的操作都成功,则执行提交操作,否则回滚所有操作。
Spring 框架在不同的底层事务管理 APIs 的顶部提供了一个抽象层。Spring 的事务支持旨在通过添加事务能力到 POJOs 来提供给 EJB 事务一个选择方案。Spring 支持编程式和声明式事务管理。EJBs 需要一个应用程序服务器,但 Spring 事务管理可以在不需要应用程序服务器的情况下实现。
局部事物 vs. 全局事务
局部事务是特定于一个单一的事务资源,如一个 JDBC 连接,而全局事务可以跨多个事务资源事务,如在一个分布式系统中的事务。
局部事务管理在一个集中的计算环境中是有用的,该计算环境中应用程序组件和资源位于一个单位点,而事务管理只涉及到一个运行在一个单一机器中的本地数据管理器。局部事务更容易实现。
全局事务管理需要在分布式计算环境中,所有的资源都分布在多个系统中。在这种情况下事务管理需要同时在局部和全局范围内进行。分布式或全局事务跨多个系统执行,它的执行需要全局事务管理系统和所有相关系统的局部数据管理人员之间的协调。
编程式 vs. 声明式
Spring 支持两种类型的事务管理:
-
编程式事务管理 :这意味着你在编程的帮助下有管理事务。这给了你极大的灵活性,但却很难维护。
- 声明式事务管理 :这意味着你从业务代码中分离事务管理。你仅仅使用注释或 XML 配置来管理事务。
声明式事务管理比编程式事务管理更可取,尽管它不如编程式事务管理灵活,但它允许你通过代码控制事务。但作为一种横切关注点,声明式事务管理可以使用 AOP 方法进行模块化。Spring 支持使用 Spring AOP 框架的声明式事务管理。
Spring 事务抽象
Spring 事务抽象的关键是由 org.springframework.transaction.PlatformTransactionManager 接口定义,如下所示:
public interface PlatformTransactionManager { TransactionStatus getTransaction(TransactionDefinition definition); throws TransactionException; void commit(TransactionStatus status) throws TransactionException; void rollback(TransactionStatus status) throws TransactionException; }
序号 | 方法 & 描述 |
---|---|
1 |
TransactionStatus getTransaction(TransactionDefinition definition) 根据指定的传播行为,该方法返回当前活动事务或创建一个新的事务。 |
2 |
void commit(TransactionStatus status) 该方法提交给定的事务和关于它的状态。 |
3 |
void rollback(TransactionStatus status) 该方法执行一个给定事务的回滚。 |
TransactionDefinition 是在 Spring 中事务支持的核心接口,它的定义如下:
public interface TransactionDefinition { int getPropagationBehavior(); int getIsolationLevel(); String getName(); int getTimeout(); boolean isReadOnly(); }
序号 | 方法 & 描述 |
---|---|
1 |
int getPropagationBehavior() 该方法返回传播行为。Spring 提供了与 EJB CMT 类似的所有的事务传播选项。 |
2 |
int getIsolationLevel() 该方法返回该事务独立于其他事务的工作的程度。 |
3 |
String getName() 该方法返回该事务的名称。 |
4 |
int getTimeout() 该方法返回以秒为单位的时间间隔,事务必须在该时间间隔内完成。 |
5 |
boolean isReadOnly() 该方法返回该事务是否是只读的。 |
Spring中的隔离级别
:
序号 | 隔离 & 描述 |
---|---|
1 |
TransactionDefinition.ISOLATION_DEFAULT 这是默认的隔离级别。 |
2 |
TransactionDefinition.ISOLATION_READ_COMMITTED 表明能够阻止误读;可以发生不可重复读和虚读。 |
3 |
TransactionDefinition.ISOLATION_READ_UNCOMMITTED 表明可以发生误读、不可重复读和虚读。 |
4 |
TransactionDefinition.ISOLATION_REPEATABLE_READ 表明能够阻止误读和不可重复读;可以发生虚读。 |
5 |
TransactionDefinition.ISOLATION_SERIALIZABLE 表明能够阻止误读、不可重复读和虚读。 |
Spring 事务的传播属性
:
序号 | 传播 & 描述 |
---|---|
1 |
TransactionDefinition.PROPAGATION_MANDATORY 支持当前事务;如果不存在当前事务,则抛出一个异常。 |
2 |
TransactionDefinition.PROPAGATION_NESTED 如果存在当前事务,则在一个嵌套的事务中执行。 |
3 |
TransactionDefinition.PROPAGATION_NEVER 不支持当前事务;如果存在当前事务,则抛出一个异常。 |
4 |
TransactionDefinition.PROPAGATION_NOT_SUPPORTED 不支持当前事务;而总是执行非事务性。 |
5 |
TransactionDefinition.PROPAGATION_REQUIRED 支持当前事务;如果不存在事务,则创建一个新的事务。 |
6 |
TransactionDefinition.PROPAGATION_REQUIRES_NEW 创建一个新事务,如果存在一个事务,则把当前事务挂起。 |
7 |
TransactionDefinition.PROPAGATION_SUPPORTS 支持当前事务;如果不存在,则执行非事务性。 |
8 |
TransactionDefinition.TIMEOUT_DEFAULT 使用默认超时的底层事务系统,或者如果不支持超时则没有。 |
TransactionStatus 接口为事务代码提供了一个简单的方法来控制事务的执行和查询事务状态。
public interface TransactionStatus extends SavepointManager { boolean isNewTransaction(); boolean hasSavepoint(); void setRollbackOnly(); boolean isRollbackOnly(); boolean isCompleted(); }
序号 | 方法 & 描述 |
---|---|
1 |
boolean hasSavepoint() 该方法返回该事务内部是否有一个保存点,也就是说,基于一个保存点已经创建了嵌套事务。 |
2 |
boolean isCompleted() 该方法返回该事务是否完成,也就是说,它是否已经提交或回滚。 |
3 |
boolean isNewTransaction() 在当前事务时新的情况下,该方法返回 true。 |
4 |
boolean isRollbackOnly() 该方法返回该事务是否已标记为 rollback-only。 |
5 |
void setRollbackOnly() 该方法设置该事务为 rollback-only 标记。 |
数据库隔离级别
隔离级别 | 隔离级别的值 | 导致的问题 |
Read-Uncommitted | 0 | 导致脏读 |
Read-Committed | 1 | 避免脏读,允许不可重复读和幻读 |
Repeatable-Read | 2 | 避免脏读,不可重复读,允许幻读 |
Serializable | 3 | 串行化读,事务只能一个一个执行,避免了脏读、不可重复读、幻读。执行效率慢,使用时慎重 |
脏读:一事务对数据进行了增删改,但未提交,另一事务可以读取到未提交的数据。如果第一个事务这时候回滚了,那么第二个事务就读到了脏数据。
不可重复读:一个事务中发生了两次读操作,第一次读操作和第二次操作之间,另外一个事务对数据进行了修改,这时候两次读取的数据是不一致的。
幻读:第一个事务对一定范围的数据进行批量修改,第二个事务在这个范围增加一条数据,这时候第一个事务就会丢失对新增数据的修改。
总结:
隔离级别越高,越能保证数据的完整性和一致性,但是对并发性能的影响也越大。
大多数的数据库默认隔离级别为 Read Commited,比如 SqlServer、Oracle
少数数据库默认隔离级别为:Repeatable Read 比如: MySQL InnoDB
事务的嵌套
通过上面的理论知识的铺垫,我们大致知道了数据库事务和spring事务的一些属性和特点,接下来我们通过分析一些嵌套事务的场景,来深入理解spring事务传播的机制。
假设外层事务 Service A 的 Method A() 调用 内层Service B 的 Method B()
PROPAGATION_REQUIRED(spring 默认)
如果ServiceB.methodB() 的事务级别定义为 PROPAGATION_REQUIRED,那么执行 ServiceA.methodA() 的时候spring已经起了事务,这时调用 ServiceB.methodB(),ServiceB.methodB() 看到自己已经运行在 ServiceA.methodA() 的事务内部,就不再起新的事务。
假如 ServiceB.methodB() 运行的时候发现自己没有在事务中,他就会为自己分配一个事务。
这样,在 ServiceA.methodA() 或者在 ServiceB.methodB() 内的任何地方出现异常,事务都会被回滚。
PROPAGATION_REQUIRES_NEW
比如我们设计 ServiceA.methodA() 的事务级别为 PROPAGATION_REQUIRED,ServiceB.methodB() 的事务级别为 PROPAGATION_REQUIRES_NEW。
那么当执行到 ServiceB.methodB() 的时候,ServiceA.methodA() 所在的事务就会挂起,ServiceB.methodB() 会起一个新的事务,等待 ServiceB.methodB() 的事务完成以后,它才继续执行。
他与 PROPAGATION_REQUIRED 的事务区别在于事务的回滚程度了。因为 ServiceB.methodB() 是新起一个事务,那么就是存在两个不同的事务。如果 ServiceB.methodB() 已经提交,那么 ServiceA.methodA() 失败回滚,ServiceB.methodB() 是不会回滚的。如果 ServiceB.methodB() 失败回滚,如果他抛出的异常被 ServiceA.methodA() 捕获,ServiceA.methodA() 事务仍然可能提交(主要看B抛出的异常是不是A会回滚的异常)。
PROPAGATION_SUPPORTS
假设ServiceB.methodB() 的事务级别为 PROPAGATION_SUPPORTS,那么当执行到ServiceB.methodB()时,如果发现ServiceA.methodA()已经开启了一个事务,则加入当前的事务,如果发现ServiceA.methodA()没有开启事务,则自己也不开启事务。这种时候,内部方法的事务性完全依赖于最外层的事务。
PROPAGATION_NESTED
现在的情况就变得比较复杂了, ServiceB.methodB() 的事务属性被配置为 PROPAGATION_NESTED, 此时两者之间又将如何协作呢? ServiceB#methodB 如果 rollback, 那么内部事务(即 ServiceB#methodB) 将回滚到它执行前的 SavePoint 而外部事务(即 ServiceA#methodA) 可以有以下两种处理方式:
a、捕获异常,执行异常分支逻辑
void methodA() { try { ServiceB.methodB(); } catch (SomeException) { // 执行其他业务, 如 ServiceC.methodC(); } }
这种方式也是嵌套事务最有价值的地方, 它起到了分支执行的效果, 如果 ServiceB.methodB 失败, 那么执行 ServiceC.methodC(), 而 ServiceB.methodB 已经回滚到它执行之前的 SavePoint, 所以不会产生脏数据(相当于此方法从未执行过), 这种特性可以用在某些特殊的业务中, 而 PROPAGATION_REQUIRED 和 PROPAGATION_REQUIRES_NEW 都没有办法做到这一点。
b、 外部事务回滚/提交 代码不做任何修改, 那么如果内部事务(ServiceB#methodB) rollback, 那么首先 ServiceB.methodB 回滚到它执行之前的 SavePoint(在任何情况下都会如此), 外部事务(即 ServiceA#methodA) 将根据具体的配置决定自己是 commit 还是 rollback
另外三种事务传播属性基本用不到,在此不做分析。
总结
对于项目中需要使用到事务的地方,我建议开发者还是使用spring的TransactionCallback接口来实现事务,不要盲目使用spring事务注解,如果一定要使用注解,那么一定要对spring事务的传播机制和隔离级别有个详细的了解,否则很可能发生意想不到的效果。