Spring 事务

本文最后更新于:2024年9月8日 晚上

Spring 事务

pom.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<!--Spring Boot-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>

<!--Spring-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
<version>5.3.13</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>5.3.13</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>5.3.13</version>
</dependency>

Spring 事务属性

  • Spring事务属性对应TransactionDefinition类里面的各个方法,TransactionDefinition类方法如下所示:
1
2
3
4
5
6
7
8
9
10
11
12
13
public interface TransactionDefinition {
// 返回事务传播行为。
int getPropagationBehavior();
// 返回事务的隔离级别,事务管理器根据它来控制另外一个事务可以看到本事务内的哪些数据。
int getIsolationLevel();
// 事务超时时间,事务必须在多少秒之内完成。
int getTimeout();
// 事务是否只读,事务管理器能够根据这个返回值进行优化,确保事务是只读的。
boolean isReadOnly();
// 事务名字。
@Nullable
String getName();
}
  • 事务属性可以理解成事务的一些基本配置,描述了事务策略如何应用到方法上,事务属性包含了5个方面:传播行为,隔离规则,回滚规则,事务超时,是否只读。

事务传播特性

  • 当事务方法被另一个事务方法调用时,必须指定事务应该如何传播,例如:方法可能继续在现有事务中运行,也可能开启一个新事务,并在自己的事务中运行,Spring定义了七种传播行为:
传播行为 含义
TransactionDefinition.PROPAGATION_REQUIRED 如果当前没有事务,就新建一个事务,如果已经存在一个事务,则加入到这个事务中
TransactionDefinition.PROPAGATION_SUPPORTS 支持当前事务,如果当前没有事务,就以非事务方式执行,
TransactionDefinition.PROPAGATION_MANDATORY 表示该方法必须在事务中运行,如果当前事务不存在,则会抛出一个异常
TransactionDefinition.PROPAGATION_REQUIRED_NEW 表示当前方法必须运行在它自己的事务中,一个新的事务将被启动,如果存在当前事务,在该方法执行期间,当前事务会被挂起,
TransactionDefinition.PROPAGATION_NOT_SUPPORTED 表示该方法不应该运行在事务中,如果当前存在事务,就把当前事务挂起,
TransactionDefinition.PROPAGATION_NEVER 表示当前方法不应该运行在事务上下文中,如果当前正有一个事务在运行,则会抛出异常
TransactionDefinition.PROPAGATION_NESTED 如果当前存在事务,则在嵌套事务内执行,如果当前没有事务,则执行与PROPAGATION_REQUIRED类似的操作,
  • Spring 默认的事务传播行为是PROPAGATION_REQUIRED,它适合于绝大多数的情况。

隔离级别

  • 隔离级别定义了一个事务可能受其他并发事务影响的程度。
  • 并发状态下可能产生: 脏读,不可重复读,幻读的情况,因此我们需要将事务与事务之间隔离,根据隔离的方式来避免事务并发状态下脏读,不可重复读,幻读的产生,Spring中定义了五种隔离规则:
隔离级别 含义 脏读 不可重复读 幻读
TransactionDefinition.ISOLATION_DEFAULT 使用后端数据库默认的隔离级别
TransactionDefinition.ISOLATION_READ_UNCOMMITTED 允许读取尚未提交的数据变更(最低的隔离级别)
TransactionDefinition.ISOLATION_READ_COMMITTED 允许读取并发事务已经提交的数据
TransactionDefinition.ISOLATION_REPEATABLE_READ 对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改
TransactionDefinition.ISOLATION_SERIALIZABLE 最高的隔离级别,完全服从ACID的隔离级别,也是最慢的事务隔离级别,因为它通常是通过完全锁定事务相关的数据库表来实现的
  • 注意:ISOLATION_SERIALIZABLE隔离规则类型在开发中很少用到,如果使用了ISOLATION_SERIALIZABLE规则,A,B两个事务操作同一个数据表并发过来了,A先执行,A事务这个时候会把表给锁住,B事务执行的时候直接报错。
  • 事务隔离级别与锁的关系。
    • 事务隔离级别为ISOLATION_READ_UNCOMMITTED时,写数据只会锁住相应的行。
    • 事务隔离级别为可ISOLATION_REPEATABLE_READ时,如果检索条件有索引(包括主键索引)的时候,默认加锁方式是next-key锁,如果检索条件没有索引,更新数据时会锁住整张表,一个间隙被事务加了锁,其他事务是不能在这个间隙插入记录的,这样可以防止幻读。
    • 事务隔离级别为ISOLATION_SERIALIZABLE时,读写数据都会锁住整张表。
    • 隔离级别越高,越能保证数据的完整性和一致性,但是对并发性能的影响也就越大。

回滚规则

  • 事务回滚规则定义了哪些异常会导致事务回滚而哪些不会。
  • 默认情况下,只有RuntimeException和Error类型的异常会导致事务回滚,而在遇到CheckedException异常时不会回滚。
  • 可以声明事务在遇到特定的CheckedException时像遇到RuntimeException那样回滚,也还可以声明事务遇到特定的异常不回滚,即使这些异常是RuntimeException

事务超时

  • 为了使应用程序很好地运行,事务不能运行太长的时间,因为事务可能涉及对后端数据库的锁定,也会占用数据库资源,事务超时就是事务的一个定时器,在特定时间内事务如果没有执行完毕,那么就会自动回滚,而不是一直等待其结束。

是否只读

  • 如果在一个事务中所有关于数据库的操作都是只读的,也就是说,这些操作只读取数据库中的数据,而并不更新数据, 这个时候我们应该给该事务设置只读属性,这样可以帮助数据库引擎优化事务,提升效率。

Spring 事务管理

  • Spring 为事务管理提供了丰富的功能支持,Spring 事务管理分为编码式和声明式的两种方式:
    • 编程式事务:允许用户在代码中精确定义事务的边界,编程式事务管理使用TransactionTemplate或者直接使用底层的PlatformTransactionManager,对于编程式事务管理,spring推荐使用TransactionTemplate
    • 声明式事务:基于AOP,有助于用户将操作与事务规则进行解耦,其本质是对方法前后进行拦截,然后在目标方法开始之前创建或者加入一个事务,在执行完目标方法之后根据执行情况提交或者回滚事务,声明式事务管理也有两种常用x方式,一种是在配置文件(xml)中做相关的事务规则声明,另一种是基于@Transactional注解的方式。

注意:显然声明式事务管理要优于编程式事务管理,这正是spring倡导的非侵入式的开发方式,声明式事务管理使业务代码不受污染,一个普通的POJO对象,只要加上注解就可以获得完全的事务支持,和编程式事务相比,声明式事务唯一不足地方是,后者的最细粒度只能作用到方法级别,无法做到像编程式事务那样可以作用到代码块级别,但是即便有这样的需求,也存在很多变通的方法,比如,可以将需要进行事务管理的代码块独立为方法等等。

使用配置文件实现声明式事务

  • 将事务管理作为横切关注点,通过aop方法模块化,Spring中通过Spring AOP框架支持声明式事务管理。

事务管理器

  • 无论使用Spring的哪种事务管理策略(编程式或者声明式)事务管理器都是必须的。
  • 事务管理器就是 Spring的核心事务管理抽象,管理封装了一组独立于技术的方法。
  1. 配置事务管理器。
1
2
3
4
5
<!-- 配置事务管理器 -->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<!-- 注入数据库连接池 -->
<property name="dataSource" ref="dataSource"/>
</bean>
  1. 配置好事务管理器后我们需要去配置事务的通知。
1
2
3
4
5
6
7
8
9
10
11
12
<!--配置事务通知-->
<tx:advice id="txAdvice" transaction-manager="transactionManager">
<tx:attributes>
<!--配置哪些方法使用什么样的事务,配置事务的传播特性-->
<tx:method name="add" propagation="REQUIRED"/>
<tx:method name="delete" propagation="REQUIRED"/>
<tx:method name="update" propagation="REQUIRED"/>
<tx:method name="search*" propagation="REQUIRED"/>
<tx:method name="get" read-only="true"/>
<tx:method name="*" propagation="REQUIRED"/>
</tx:attributes>
</tx:advice>
  1. 配置AOP
1
2
3
4
5
<!--配置aop织入事务-->
<aop:config>
<aop:pointcut id="txPointcut" expression="execution(* com.test.mapper.*.*(..))"/>
<aop:advisor advice-ref="txAdvice" pointcut-ref="txPointcut"/>
</aop:config>
  1. 测试。
1
2
3
4
5
6
7
@Test
public void test2(){
ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
UserMapper mapper = (UserMapper) context.getBean("userMapper");
List<User> user = mapper.selectUser();
System.out.println(user);
}

使用注解实现声明式事务

@Transactional属性

  • @Transactional注解里面的各个属性和Spring的事务属性里面是一一对应的,用来设置事务的传播行为,隔离规则,回滚规则,事务超时,是否只读。
value/transactionManager
  • 大多数项目只需要一个事务管理器,然而,有些项目为了提高效率,或者有多个完全不同又不相干的数据源,从而使用了多个事务管理器,当配置了多个事务管理器时,可以使用该属性指定选择哪个事务管理器。
  • 首先定义多个transactional manager,并为qualifier属性指定不同的值,然后在需要使用@Transactional注解的时候指定TransactionManager的qualifier属性值或者直接使用bean名称。
1
2
3
4
5
6
7
8
9
10
11
<tx:annotation-driven/>

<bean id="transactionManager1" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="datasource1"></property>
<qualifier value="datasource1Tx"/>
</bean>

<bean id="transactionManager2" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="datasource2"></property>
<qualifier value="datasource2Tx"/>
</bean>
1
2
3
4
5
6
7
8
9
public class TransactionalService {

@Transactional("datasource1Tx")
public void setSomethingInDatasource1() { ... }

@Transactional("datasource2Tx")
public void doSomethingInDatasource2() { ... }

}
propagation
  • propagation用于指定事务的传播行为,默认值为 REQUIRED
1
@Transactional(propagation = REQUIRED)
isolation
  • isolation用于指定事务的隔离规则,默认值为DEFAULT
1
@Transactional(isolation = READ_COMMITTED)
timeout
  • timeout用于设置事务的超时属性。
1
@Transactional(timeout = 10000)
readOnly
  • readOnly用于设置事务是否只读属性。
1
@Transactional(readOnly = true)
rollbackFor,rollbackForClassName,noRollbackFor,noRollbackForClassName
  • rollbackFor,rollbackForClassName用于设置那些异常需要回滚。
  • noRollbackFor,noRollbackForClassName用于设置那些异常不需要回滚,他们就是在设置事务的回滚规则。
1
@Transactional(rollbackFor = IOException.class, norollbackFor = NullPointerException.class)

@Transactional的使用

  • @Transactional注解可以作用于接口,接口方法,类以及类方法上,当作用于类上时,该类的所有 public 方法将都具有该类型的事务属性,也可以在方法级别使用该标注来覆盖类级别的定义。
  • 虽然@Transactional 注解可以作用于接口,接口方法,类以及类方法上,但是 Spring 建议不要在接口或者接口方法上使用该注解,因为这只有在使用基于接口的代理时它才会生效。
  • @Transactional注解应该只被应用到 public 方法上,这是由Spring AOP的本质决定的,如果你在 protected,private 或者默认可见性的方法上使用 @Transactional 注解,这将被忽略,也不会抛出任何异常。
  • 默认情况下,只有来自外部的方法调用才会被AOP代理捕获,也就是,类内部方法调用本类内部的其他方法并不会引起事务行为,即使被调用方法使用@Transactional注解进行修饰。
  • 要明确事务的作用范围,有@Transactional的函数调用有@Transactional的函数的时候,注意区分进入第二个函数的时候是新的事务,还是沿用之前的事务,稍不注意就会抛UnexpectedRollbackException异常。
同一个类中函数相互调用
  • 同一个类AClass中,有两个函数aFunction,aInnerFunction,aFunction调用aInnerFunction,而且aFunction函数会被外部调用。

情况1: aFunction添加了@Transactional注解,aInnerFunction函数没有添加,aInnerFunction抛异常。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class AClass {

@Transactional(rollbackFor = Exception.class)
public void aFunction() {
//todo:数据库操作A(增,删,该)
aInnerFunction(); // 调用内部没有添加@Transactional注解的函数。
}

private void aInnerFunction() {
//todo:操作数据B(做了增,删,改操作)
throw new RuntimeException("函数执行有异常!");
}

}
  • 结果:两个函数操作的数据都会回滚。

情况2:两个函数都添加了@Transactional注解,aInnerFunction抛异常。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class AClass {

@Transactional(rollbackFor = Exception.class)
public void aFunction() {
//todo:数据库操作A(增,删,该)
aInnerFunction(); // 调用内部没有添加@Transactional注解的函数。
}

@Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class)
private void aInnerFunction() {
//todo:操作数据B(做了增,删,改操作)
throw new RuntimeException("函数执行有异常!");
}

}
  • 结果:同第一种情况一样,两个函数对数据库操作都会回滚,因为同一个类中函数相互调用的时候,内部函数添加@Transactional注解无效,@Transactional注解只有外部调用才有效。

情况3: aFunction不添加注解,aInnerFunction添加注解,aInnerFunction抛异常。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class AClass {

public void aFunction() {
//todo:数据库操作A(增,删,该)
aInnerFunction(); // 调用内部没有添加@Transactional注解的函数。
}

@Transactional(rollbackFor = Exception.class)
protected void aInnerFunction() {
//todo:操作数据B(做了增,删,改操作)
throw new RuntimeException("函数执行有异常!");
}

}
  • 结果:两个函数对数据库的操作都不会回滚,因为内部函数@Transactional注解添加和没添加一样。

情况4:aFunction添加了@Transactional注解,aInnerFunction函数没有添加,aInnerFunction抛异常,不过在aFunction里面把异常抓出来了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class AClass {

@Transactional(rollbackFor = Exception.class)
public void aFunction() {
//todo:数据库操作A(增,删,该)
try {
aInnerFunction(); // 调用内部没有添加@Transactional注解的函数。
} catch (Exception e) {
e.printStackTrace();
}
}

private void aInnerFunction() {
//todo:操作数据B(做了增,删,改操作)
throw new RuntimeException("函数执行有异常!");
}

}
  • 结果:两个函数里面的数据库操作都成功,事务回滚的动作发生在当有@Transactional注解函数有对应异常抛出时才会回滚。
不同类中函数相互调用
  • 两个类AClass,BClass,AClass类有aFunction,BClass类有bFunction,AClass类aFunction调用BClass类bFunction,最终会在外部调用AClass类的aFunction

情况1:aFunction添加注解,bFunction不添加注解,bFunction抛异常。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@Service()
public class AClass {

private BClass bClass;

@Autowired
public void setbClass(BClass bClass) {
this.bClass = bClass;
}

@Transactional(rollbackFor = Exception.class)
public void aFunction() {
//todo:数据库操作A(增,删,该)
bClass.bFunction();
}

}

@Service()
public class BClass {

public void bFunction() {
//todo:数据库操作A(增,删,该)
throw new RuntimeException("函数执行有异常!");
}
}
  • 结果:两个函数对数据库的操作都回滚了。

情况2:aFunction,bFunction两个函数都添加注解,bFunction抛异常。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@Service()
public class AClass {

private BClass bClass;

@Autowired
public void setbClass(BClass bClass) {
this.bClass = bClass;
}

@Transactional(rollbackFor = Exception.class)
public void aFunction() {
//todo:数据库操作A(增,删,该)
bClass.bFunction();
}

}

@Service()
public class BClass {

@Transactional(rollbackFor = Exception.class)
public void bFunction() {
//todo:数据库操作A(增,删,该)
throw new RuntimeException("函数执行有异常!");
}
}
  • 结果:两个函数对数据库的操作都回滚了,两个函数里面用的还是同一个事务,这种情况下,你可以认为事务rollback了两次,两个函数都有异常。

情况3:aFunction,bFunction两个函数都添加注解,bFunction抛异常,aFunction抓出异常。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
@Service()
public class AClass {

private BClass bClass;

@Autowired
public void setbClass(BClass bClass) {
this.bClass = bClass;
}

@Transactional(rollbackFor = Exception.class)
public void aFunction() {
//todo:数据库操作A(增,删,该)
try {
bClass.bFunction();
} catch (Exception e) {
e.printStackTrace();
}
}

}

@Service()
public class BClass {

@Transactional(rollbackFor = Exception.class)
public void bFunction() {
//todo:数据库操作A(增,删,该)
throw new RuntimeException("函数执行有异常!");
}
}
  • 结果:两个函数数据库操作都没成功,而且还抛出异常:org.springframework.transaction.UnexpectedRollbackException: Transaction rolled back because it has been marked as rollback-only
  • 可以这么理解,两个函数用的是同一个事务,bFunction函数抛了异常,调了事务的rollback函数,事务被标记了只能rollback了,程序继续执行,aFunction函数里面把异常给抓出来了,这个时候aFunction函数没有抛出异常,既然你没有异常那事务就需要提交,会调事务的commit函数,而之前已经标记了事务只能rollback-only(以为是同一个事务),直接就抛异常了,不让调了。

情况4:aFunction,bFunction两个函数都添加注解,bFunction抛异常,aFunction抓出异常,这里要注意bFunction函数@Transactional注解我们是有变化的,加了一个参数propagation = Propagation.REQUIRES_NEW,控制事务的传播行为,表明是一个新的事务,其实情况3就是来解决情况2的问题的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
@Service()
public class AClass {

private BClass bClass;

@Autowired
public void setbClass(BClass bClass) {
this.bClass = bClass;
}

@Transactional(rollbackFor = Exception.class)
public void aFunction() {
//todo:数据库操作A(增,删,该)
try {
bClass.bFunction();
} catch (Exception e) {
e.printStackTrace();
}
}

}

@Service()
public class BClass {

@Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class)
public void bFunction() {
//todo:数据库操作A(增,删,该)
throw new RuntimeException("函数执行有异常!");
}
}
  • 结果:bFunction函数里面的操作回滚了,aFunction里面的操作成功了,有了前面情况3的理解,这种情况也很好解释,两个函数不是同一个事务了。