一、概述
Spring 声明式事务指的是通过配置事务管理器,将涉及到数据库的写操作统一交给事务管理器去管理,最终保证写操作之后数据一致性的一种事务解决方案。本篇文章将介绍数据库事务的相关基础内容,以及通过 Spring 提供的数据访问模块 Spring JDBC 介绍Spring 声明式事务的基本使用。
二、事务基础知识
数据库的事务就是将一系列数据库的操作当做一个独立的单元,这个单元中的所有动作要么全部执行成功,要么全部执行失败。
事务的特性
数据库的事务有如下的四个特性,也就是我们经常说的 ACID 特性。这些事务特性只针对于数据库中的 DML (数据操纵语言)操作,对 DDL(数据库定义语言)是不生效的。
(1)原子性(Atomicity)
事务是原子性操作,由一系列动作组成,事务的原子性确保动作要么全部完成,要么完全不起作用。
(2)一致性(Consistency)
一旦所有事务动作完成,事务就要被提交。数据和资源处于一种满足业务规则的一致性状态中。
(3)隔离性(Isolation)
可能多个事务会同时处理相同的数据,因此每个事务都应该与其他事务隔离开来,防止数据损坏。
(4)持久性(Durability)
事务一旦完成,无论系统发生什么错误,结果都不会受到影响。通常情况下,事务的结果被写到持久化存储器中。
并发事务导致的问题和隔离级别
事务特性中定义了事务的隔离性特性,这个特性用来保证多个并发事务同时操作某些相同数据库时的数据一致性。
并发事务引发的问题
在并发事务操作的过程中,可能会出现如下的三种类型的问题
(1)脏读
一个事务A读取到了另外一个事务B尚未提交的数据。这个时候,B事务如果回滚了,事务A读取到的数据将会是和预期不一样的。一般应用中是不允许出现这种情况的。
(2)不可重复读
一个事务执行了多次相同的查询操作,读取到的数据却不一样,通常是因为有另外一个并发事务在第一个事务读取期间进行了数据的更新操作。
(3)幻读
一个事务执行多次查询操作时,每次查询到的数据条数不同,通常是因为有另外一个并发事务在多次查询之间执行了插入或者删除数据的操作。
注意:不可重复读和幻读主要区别是不可重复读侧重的是并发事务对数据进行了更新操作,而幻读则侧重的是并发事务对数据的新增或者删除操作。
隔离级别
针对这个隔离性及并发事务可能出现的几种问题,在数据库中定义了如下的四种隔离级别用来解决这种并发事务导致的问题。
(1)读未提交(READ UNCOMMITED)
允许一个事务读取到其他事务未提交的数据,会出现脏读,不可重复读和幻读的问题。
(2)读已提交(READ COMMITED)
只允许一个事务读取其他事务已经提交的数据,可以避免脏读,但是不能避免不可重复读和幻读的问题。
(3)可重复读(REPEATABLE READ)
确保一个事务多次读取某一行都是相同的值,在读取期间,禁止其他事务对改数据进行更新操作,避免了脏读和不可重复读,但还是会出现幻读。这种隔离级别是MySQL的默认隔离级别。
(4)串行化(SERIALIZABLE)
保证一个事务多次读取同一行都是相同的数据,在读取期间,禁止其他事务对该表进行更新,删除,插入操作,避免了脏读,不可重复读,幻读,但是性能低下,相当于所有的并发事务都被串行去执行了。
三、Spring框架对事务的支持
事务管理的概念,通常有以下的两种实现方式:
编程式事务
在应用代码中通过事务管理的代码来保证事务,这种方法会导致应用中出现很多事务管理的重复代码,所以很少用。
声明式事务
通过AOP将事务管理的功能横切出来,对需要使用事务的方法进行动态织入,解决了应用代码和事务管理代码的强耦合性,应用中通常使用该种方式。这种方式实现过程中,将事务的管理工作交给一个叫做事务管理的组件来完成,事务管理器是个什么玩意呢?继续向下看:
事务管理器
事务管理器是 Spring 将事务操作(开启事务,关闭事务,提交,回滚)封装之后,形成的一种事务操作的抽象对象,是 Spring 事务管理的核心,无论是编程式的事务管理还是声明式的事务管理,都需要事务管理器的支持。在 Spring 框架中,中定义了一个用于事务管理的核心接口,即:PlatformTransactionManager,里面提供了事务的常见操作,针对这个接口,有如下的几种具体实现:
(1)DataSourceTransactionManager
Spring-JDBC 中实现的事务管理器,使用的时候需要提供数据源dataSource。
(2)HibernateTransactionManager
Hibernate 框架中实现的事务管理器,使用时需要提供 session 工厂 sessionFactory。
(3)JpaTransactionManager
JPA框架中实现的事务管理器,使用时需要提供 session 工厂 sessionFactory。
事务属性之传播行为
有了事务管理器之后,执行事务操作时,事务的回滚和提交都交给了事务管理器。那么在一个事务范围中调用另外一个事务方法,这个时候用的是哪个事务事务范围中的事务呢?这就涉及到了事务的传播行为了,Spring 中定义了如下的 7 种事务传播行为:
REQUIRED
如果有事务在运行,当前的方法就在这个事务内运行,否则就开启一个新的事务,并在自己的事务内运行,默认传播行为。
示例:service1 和 service2 方法中都声明了事务,默认传播行为为 REQUIRED,则在 service 的调用过程中,当前只存在着一个共享的事务,当有任何的异常抛出时,所有的操作都会回滚
public class OrderService {
@Transactional
public void service() {
service1();
service2();
}
@Transactional
public void service1() {
}
@Transactional
public void service2() {
}
}
REQUIRED_NEW
如果当前存在事务,则暂停当前的事务,重新创建一个新的事务(如果当前没有事务,则直接创建就行)。具体做法是:将当前事务封装到一个实体中,然后去创建一个新的事务,新的事务接收这个实体为参数,用于事务的恢复。如果当前有事务,经过这个操作之后,就会存在着两个事务,这两个事务之间没有任何依赖关系,可以实现新事务回滚,外部事务可以继续执行。
示例: 如下示例中,当前执行的 service 方法中存在着事务,然后去调用 service2 的时候,service2 中使用了 REQUIRES_NEW,所以会重新创建一个新的事务。而在 service2 中抛出了异常,将会导致service2中的操作都被回滚,但是 service2 中的异常在 service 方法中被 catch住,所以,service1 还是正常执行,提交事务。
注意: service 中的 try catch 是必须的,否则,service2 中的事务也将会导致service中调用的service1操作也被回滚。
@Transactional
public void service() {
service1();
try {
service2();
} catch(Exception e) {}
}
service1() {
executeUpdate(sql);
}
@Transactional(propagation=Propagation.REQUIRES_NEW)
service2() {
executeUpdate(sql1);
int i = 1 / 0;
executeUpdate(sql2);
}
SUPPORTS
如果有事务在运行,当前的方法就在这个事务内运行,否则可以不运行在事务中。
示例1: 下述示例中,在 service 执行时没有事务,所以在service中抛出的异常不会导致 service1 方法回滚。
public void service() {
service1();
throw new RuntimeException();
}
@Transactional(propagation=Propagation.SUPPORTS)
service1() {}
示例2: 如下,如果在service1方法中执行了多次数据库DML操作,中间抛出了异常。service上没开事务,这个时候 DML 是否生效和数据库底层中的 defaultAutoCommit 属性相关。
- 如果defaultAutoCommit=true,则表示自动提交,此时相当于没有事务,异常之前的DML操作都会生效;
- 如果defaultAutoCommit=false,此时异常之前的SQL不会生效;
- 在service执行的时候如果加入了事务,则service1中的异常会导致所有DML操作都回滚;
public void service() {
service1();
}
@Transactional(propagation=Propagation.SUPPORTS)
service1() {
executeUpdate(sql1); // DML1
int a = 1 / 0;
executeUpdate(sql2); // DML2
}
NOT_SUPPORTED
如果当前方法中存在事务,则当前的事务会被挂起。然后在当前方法中调用的新方法都会以无事务的方式执行。如果其他方法在无事务的环境下执行,能否生效将会取决于数据库底层的defaultAutoCommit 属性。
示例: 如下示例,在 service 方法中存在事务,调用 service2 方法时,由于service2是NOT_SUPPORTED 类型的,所以执行 service2 的时候将会以无事务的方式运行,此时 service1方法中的执行结果将会被回滚,但是 service2 中的 sql1 执行结果是否生效取决于数据库底层的defaultAutoCommit 属性,如果为 true,sql1 的执行结果将会生效;如果为 false,sql1 的执行结果将不会生效。
@Transactional
public void service() {
service1();
service2();
}
service1() {
executeUpdate(sql);
}
@Transactional(propagation=Propagation.NOT_SUPPORTED)
service2() {
executeUpdate(sql1);
int i = 1 / 0;
executeUpdate(sql2);
}
MANDATORY
当前的方法必须运行在事务内部,如果没有正在运行的事务,就会抛出异常。
示例: 如下,在执行 service 方法的时候,由于当前没有事务,去调用一个带事务的方法,会直接抛出异常抛出异常之后,有如下的两种情况:
- 如果数据库底层设置的defaultAutoCommit=true,则service1中将不存在着事务,sql执行将会生效;
- 如果数据库底层设置的defaultAutoCommit=false,则service1中的sql执行完成之后未提交,sql将不会生效
public void service() {
service1();
service2();
}
service1() {
executeUpdate(sql);
}
@Transactional(propagation=Propagation.MANDATORY)
service2() {
executeUpdate(sql);
}
NEVER
如果当前存在事务,则会直接抛出异常。如果当前不存在事务,将会以无事务的方式执行。
示例: 如下示例,service 上如果标注异常,则会直接抛出异常,都不会执行。service上不存在事务,而 service2 方法上标注了NEVER类型的传播行为,此时 service2 将会以无事务的方式执行,service1和service2中的sql1是否生效取决于数据库底层的 defaultAutoCommit 属性,如果为true,则都会生效,否则都不生效。
public void service() {
service1();
service2();
}
service1() {
executeUpdate(sql);
}
@Transactional(propagation=Propagation.NEVER)
service2() {
executeUpdate(sql1);
int i = 1 / 0;
executeUpdate(sql2);
}
NESTED
如果当前存在事务,则使用 SavePoin t将当前的事务状态保存起来,然后底层和嵌套事务使用同一个连接,当嵌套事务出现异常时,将会自动回滚到 SavePoint 这个状态,如果嵌套事务出现的异常被当前事务捕获到之后,当前事务可以继续向下执行,而不会影响到当前事务。但是如果当前的事务中出现了异常,将会导致当前事务以及所有的内嵌事务全部回滚。
Spring 中配置事务管理器时,默认不配置的话是不支持嵌套事务的。如果需要启用,需要在事务管理器中指定:
1. xml方式:
<bean id="dataTransactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataDataSource" />
<property name="nestedTransactionAllowed" value="true" />
</bean>
2. JavaConfig方式
@Bean
public PlatformTransactionManager transactionManager()
throws Exception
// dataSource为另外一个@Bean声明的方法,表示被事务管理器管理的数据源
DataSourceTransactionManager transactionManager = new DataSourceTransactionManager(dataSource());
transactionManager.setNestedTransactionAllowed(true);
return transactionManager;
}
示例1: 如下示例中,由于service2属于service的嵌套事务,所以service2中出现的异常将会导致service2方法中的操作被回滚。但是service1中的操作不会受影响,catch到异常之后将会继续向下执行。
@Transactional
public void service() {
service1();
try {
service2();
} catch(Exception e) {}
}
service1() {
executeUpdate(sql);
}
@Transactional(propagation=Propagation.NESTED)
service2() {
executeUpdate(sql1);
int i = 1 / 0;
executeUpdate(sql2);
}
示例2: 如下示例,在 service 中开启了事务,然后调用了嵌套事务 service2,当 service 中发生异常时,除了 service 中本身的操作被回滚之外,service2中的操作也将会被全部回滚。这是和PROPAGATION_REQUIRES_NEW方式不同的地方,NESTED下的嵌套事务将会受到外部事务中的异常而回滚。而PROPAGATION_REQUIRES_NEW中,内嵌的事务不会因为外部事务异常而回滚。
@Transactional
public void service() {
service1();
service2();
int i = 1 / 0;
}
service1() {
executeUpdate(sql);
}
@Transactional(propagation=Propagation.NESTED)
service2() {
executeUpdate(sql1);
executeUpdate(sql2);
}
事务属性之回滚规则
Spring 中事务管理器管理的事务默认情况下,只有发生了运行时异常 RuntimeException 或者错误Error 时,才会回滚。但是事务的回滚规则可以通过事务管理器的属性来指定,通常可以指定什么时候回滚和什么时候不回滚。如下:
rollbackFor: 指定一组异常类,表示遇到这种异常时进行回滚
noRollbackFor: 指定一组异常类,表示遇到这种异常时不进行回滚
只读属性
可以通过事务管理器中的属性来指定该事务为一个只读事务,只读事务表示该事务只读读取数据,不进行数据的更新操作。指定了只读属性之后,数据库引擎会对该事务进行优化。
事务超时属性
事务在运行过程中会获取到表上或者数据行上的锁,如果长时间持有锁,可能会导致其他操作阻塞比较长的时间,影响应用的性能。所以可以通过指定事务的超时属性来强制事务最多经过多长时间之后必须要回滚,释放资源。
至此,数据库事务的基础内容及Spring对事务支持的相关内容介绍完毕。