6.1 事务介绍

数据库事务(transaction)是访问并可能操作各种数据项的一个数据库操作序列,这些操作要么全部执行,要么全部不执行,是一个不可分割的工作单位。事务由事务开始与事务结束之间执行的全部数据库操作组成。

比如说,在人员管理系统中,你删除一个人员,你即需要删除人员的基本资料,也要删除和该人员相关的信息,如信箱,文章等等,所有这些数据库的删除操作语句就构成一个数据库事务:要么删除了该用户和该用户的相关信息(如信箱,文章等等),要么什么都不删除。

本小节,介绍事务有关的重要概念,希望读者一定要认真思考,以便了解 Spring 是怎样管理事务的。

6.1.1 事务的 ACID 特性

在任何地方讲到事务,都会提到事务的 ACID 特性。

事务的 ACID 特性是由关系数据库系统(DBMS)来实现的,DBMS 采用日志来保证事务的原子性、一致性和持久性。日志记录了事务对数据库所作的更新,如果某个事务在执行过程中发生错误,就可以根据日志撤销事务对数据库已做的更新,使得数据库回滚到执行事务前的初始状态。

对于事务的隔离性,DBMS 是采用锁机制来实现的。当多个事务同时更新数据库中相同的数据时,只允许持有锁的事务能更新该数据,其他事务必须等待,直到前一个事务释放了锁,其他事务才有机会更新该数据。

所以,数据库通过日志+锁 的方式实现了事务的 ACID 特性。

6.1.1.1 原子性(Atomicity)

所谓的原子性就是说,在整个事务中的所有操作,要么全部完成,要么全部不做,没有中间状态。对于事务在执行中发生错误时,所有的操作就都会被回滚,整个事务就像从来没有被执行过一样。

6.1.1.2 一致性(Consistency)

事务的执行必须保证系统的一致性,就拿转账为例,A有500元,B有300元,如果在一个事务里 A 成功转给 B 50元,那么不管并发多少,不管发生什么,只要事务执行成功了,那么最后 A 账户一定是 450 元,B 账户一定是 350 元。

6.1.1.3 隔离性(Isolation)

所谓的隔离性就是说,事务与事务之间不会互相影响,一个事务的中间状态不会被其他事务所感知。

6.1.1.4 持久性(Durability)

所谓的持久性,就是说一但事务完成了,那么事务对数据所做的变更就完全保存在了数据库中,即使发生停电,系统宕机也是如此,数据不会丢失。

6.1.2 事务的传播行为

事务的传播行为(propagation behavior)决定了事务和事务之间如何协作。在程序中当一个事务方法被另一个事务方法调用时,必须指定事务应该如何传播。例如:方法可能继续在现有事务中运行,也可能开启一个新的事务,并在自己的事务中运行。Spring 定义了七种传播行为:

传播行为 含义
PROPAGATION_REQUIRED 表示当前方法必须运行在事务中。如果当前事务存在,方法将会在该事务中运行。否则,会启动一个新的事务。
PROPAGATION_SUPPORTS 表示当前方法不需要事务上下文,但是如果存在当前事务的话,那么该方法会在这个事务中运行。
PROPAGATION_MANDATORY 表示该方法必须在事务中运行,如果当前事务不存在,则会抛出一个异常
PROPAGATION_REQUIRED_NEW 表示当前方法必须运行在它自己的事务中。一个新的事务将被启动。如果存在当前事务,在该方法执行期间,当前事务会被挂起。如果使用 JTATransactionManager 的话,则需要访问 TransactionManager。
PROPAGATION_NOT_SUPPORTED 表示该方法不应该运行在事务中。如果存在当前事务,在该方法运行期间,当前事务将被挂起。如果使用 JTATransactionManager 的话,则需要访问 TransactionManager。
PROPAGATION_NEVER 表示当前方法不应该运行在事务上下文中。如果当前正有一个事务在运行,则会抛出异常。
PROPAGATION_NESTED 表示如果当前已经存在一个事务,那么该方法将会在嵌套事务中运行。嵌套的事务可以独立于当前事务进行单独地提交或回滚。如果当前事务不存在,那么其行为与 PROPAGATION_REQUIRED 一样。注意各厂商对这种传播行为的支持是有所差异的。可以参考资源管理器的文档来确认它们是否支持嵌套事务。

Spring 在org.springframework.transaction.annotation.Propagation枚举类中定义了上述七个表示传播行为的枚举值:

public enum Propagation {
	REQUIRED(TransactionDefinition.PROPAGATION_REQUIRED),
	SUPPORTS(TransactionDefinition.PROPAGATION_SUPPORTS),
	MANDATORY(TransactionDefinition.PROPAGATION_MANDATORY),
	REQUIRES_NEW(TransactionDefinition.PROPAGATION_REQUIRES_NEW),
	NOT_SUPPORTED(TransactionDefinition.PROPAGATION_NOT_SUPPORTED),
	NEVER(TransactionDefinition.PROPAGATION_NEVER),
	NESTED(TransactionDefinition.PROPAGATION_NESTED);
}

在程序中,通过@Transactional注解的propagation属性设置事务传播行为。

@Transactional(propagation = Propagation.REQUIRED)

6.1.3 事务的隔离级别

事务的隔离级别是指若干个并发的事务之间的隔离程度,与我们开发的时候主要相关的场景包括:脏读、重复读、幻读

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

我们可以看org.springframework.transaction.annotation.Isolation枚举类中定义了五个表示隔离级别的值:

public enum Isolation {
	DEFAULT(TransactionDefinition.ISOLATION_DEFAULT),
	READ_UNCOMMITTED(TransactionDefinition.ISOLATION_READ_UNCOMMITTED),
	READ_COMMITTED(TransactionDefinition.ISOLATION_READ_COMMITTED),
	REPEATABLE_READ(TransactionDefinition.ISOLATION_REPEATABLE_READ),
	SERIALIZABLE(TransactionDefinition.ISOLATION_SERIALIZABLE);
}
  • DEFAULT(默认):这是默认值,表示使用底层数据库的默认隔离级别。对大部分数据库而言,通常这值就是:READ_COMMITTED
  • READ_UNCOMMITTED:该隔离级别表示一个事务可以读取另一个事务修改但还没有提交的数据。该级别不能防止脏读和不可重复读,与因此很少使用该隔离级别。
  • READ_COMMITTED:该隔离级别表示一个事务只能读取另一个事务已经提交的数据。该级别可以防止脏读,这也是大多数情况下的推荐值。
  • REPEATABLE_READ(可重复读):该隔离级别表示一个事务在整个过程中可以多次重复执行某个查询,并且每次返回的记录都相同。即使在多次查询之间有新增的数据满足该查询,这些新增的记录也会被忽略。该级别可以防止脏读和不可重复读。
  • SERIALIZABLE:所有的事务一次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,该级别可以防止脏读、不可重复读以及幻读。但是这将严重影响程序的性能。通常情况下也不会用到该级别。

在程序中,通过@Transactional注解的isolation属性设置事务的隔离级别。

@Transactional(isolation=Isolation.DEFAULT)

6.1.4 脏读

脏读 是指一个事务处理过程里读取了另一个未提交的事务中的数据。 当一个事务正在多次修改某个数据,而在这个事务中这多次的修改都还未提交,这时一个并发的事务来访问该数据,就会造成两个事务得到的数据不一致。例如:用户 A 向用户 B 转账 100 元,对应SQL命令如下:

update account set money=money+100 where name=’B’;(此时A通知B)
update account set money=money-100 where name=’A’;

当只执行第一条 SQL 时,A 通知 B 查看账户,B 发现确实钱已到账(此时即发生了脏读),而之后无论第二条 SQL 是否执行,只要该事务不提交,则所有操作都将回滚,那么当 B 以后再次查看账户时就会发现钱其实并没有转。

6.1.5 不可重复读

不可重复读 是指在对于数据库中的某个数据,一个事务范围内多次查询却返回了不同的数据值,这是由于在查询间隔,被另一个事务修改并提交了。

例如事务 T1 在读取某一个数据,而事务 T2 立马修改了这个数据并且提交给数据库,事务 T1 再次读取该数据就得到了不同的结果,发生了不可重复读。

不可重复读和脏读的区别是,脏读是某一事务读取了另一个事务未提交的脏数据,而不可重复读则是读取了前一事务提交的数据。

在某些情况下,不可重复读并不是问题,比如我们多次查询某个数据当然以最后查询得到的结果为主。但在另一些情况下就有可能发生问题,例如对于同一个数据 A 和 B 依次查询就可能不同,A 和 B 就可能冲突。

6.1.6 幻读(虚读)

幻读事务非独立执行时发生的一种现象。例如事务 T1 对一个表中所有的行的某个数据项做了从“1”修改为“2”的操作,这时事务 T2 又对这个表中插入了一行数据项,而这个数据项的数值还是为“1”并且提交给数据库。而操作事务 T1 的用户如果再查看刚刚修改的数据,会发现还有一行没有修改,其实这行是从事务 T2 中添加的,就好像产生幻觉一样,这就是发生了幻读。

幻读和不可重复读都是读取了另一条已经提交的事务(这点与脏读不同),所不同的是不可重复读查询的都是同一个数据项,而幻读针对的是一批数据整体(比如用 count 查询数据的个数)。

下一节:Java EE 中访问数据的技术众多,包含了从最基础的 JDBC 到各种方便快捷的 ORM 技术,如 JTA、Hibernate、MyBatis 等。Spring 面对众多的数据访问技术,在事务管理 API 上定义了一个抽象的 PlatformTransactionManager 接口来隔离事务底层的复杂性,让应用程序开发人员不需要了解底层的事务 API 就可以得心应手的使用事务管理机制。