什么是事务?
有一个最典型的例子用来描述事务:
在银行里,一个人A给另一个人B转账100元,那么银行会有以下两个操作:
- 给A的账户扣100元
- 给B的账户增加100元
但如果在转账的过程中银行系统出了问题,可能会有以下情况:
- A的账户扣了钱,B的账户没有增加钱
- A的账户没扣钱,B的账户增加了钱
上述两种情况都是不符合期望的,但故障总是可能会发生的,那怎么解决这个问题呢?就是用事务。事务是一系列的动作(比如给A账户扣100元和给B账户增加100元这两个动作),它们综合在一起才是一个完整的工作单元,这些动作必须全部完成,如果有一个失败的话,那么事务就会回滚到最开始的状态,仿佛什么都没发生过一样。
也就是说,不管是发生了哪种故障,事务都能回滚到最初的状态:A没有扣钱,B也没有加钱。这样,就能保证整个银行系统的数据是正常的。
在企业级应用程序开发中,事务管理必不可少的技术,主要应用于数据持久化层面(比如数据库),用来确保数据的完整性和一致性。
事务有什么特性?
事务主要有四个特性,我们根据它的英文大写字母简写成:ACID
- 原子性(Atomicity)
事务是一个原子操作,由一系列动作组成。事务的原子性确保动作要么全部完成,要么完全不起作用。所以你可以把一个事务看成一个原子操作。
- 一致性(Consistency)
一旦事务完成(不管成功还是失败),系统必须确保它所建模的业务处于一致的状态,而不会是部分完成部分失败。在现实中的数据不应该被破坏。比如在银行转账的例子中,无论转账成功与否,都需要保证银行的总存款是不变的,这才符合业务的“一致”状态。
- 隔离性(Isolation)
可能有许多事务会同时处理相同的数据,因此每个事务都应该与其他事务隔离开来,防止数据损坏。这里涉及到事务的几种隔离级别,将在下文详细介绍。比如多个人都像B转账,他们之间的事务互相不能影响。
- 持久性(Durability)
一旦事务完成,无论发生什么系统错误,它的结果都不应该受到影响,这样就能从任何系统崩溃中恢复过来。通常情况下,事务的结果被写到持久化存储器(如数据库)中。
Spring事务的核心接口
先上图:
可以看到,Spring只提供事务的接口,不提供具体的实现。具体的实现是由不同的持久层框架自己去做的。这是典型的“门面模式”。主要有三个接口。
这三个接口源码方法上都有大量的注释用于解释每个方法的用途和相应的规则,建议读者详细阅读这三个接口的源码。
- PlatformTransactionManager
事务管理器,主要用于得到事务状态、提交、回滚等操作。
public interface PlatformTransactionManager {
// 得到当前事务或者创建一个新的事务(取决于事务的传播行为)
TransactionStatus getTransaction(@Nullable TransactionDefinition definition) throws TransactionException;
// 提交一个事务
void commit(TransactionStatus status) throws TransactionException;
// 回滚一个事务
void rollback(TransactionStatus status) throws TransactionException;
}
- TransactionDefinition
用于定义事务的一些属性。这个类里面定义了一些常量。
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 = Connection.TRANSACTION_READ_UNCOMMITTED;
int ISOLATION_READ_COMMITTED = Connection.TRANSACTION_READ_COMMITTED;
int ISOLATION_REPEATABLE_READ = Connection.TRANSACTION_REPEATABLE_READ;
int ISOLATION_SERIALIZABLE = Connection.TRANSACTION_SERIALIZABLE;
// 默认-1表示不设置超时
int TIMEOUT_DEFAULT = -1;
int getPropagationBehavior();
int getIsolationLevel();
int getTimeout();
boolean isReadOnly();
@Nullable
String getName();
}
- TransactionStatus
用于判断当前事务的状态。
public interface TransactionStatus extends SavepointManager, Flushable {
boolean isNewTransaction(); // 是否是新的事务
boolean hasSavepoint(); // 是否有恢复点
void setRollbackOnly(); // 设置为只回滚
boolean isRollbackOnly(); // 是否为只回滚
@Override
void flush(); // 刷新session,如果下游实现支持的话
boolean isCompleted(); // 是否已完成
}
事务的属性
可以给事务定义一些属性,根据TransactionDefinition
接口提供的方法来看,主要有传播行为、隔离级别、超时设置、是否只读等四个属性。这里再加上回滚条件,它是在其它地方定义的,但是也算是事务的属性之一。
- 传播行为
再回顾一下TransactionDefinition
接口里面定义的关于传播行为的7个常量:
// 传播行为
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;
事务的传播行为主要用于控制方法间的互相调用的时候,事务的关系。在使用@Transactional
注解的时候,也可以设置当前的事务传播行为,@Transactional
内部有一个枚举类TxType
,里面有前6中传播行为。
public enum TxType {
REQUIRED,
REQUIRES_NEW,
MANDATORY,
SUPPORTS,
NOT_SUPPORTED,
NEVER
}
分别介绍一下这七种传播行为。
REQUIRED
当前方法必须运行在事务中。如果当前事务存在,方法将在该事务中运行。否则,启动一个新的事务。这通常是默认设置,它能够满足我们绝大多数的事务需求。
SUPPORTS
当前方法不需要事务上下文,但是如果存在当前事务的话,那么该方法会在这个事务中运行。
MANDATORY
当前方法必须在事务中运行,如果当前事务不存在,则会抛出一个异常。
REQUIRED_NEW
当前方法必须运行在它自己的事务中。一个新的事务将被启动。如果存在当前事务,在该方法执行期间,当前事务会被挂起。如果使用JTATransactionManager的话,则需要访问TransactionManager。
NOT_SUPPORTED
当前方法不应该运行在事务中。如果存在当前事务,在该方法运行期间,当前事务将被挂起。如果使用JTATransactionManager的话,则需要访问TransactionManager。
NEVER
当前方法不应该运行在事务上下文中。如果当前正有一个事务在运行,则会抛出异常。
NESTED
如果当前已经存在一个事务,那么该方法将会在嵌套事务中运行。嵌套的事务可以独立于当前事务进行单独地提交或回滚。
如果当前事务不存在,那么其行为与REQUIRED
一样。注意各厂商对这种传播行为的支持是有所差异的。可以参考资源管理器的文档来确认它们是否支持嵌套事务。
REQUIRES_NEW
和NESTED
的最大区别在于,REQUIRES_NEW
完全是一个新的事务,而NESTED则是外部事务的子事务,如果外部事务commit,嵌套事务也会被commit,这个规则同样适用于roll back。
- 隔离级别
并发事务引起的问题
就如同我们在本文开头谈到的案例,如果多个人同时向B汇款,那就会同时产生多个事务。抽象到程序设计里面,就是一个并发下的事务问题。多个事务并发运行,可能会导致以下问题:
- 脏读:发生在一个事务读取了另一个事务改写但尚未提交的数据时。如果改写在稍后被回滚了,那么第一个事务获取的数据就是无效的。
- 不可重复读:发生在一个事务执行相同的查询两次或两次以上,但是每次都得到不同的数据时。这通常是因为另一个并发事务在两次查询期间进行了更新。
- 幻读:与不可重复读类似。它发生在一个事务(T1)读取了几行数据,接着另一个并发事务(T2)插入了一些数据时。在随后的查询中,第一个事务(T1)就会发现多了一些原本不存在的记录。
Tips:不可重复读和幻读的区别是,不可重复读针对的是记录里面的值,而幻读针对的是记录的增加或删除。前者只需要锁住满足条件的记录,而后者需要锁住满足条件及其相近的记录。
三种问题,越往后越满足事务的隔离性,但需要锁的东西也越多。
隔离级别定义
再回顾一下TransactionDefinition
接口里面定义的关于隔离级别的5个常量:
// 数据库默认
int ISOLATION_DEFAULT = -1;
// 允许读取尚未提交的数据变更
// 可能会导致所有类型的并发问题
int ISOLATION_READ_UNCOMMITTED = Connection.TRANSACTION_READ_UNCOMMITTED;
// 允许读取并发事务已经提交的数据
// 可以解决 脏读
int ISOLATION_READ_COMMITTED = Connection.TRANSACTION_READ_COMMITTED;
// 对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改
// 可以解决 脏读 和 不可重复读
int ISOLATION_REPEATABLE_READ = Connection.TRANSACTION_REPEATABLE_READ;
// 最高的隔离级别,完全服从ACID的隔离级别
// 可以阻止脏读、不可重复读以及幻读,
// 也是最慢的事务隔离级别,因为它通常是通过完全锁定事务相关的数据库表来实现的
int ISOLATION_SERIALIZABLE = Connection.TRANSACTION_SERIALIZABLE;
- 超时
超时很好理解,因为事务可能涉及对后端数据库的锁定,所以长时间的事务会不必要的占用数据库资源。事务超时就是事务的一个定时器,在特定时间内事务如果没有执行完毕,那么就会自动回滚,而不是一直等待其结束。
- 是否只读
如果事务只对后端的数据库进行该操作,数据库可以利用事务的只读特性来进行一些特定的优化。通过将事务设置为只读,你就可以给数据库一个机会,让它应用它认为合适的优化措施。
而我在《Spring Data JPA进阶(五):事务和锁》这篇文章里提到,在Spring Data JPA中,如果你设置了readOnly为true,hibernate的flush模式会自动设置为NEVER,这样就可以让hibernate跳过“脏检查”阶段,可以显著提升大对象(很多层子对象组成的对象树)的查询性能。
- 回滚条件
默认情况下,事务只有遇到运行期异常RuntimeException
时才会回滚,而在遇到检查型异常时不会回滚(这一行为与EJB的回滚行为是一致的)。
但是你可以声明事务在遇到特定的检查型异常时像遇到运行期异常那样回滚。同样,你还可以声明事务遇到特定的异常不回滚,即使这些异常是运行期异常。
这个回滚条件不是在TransactionDefinition
接口里面定义的,我们可以在使用@Transactional
注解的时候定义。
@Transactional(rollbackOn = {RuntimeException.class, MyException.class},
dontRollbackOn = {FileNotFoundException.class})
事务状态
从TransactionStatus
接口的方法可以了解到事务主要有哪些状态。而PlatformTransactionManager
管理事务也要依靠事务的状态。
public interface TransactionStatus extends SavepointManager, Flushable {
boolean isNewTransaction(); // 是否是新的事务
boolean hasSavepoint(); // 是否有恢复点
void setRollbackOnly(); // 设置为只回滚
boolean isRollbackOnly(); // 是否为只回滚
@Override
void flush(); // 刷新session,如果下游实现支持的话
boolean isCompleted(); // 是否已完成
}
Spring的事务管理抽象主要提供了DefaultTransactionStatus
和SimpleTransactionStatus
两个实现。而MultiTransactionStatus
是由Spring Data项目提供的实现。
这里主要介绍一下Savepoint
和RollbackOnly
两个概念。
- Savepoint
Savepoint的概念是来自于SavepointManager接口,可以在一个事务中创建多个“恢复点”,在回滚的时候可以回滚到定义好的“恢复点”:
public interface SavepointManager {
// 创建一个恢复点
Object createSavepoint() throws TransactionException;
// 回滚到恢复点
void rollbackToSavepoint(Object savepoint) throws TransactionException;
// 删除一个恢复点
void releaseSavepoint(Object savepoint) throws TransactionException;
}
- RollbackOnly
这个其实是与事务的传播行为有关。在Spring中,默认的事务传播行为通常是REQUIRED,如果当前事务存在,方法将在该事务中运行。否则,启动一个新的事务。
那这种情况下,A方法调用B方法,需要等AB方法都执行完之后才能提交事务。如果在这个过程中,B方法发生了异常,Spring就会将该事务标志为RollbackOnly,在A方法执行完后提交事务之前会检查当前事务的RollbackOnly标志,如果是true,就会回滚。
所有在使用Spring Data JPA或Mybatis等框架时可能经常遇到报Transaction rolled back because it has been marked as rollback-only的异常,这个异常的原理就是这样的。解决这个问题的方式有两种:
- 修改事务的传播行为
- 在使用
@Transactional
的时候设置好dontRollbackOn
属性,忽略指定的异常。