# Spring 事务管理
# 1. 事务的四个特性
原子性 Atomicity
事务包含的所有操作要么全部成功,要么全部失败回滚;成功必须要完全应用到数据库,失败则不能对数据库产生影响。
一致性 Consistency
事务执行前和执行后必须处于一致性状态。例如:转账事务执行前后,两账户余额的总和不变。
隔离性 Isolation
多个并发的事务之间要相互隔离。
持久性 Durability
事务一旦提交,对数据库的改变是永久性的.
# 2. 声明式事务管理
在 Spring 进行声明式事务管理,底层使用 AOP 原理。
# 2.1 重要的接口 PlatformTransactionManager
Spring 提供一个接口 PlatformTransactionManager,代表事务管理器,这个接口针对不同的框架提供不同的实现类:
# 2.2 基于 XML 配置
# 2.2.1 引入事务管理命名空间 tx
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd">
# 2.2.2 配置事务管理器
<!--配置事务管理-->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<!--注入数据源-->
<property name="dataSource" ref="dataSource"></property>
</bean>
# 2.2.3 配置通知
<!--2 配置通知-->
<tx:advice id="txadvice">
<!--配置事务参数-->
<tx:attributes>
<!--指定在符合哪种规则的方法上添加事务-->
<!--这个表示在 transfer 方法上添加事务-->
<tx:method name="transfer" propagation="REQUIRED"
isolation="REPEATABLE_READ"
timeout="-1"
read-only="false"/>
<!-- tx:method name="transfer*"></tx:method>-->
</tx:attributes>
</tx:advice>
# 2.2.4 配置切入点和切面
<!--3 配置切入点和切面-->
<aop:config>
<!--配置切入点-->
<aop:pointcut id="pointcut" expression="execution(* com.hedon.service.AccountService.*(..))"/>
<!--配置切面-->
<aop:advisor advice-ref="txadvice" pointcut-ref="pointcut"></aop:advisor>
</aop:config>
# 2.2.5 开启事务注解
执行前:
执行后报错:
数据库还是没有变,说明事务管理起到了作用:
# 2.3 基于注解
# 2.3.1 创建配置类 SpringConfig
@Configuration 配置类
@ComponentScan 组件扫描
@EnableTransactionManagement 开启事务支持
这里相当于 xml 文件中的:
<!--开启事务管理--> <tx:annotation-driven transaction-manager="transactionManager"></tx:annotation-driven>
@Configuration //配置类
@ComponentScan(basePackages = {"com.hedon"}) //组件扫描
@EnableTransactionManagement //开启事务支持
public class SpringConfig {
# 2.3.2 配置数据源 DruidDataSource
@Bean
public DruidDataSource druidDataSource(){
DruidDataSource druidDataSource = new DruidDataSource();
druidDataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
druidDataSource.setUrl("jdbc:mysql://localhost:3306/user_db");
druidDataSource.setUsername("root");
druidDataSource.setPassword("root");
return druidDataSource;
}
# 2.3.3 配置 JdbcTemplate
/**
* 配置 JdbcTemplate
*
* 参数 dataSource : 去 IoC 容器中默认根据类型注入 dataSource
*/
@Bean
public JdbcTemplate jdbcTemplate(DataSource dataSource){
JdbcTemplate jdbcTemplate = new JdbcTemplate();
//注入数据源
jdbcTemplate.setDataSource(dataSource);
return jdbcTemplate;
}
# 2.3.4 配置事务管理器 DataSourceTransactionManager
/**
* 配置事务管理器
* @return
*/
@Bean
public DataSourceTransactionManager dataSourceTransactionManager(DataSource dataSource){
DataSourceTransactionManager dataSourceTransactionManager = new DataSourceTransactionManager();
//注入数据源
dataSourceTransactionManager.setDataSource(dataSource);
return dataSourceTransactionManager;
}
# 2.3.5 添加事务注解 @Transactional
- 添加到类上:这个类里面所有的方法都添加事务
- 添加到方法上:为这个方法添加事务
@Service
@Transactional
public class AccountService {
# 2.3.6 测试
@Test
public void test1(){
ApplicationContext context = new AnnotationConfigApplicationContext(SpringConfig.class);
AccountService accountService = context.getBean("accountService", AccountService.class);
accountService.transfer();
}
结果是一样没有问题的。
# 2.4 @Transactional 参数说明
@Service
@Transactional(propagation = Propagation.REQUIRED,
isolation = Isolation.REPEATABLE_READ,
timeout = 10,
readOnly = false,
rollbackFor = NullPointerException.class,
noRollbackFor = FileNotFoundException.class)
public class AccountService {
# 2.4.1 propagation 事务传播行为
参考博客,讲得是真滴好! (opens new window)
指明当多事务方法直接进行调用时,这个过程中的事务是如何进行管理的。
如下:add() 方法有进行事务管理,而 update() 方法没有进行事务管理,一个有事务管理的方法调用一个没有事务管理的方法的时候,事务该如何传播呢?
# 2.4.1.1 REQUIRED
@Transactional(propagation = Propagation.REQUIRED)
public void methodA() {
methodB();
// do something
}
@Transactional(propagation = Propagation.REQUIRED)
public void methodB() {
// do something
}
单独调用methodB方法时,因为当前上下文不存在事务,所以会开启一个新的事务。
调用methodA方法时,因为当前上下文不存在事务,所以会开启一个新的事务。当执行到methodB时,methodB发现当前上下文有事务,因此就加入到当前事务中来。
# 2.4.1.2 REQUIRES_NEW
- 使用 add() 方法调用 update() 方法,无论 add() 是否有事务,都创建新事务。
使用 PROPAGATION_REQUIRES_NEW
,需要使用 JtaTransactionManager
作为事务管理器。 它会开启一个新的事务。如果一个事务已经存在,则先将这个存在的事务挂起。
如下代码:
@Transactional(propagation = Propagation.REQUIRED)
public void methodA() {
doSomeThingA();
methodB();
doSomeThingB();
// do something else
}
// 事务属性为REQUIRES_NEW
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void methodB() {
// do something
}
当单独调用 methodA 时,即:
main{
methodA();
}
相当于:
main(){
TransactionManager tm = null;
try{
//获得一个JTA事务管理器
tm = getTransactionManager();
tm.begin();//开启一个新的事务
Transaction ts1 = tm.getTransaction();
doSomeThing();
tm.suspend();//挂起当前事务
try{
tm.begin();//重新开启第二个事务
Transaction ts2 = tm.getTransaction();
methodB();
ts2.commit();//提交第二个事务
} Catch(RunTimeException ex) {
ts2.rollback();//回滚第二个事务
} finally {
//释放资源
}
//methodB执行完后,恢复第一个事务
tm.resume(ts1);
doSomeThingB();
ts1.commit();//提交第一个事务
} catch(RunTimeException ex) {
ts1.rollback();//回滚第一个事务
} finally {
//释放资源
}
}
在这里,我把 ts1 称为外层事务,ts2称为内层事务。从上面的代码可以看出,ts2 与 ts1是两个独立的事务,互不相干。Ts2 是否成功并不依赖于 ts1。如果 methodA 方法在调用 methodB 方法后的 doSomeThingB 方法失败了,而 methodB 方法所做的结果依然被提交。而除了 methodB 之外的其它代码导致的结果却被回滚了
# 2.4.1.3 MANDATORY
- 支持当前事务,如果当前没有事务,就抛出异常。
@Transactional(propagation = Propagation.REQUIRED)
public void methodA() {
methodB();
// do something
}
// 事务属性为MANDATORY
@Transactional(propagation = Propagation.MANDATORY)
public void methodB() {
// do something
}
当单独调用 methodB 时,因为当前没有一个活动的事务,则会抛出异常 throw new IllegalTransactionStateException(“Transaction propagation ‘mandatory’ but no existing transaction found”);
当调用 methodA 时,methodB 则加入到 methodA 的事务中,事务地执行。
# 2.4.1.4 SUPPORTS
@Transactional(propagation = Propagation.REQUIRED)
public void methodA() {
methodB();
// do something
}
// 事务属性为SUPPORTS
@Transactional(propagation = Propagation.SUPPORTS)
public void methodB() {
// do something
}
单纯的调用 methodB 时,methodB 方法是非事务的执行的。当调用 methdA 时,methodB 则加入了 methodA 的事务中,事务地执行。
# 2.4.1.5 NOT_SUPPORTED
- 使用 add() 方法调用 update() 方法,无论 add() 是否有事务,update() 都不进行事务管理。
PROPAGATION_NOT_SUPPORTED
总是非事务地执行,并挂起任何存在的事务。使用PROPAGATION_NOT_SUPPORTED
,也需要使用JtaTransactionManager
作为事务管理器。
# 2.4.1.6 NEVER
- 以非事务方式执行,如果当前存在事务,则抛出异常。
# 2.4.1.7 NESTED
如果一个活动的事务存在,则运行在一个嵌套的事务中。 如果没有活动事务,则按 TransactionDefinition.PROPAGATION_REQUIRED
属性执行。
这是一个嵌套事务,使用 JDBC 3.0 驱动时,仅仅支持 DataSourceTransactionManager
作为事务管理器。需要 JDBC 驱动的java.sql.Savepoint
类。使用 PROPAGATION_NESTED
,还需要把 PlatformTransactionManager
的 nestedTransactionAllowed
属性设为 true
(属性值默认为false)。
这里的关键是嵌套事务,如下代码:
@Transactional(propagation = Propagation.REQUIRED)
methodA(){
doSomeThingA();
methodB();
doSomeThingB();
}
@Transactional(propagation = Propagation.NEWSTED)
methodB(){
……
}
如果单独调用 methodB 方法,则按 REQUIRED 属性执行。如果调用 methodA 方法,相当于下面的效果:
main(){
Connection con = null;
Savepoint savepoint = null;
try{
con = getConnection();
con.setAutoCommit(false);
doSomeThingA();
savepoint = con2.setSavepoint();
try{
methodB();
} catch(RuntimeException ex) {
con.rollback(savepoint);
} finally {
//释放资源
}
doSomeThingB();
con.commit();
} catch(RuntimeException ex) {
con.rollback();
} finally {
//释放资源
}
}
当 methodB 方法调用之前,调用 setSavepoint 方法,保存当前的状态到 savepoint。如果 methodB 方法调用失败,则恢复到之前保存的状态。但是需要注意的是,这时的事务并没有进行提交,如果后续的代码 (doSomeThingB()方法) 调用失败,则回滚包括 methodB 方法的所有操作。嵌套事务一个非常重要的概念就是内层事务依赖于外层事务。外层事务失败时,会回滚内层事务所做的动作。而内层事务操作失败并不会引起外层事务的回滚。
# △ 比较PROPAGATION_NESTED 与PROPAGATION_REQUIRES_NEW的区别
# 相似点
都像一个嵌套事务,如果不存在一个活动的事务,都会开启一个新的事务。
# 不同点
① 使用 PROPAGATION_REQUIRES_NEW 时,内层事务与外层事务就像两个独立的事务一样,一旦内层事务进行了提交后,外层事务不能对其进行回滚。两个事务互不影响。两个事务不是一个真正的嵌套事务。同时它需要JTA事务管理器的支持。
② 使用PROPAGATION_NESTED 时,外层事务的回滚可以引起内层事务的回滚。而内层事务的异常并不会导致外层事务的回滚,它是一个真正的嵌套事务。DataSourceTransactionManager 使用 savepoint 支持 PROPAGATION_NESTED 时,需要 JDBC 3.0以上驱动及 1.4 以上的JDK版本支持。其它的 JTATrasactionManager 实现可能有不同的支持方式。
③ PROPAGATION_REQUIRES_NEW 启动一个新的, 不依赖于环境的 “内部” 事务. 这个事务将被完全 commited 或 rolled back 而不依赖于外部事务, 它拥有自己的隔离范围,自己的锁, 等等. 当内部事务开始执行时, 外部事务将被挂起, 内务事务结束时, 外部事务将继续执行。
④ 另一方面, PROPAGATION_NESTED 开始一个 “嵌套的” 事务, 它是已经存在事务的一个真正的子事务. 嵌套事务开始执行时, 它将取得一个 savepoint. 如果这个嵌套事务失败, 我们将回滚到此 savepoint. 潜套事务是外部事务的一部分, 只有外部事务结束后它才会被提交。
# 最大区别
PROPAGATION_REQUIRES_NEW 完全是一个新的事务, 而 PROPAGATION_NESTED 则是外部事务的子事务, 如果外部事务 commit, 嵌套事务也会被 commit, 这个规则同样适用于 roll back。
# 2.4.2 isolation 事务隔离级别
# 2.4.2.1 事务的隔离性
多事务操作之间不会产生影响。
# 2.4.2.2 三个读问题
脏读
一个未提交的事务读取到另一个未提交的事务的数据。
如上:东方不败想从5000改到100,而岳不群想从5000改到60000。这个时候岳不群先改了,然后东方不败读取到数据已经改成60000了,所以东方不败就会继续在60000的基础上进行修改。但是这个时候,岳不群的事务并没有进行提交,而且进行了事务回滚,所以真实的数据现在还是5000,而东方不败操作的数据是60000。这就叫脏读。
不可重复读
一个未提交事务读取到另一提交事务修改数据。
如上:东方不败先读取到数据是5000,想对数据进行操作,但是这个时候岳不群已经将数据改成900了。而东方不败又检测到了数据已经改成900了,读两次,数据不一致,这就是不可重复读(因为不知道再读的话是不是又会不一样了)。
虚(幻)读
一个未提交事务读取到另一提交事务增加的数据。
# 2.4.2.3 解决读问题 —— 设置事务的隔离性
isolation 属性值 | 意思 | 脏读 | 不可重复读 | 虚读 | 作用 |
---|---|---|---|---|---|
READ UNCOMMITTED | 读未提交 | 有 | 有 | 有 | 效率高,但是啥也避免不了 |
READ COMMITTED | 读已提交 | 无 | 有 | 有 | 常用,可避免脏读 |
REPEATABLE READ | 可重复读 | 无 | 无 | 有 | 可以用在非 insert 方法上 |
SERIALIZABLE | 串行化 | 无 | 无 | 无 | 三个问题都解决了,但效率低 |
DEFAULT | 使用数据库默认 | MySQL 的话就是 REPEATABLE READ |
# 2.4.3 timeout 超时时间
事务需要在一定时间内进行提交,如果不提交进行回滚。
默认时间是 -1(永不超时),设置时间以秒单位进行计算。
# 2.4.4 readOnly 是否只读
readOnly 默认值 false,表示可以查询,可以添加修改删除操作。
设置 readOnly 值是 true,设置成 true 之后,只能查询。
# 2.4.5 rollbackFor 回滚
设置出现哪些异常进行事务回滚。
# 2.4.6 noRollbackFor 不回滚
设置出现哪些异常不进行事务回滚。