Spring事务的基本原理
Spring事务的本质其实就是数据库对事务的支持,没有数据库的事务支持,spring是无法提供事务功能的。对于纯JDBC操作数据库,想要用到事务,可以按照以下步骤进行:
-
获取连接 Connection con = DriverManager.getConnection()
-
开启事务con.setAutoCommit(true/false);
-
执行CRUD
-
提交事务/回滚事务 con.commit() / con.rollback();
-
关闭连接 conn.close();
使用Spring的事务管理功能后,我们可以不再写步骤 2 和 4 的代码,而是由Spirng 自动完成。那么Spring是如何在我们书写的 CRUD 之前和之后开启事务和关闭事务的呢?解决这个问题,也就可以从整体上理解Spring的事务管理实现原理了。下面简单地介绍下,注解方式为例子
-
配置文件开启注解驱动,在相关的类和方法上通过注解@Transactional标识。
-
spring 在启动的时候会去解析生成相关的bean,这时候会查看拥有相关注解的类和方法,并且为这些类和方法生成代理,并根据@Transaction的相关参数进行相关配置注入,这样就在代理中为我们把相关的事务处理掉了(开启正常提交事务,异常回滚事务)。
-
真正的数据库层的事务提交和回滚是通过binlog或者redo log实现的。
Spring的事务机制
所有的数据访问技术都有事务处理机制,这些技术提供了API用来开启事务、提交事务来完成数据操作,或者在发生错误的时候回滚数据。而Spring的事务机制是用统一的机制来处理不同数据访问技术的事务处理。Spring的事务机制提供了一个PlatformTransactionManager接口,不同的数据访问技术的事务使用不同的接口实现,如表所示:
数据访问技术
实现
JDBC
DataSourceTransactionManager
JPA
JapTransactionManager
Hibernate
HibernateTransactionManager
JDO
JdoTransactionManager
分布式事务
JtaTransactionManager
在程序中定义事务管理器的代码如下:
@Bean
public PlatformTransactionManager transactionManager() {
JpaTransactionManager transactionManager = new JpaTransactionManager();
transactionManager.setDataSource(dataSource());
return transactionManager;
}
声名式事务
Spring支持声名式事务,即使用注解来选择需要使用事务的方法,它使用@Transactional注解在方法上表明该方法需要事务支持。这是一个基于AOP的实现操作。
import org.springframework.transaction.annotation.Transactional;
@Transactional
public void saveSomething(Long id, String name) {
//数据库操作
}
@Transactional 注解属性
-
rollbackFor
:触发回滚的异常,默认是RuntimeException和Error -
isolation: 事务的隔离级别,默认是Isolation.DEFAULT也就是数据库自身的默认隔离级别,如MySQL是ISOLATION_REPEATABLE_READ可重复读
ps:网络上还说要在@SpringBootApplication上添加注解@EnableTransactionManagement,已经不需要了
Spring 事务的传播属性
Spring 事务传播性是指当多个含有事务的方法嵌套调用时,这多个方法处理事务的规则。比如下图,当事务方法A调用事务方法B时,内层事务B会合并到外层调用A方法的事务中,还是会开启自己的事务。另外如果合并到外层事务,那么当内层方法回滚时后,外层方法会不会回滚。在日常开发环境中,我们最常用到的事务传播行为只有两种,一是 REQUIRED,二是 REQUIRES_NEW
propagation_required
spring默认的事务传播行为是propagation_required,它指的是如果外层调用方法A已经开启事务,那么当前方法B就会加入到外层方法、如果外层方法没有开启事务,那么当前事务就会开启一个事务。这种传播行为可以保证多个嵌套的事务方法在同一个事务内执行,也就是说可以保证多个事务方法同时提交或同时回滚。这个机制满足大多数业务场景。
propagation_requires_new
这个传播行为是每次都新开启一个事务。如果外层调用方法A已经开启了事务,就先把外层的事务挂起,然后执行当前方法B的新事务,执行完毕后再恢复上层事务的执行。这样当内层方法B抛出异常回滚自己的事务时,不会影响外层事务方法的执行
propagation_supported
这个传播行为是指,如果外层调用方法A开启了事务,那当前方法B加入到外层事务。如果外层A不存在事务,那么当前方法B也不会创建新事务,直接使用非事务方式执行。
propagation_not_supported
这个传播行为不支持事务。也就是如果外层调用者开启了事务,就挂起外层事务 ,然后以非事务方式执行当前方法逻辑,等执行完毕后,再恢复外层事务的执行。
propagation_never
这个传播行为不支持事务。也就是说如果外层调用者A开启了事务,就执行当前方法B前会抛出异常。就是说调用方A不能开始事务
propagation_mandatory
这个传播行为是说,配置了这个传播性的方法只能在已经存在事务的方法中被调用。如果在不存在事务的方法中被调用,会抛出异常。就是说调用方A必须开始事务,否则被调用方B会报错
proppagation_nested(嵌套的)
当外层调用方存在事务时,当前方法B合并到外层事务,如果外层不存在事务,就当前开启事务,这点和 PROPAGATION_REQUIRED 传播性一致,不同的是,PROPAGATION_NESTED传播行为的特点是可以保存状态保存点,当事务回滚时,可以回滚到某一个保存点上,从而避免所有嵌套事务都回滚。下面通过一个例子来介绍下它的传播行为:
@Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRED)
public void methodA() {
//1.1 做自己的入库操作
insert()
System.out.println("save something to db");
//1.2 调用服务 B 的入库操作, savepoint savepoint savepoint savepoint savepoint
try{
serviceB.methodB();
}catch(Exception e){
}
//1.3 执行更新操作
update();
}
@Service
public class ServiceB {
@Transactional(rollbackFor = Exception.class, propagation = Propagation.NESTED)
public void methodB() {
insert();
System.out.println("save something to db");
}
在这段代码中,methodA 执行时,如果调用方没开启事务,它就会开启一个事务,然后执行插入操作;执行 1.2 时由于 methodB 传播性为 NESTED,所以会新建一个 savepoint,用于标记代码 1.1 执行的插入操作。然后 methodB 的执行是使用了 methodA 开启事务的。假如methodB 执行抛出异常时,methodB 的插入操作会被回滚掉,但是 methodA 中的代码 1.1 的 insert 操作不会回滚,这是因为执行 methodB 前建立了 savepoint 点,methodB 的回滚只会回滚到这个 savepoint 点创建的时刻,并且这里当 methodB 回滚后,代码 1.1 和代码 1.3 的执行都会被提交到数据库
不同的业务场景下如何选择合适的隔离界别
// TODO
数据库隔离级别
隔离级别
隔离级别
隔离级别的值
导致的问题
Read-Uncommitted
读未提交
0
导致脏读
Read-Committed
读提交
1
避免脏读,允许不可重复读和幻读
Repeatable-Read
可重复度
2
避免脏读,不可重复读,允许幻读
Serializable
串行化
3
串行化读,事务只能一个一个执行,避免了脏读、不可重复读、幻读。执行效率慢,使用时慎重
不考虑隔离性引发安全性问题:
-
脏读:事务A对数据进行了增删改,但未提交,事务B可以读取到事务A未提交的数据。如果事务A这时候回滚了,那么事务B就读到了脏数据。
-
不可重复读:事务A中在执行期间发起了两次读操作,第一次读操作和第二次操作之间,事务B对数据进行了update操作,这时候两次读取的数据是不一致的。即一个事务读到了另一个事务已经提交的 update 的数据导致多次查询结果不一致.
-
幻读:事务A对一定范围的数据进行批量修改,事务B在这个范围insert一条数据,这时候第一个事务就会丢失对新增数据的修改。即一个事务读到了另一个事务已经提交的 insert 的数据导致多次查询结果不一致.
不同的业务场景应该选择哪种隔离界别的呢
- 银行对账单就不允许幻读的出现
// TODO
Spring中的隔离级别
常量
解释
ISOLATION_DEFAULT
这是个 PlatfromTransactionManager 默认的隔离级别,使用数据库默认的事务隔离级别。另外四个与 JDBC 的隔离级别相对应。
ISOLATION_READ_UNCOMMITTED
这是事务最低的隔离级别,它充许另外一个事务可以看到这个事务未提交的数据。这种隔离级别会产生脏读,不可重复读和幻像读。
ISOLATION_READ_COMMITTED
保证一个事务修改的数据提交后才能被另外一个事务读取。另外一个事务不能读取该事务未提交的数据。
ISOLATION_REPEATABLE_READ
这种事务隔离级别可以防止脏读,不可重复读。但是可能出现幻像读。
ISOLATION_SERIALIZABLE
这是花费最高代价但是最可靠的事务隔离级别。事务被处理为顺序执行。
// TODO 数据库是通过视图和锁来实现隔离了,Spring的隔离级别还得自己测试下,难不成Spring会在数据库创建视图?
Spring Boot 对事务的支持
通过org.springframework.boot.autoconfigure.transaction.TransactionAutoConfiguration类。我们可以看出Spring Boot自动开启了对注解事务的支持 Spring
只读事务(@Transactional(readOnly = true))的一些概念
从这一点设置的时间点开始(时间点a)到这个事务结束的过程中,其他事务所提交的数据,该事务将看不见!(查询中不会出现别人在时间点a之后提交的数据)。
@Transcational(readOnly=true) 这个注解一般会写在业务类上,或者其方法上,用来对其添加事务控制。当括号中添加readOnly=true, 则会告诉底层数据源,这个是一个只读事务,对于JDBC而言,只读事务会有一定的速度优化。而这样写的话,事务控制的其他配置则采用默认值,事务的隔离级别(isolation) 为DEFAULT,也就是跟随底层数据源的隔离级别,事务的传播行为(propagation)则是REQUIRED,所以还是会有事务存在,一代在代码中抛出RuntimeException,依然会导致事务回滚。
应用场合:
-
如果你一次执行单条查询语句,则没有必要启用事务支持,数据库默认支持SQL执行期间的读一致性;
-
如果你一次执行多条查询语句,例如统计查询,报表查询,在这种场景下,多条查询SQL必须保证整体的读一致性,否则,在前条SQL查询之后,后条SQL查询之前,数据被其他用户改变,则该次整体的统计查询将会出现读数据不一致的状态,此时,应该启用事务支持。
搬运
深入理解 Spring 事务原理my.oschina.net/xiaolyuh/bl…
极客每日一课,你对Spring传播特性的理解