一、事务属性

1、事务的传播行为

① 概念

事务方法 A 直接或间接调用事务方法 B,事务方法 A 已经开启的事务如何传播给方法 B 来使用。

images

② 实际开发中的场景举例

调用的目标方法带有事务,后面的 AOP 的通知方法也需要事务,它们是在同一个线程内的,存在事务传播行为。

images

③ 设置传播行为的属性

1
@Transactional(readOnly = false, propagation = Propagation.REQUIRED)
  • 在@Transactional 注解中使用 propagation 属性设置传播行为
  • 在 Propagation 枚举类中封装可选的传播行为
    • REQUIRED:默认值
      • 当前方法必须工作在事务中。
      • 如果在当前方法执行前,线程上没有已经开启的事务,那么开启新事务,并在这个事务中运行。
      • 如果在当前方法执行前,线程上有已经开启的事务,那么就在这个已经开启的事务中运行。此时有可能和其他方法共用同一个事务。
      • 和其他操作共用事务的隐患是:其他操作回滚,当前自己的操作也会跟着一起被回滚。
    • REQUIRES_NEW建议使用
      • 当前方法必须工作在事务中。
      • 不论当前方法运行前,线程上是否已经开启了事务,都会开启新的事务,并在这个事务中运行。
      • 好处:保证当前方法在事务中运行,而且是自己开的事务,这样就不会受其他方法回滚的影响。

④ 测试代码

在 EmpServiceImpl 中增加了一个方法:updateSingle()

1
2
3
4
5
6
7
8
9
@Override
@Transactional(readOnly = false)
public void updateSingle() {

Integer empId = 7;
String empName = "CCC";

empDao.updateEmpName(empId, empName);
}

创建 PropagationService 接口

1
2
3
4
5
6
7
package com.atguigu.tx.component.service.api;

public interface PropagationService {

void testPropagation();

}

创建 PropagationServiceImpl 类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package com.atguigu.tx.component.service.impl;

@Service
public class PropagationServiceImpl implements PropagationService {

@Autowired
private EmpService empService;

@Override
@Transactional
public void testPropagation() {
empService.updateTwice();
empService.updateSingle();
}
}

juni 测试代码:

1
2
3
4
5
6
7
@Autowired
private PropagationService propagationService;

@Test
public void testPropagation() {
propagationService.testPropagation();
}

⑤ 测试用例

让 empService.updateSingle()会抛出异常。

[1]情况 1:测试 REQUIRED

images

images

把 updateTwice()和 updateSingle()这两个方法上都使用下面的设置:

1
@Transactional(readOnly = false, propagation = Propagation.REQUIRED)

效果:两个方法的操作都没有生效,updateSingle()方法回滚,导致 updateTwice()也一起被回滚

因为他们都在 propagationService.testPropagation()方法开启的同一个事务内。

[2]情况 2:测试 REQUIRES_NEW

images

images

把 updateTwice()和 updateSingle()这两个方法上都使用下面的设置:

1
@Transactional(readOnly = false, propagation = Propagation.REQUIRES_NEW)

结果:

  • updateTwice()没有受影响,成功实现了更新
  • updateSingle()自己回滚

原因:上面两个方法各自运行在自己的事务中。

2、事务的隔离级别

① 测试方法说明

images

② 情景代码补充

[1]EmpDao 补充

1
String selectEmpNameById(Integer empId);

[2]EmpDaoImpl 补充

1
2
3
4
5
6
7
@Override
public String selectEmpNameById(Integer empId) {

String sql = "select emp_name from t_emp where emp_id=?";

return jdbcTemplate.queryForObject(sql, String.class, empId);
}

[3]EmpService 补充

1
2
3
String getEmpNameById(Integer empId);

void updateEmpName(Integer empId, String empName) throws FileNotFoundException;

[4]EmpServiceImpl 补充

1
2
3
4
5
6
7
8
9
10
11
@Override
@Transactional(isolation = Isolation.READ_UNCOMMITTED)
public String getEmpNameById(Integer empId) {
return empDao.selectEmpNameById(empId);
}

@Override
@Transactional(readOnly = false, propagation = Propagation.REQUIRES_NEW, isolation = Isolation.READ_UNCOMMITTED)
public void updateEmpName(Integer empId, String empName) {
empDao.updateEmpName(empId, empName);
}

③ 设置方式(isolation)

在@Transactional 注解中,使用isolation 属性设置事务的隔离级别。可选值包括

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public enum Isolation {
//默认
DEFAULT(-1),
//读未提交
READ_UNCOMMITTED(1),
//读已提交
READ_COMMITTED(2),
//可重复读
REPEATABLE_READ(4),
//序列化
SERIALIZABLE(8);

private final int value;

private Isolation(int value) {
this.value = value;
}

public int value() {
return this.value;
}
}

④ 测试方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Test
public void testReadEmpName() {
String empName = empService.getEmpNameById(4);
System.out.println("empName = " + empName);
}

@Test
public void testWriteEmpName() {

Integer empId = 4;
String empName = "UUU";

empService.updateEmpName(empId, empName);
}

在读和写方法中分别设置断点,并把两个方法都运行起来

  • 在读操作实际执行前让程序停住
  • 执行写操作
  • 在写操作提交或回滚前执行读操作
  • 查看读操作查询到的数据
    • 读未提交:会看到读取了写操作尚未提交的修改
    • 读已提交:会看到写操作尚未提交的修改被无视了

3、事务回滚的异常(rollbackFor)

images

设置方式

1
2
3
4
5
6
7
8
9
@Override
@Transactional(readOnly = false,
propagation = Propagation.REQUIRES_NEW,
isolation = Isolation.READ_COMMITTED,
rollbackFor = Exception.class
)
public void updateEmpName(Integer empId, String empName) {
empDao.updateEmpName(empId, empName);
}

在@Transactional 注解中,使用rollbackFor 属性设置事务回滚的异常,使用noRollbackFor 属性设置事务不回滚的异常。

实际开发时通常也建议设置为根据 Exception 异常回滚

4、只读属性(readOnly)

一个事务如果是做查询操作,可以设置为只读,此时数据库可以针对查询操作来做优化,有利于提高性能。

1
@Transactional(readOnly = true)

如果是针对增删改方法设置只读属性,则会抛出下面异常:

1
2
3
表面的异常信息:TransientDataAccessResourceException: PreparedStatementCallback

根本原因:SQLException: Connection is read-only. Queries leading to data modification are not allowed(连接是只读的。查询导向数据的修改是不允许的。)

实际开发时建议把查询操作设置为只读

5、超时属性(timeout)

一个数据库操作有可能因为网络或死锁等问题卡住很长时间,从而导致数据库连接等资源一直处于被占用的状态。

所以我们可以在@Transactional 注解中设置一个超时属性 timeout,让一个事务执行太长时间后,主动回滚。事务结束后把资源释放出来。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Override
@Transactional(readOnly = false,
propagation = Propagation.REQUIRES_NEW,
isolation = Isolation.READ_COMMITTED,
rollbackFor = Exception.class,
timeout = 10
)
public void updateEmpName(Integer empId, String empName) throws FileNotFoundException, InterruptedException {

TimeUnit.SECONDS.sleep(15);

empDao.updateEmpName(empId, empName);

// 故意抛出一个编译时异常
// new FileInputStream("stupid.txt");
}

6、总结

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Transactional(

// rollbackFor属性:设置事务回滚的异常
rollbackFor = Exception.class,

// noRollbackFor属性:设置事务不回滚的异常
noRollbackFor = FileNotFoundException.class,

// isolation属性:设置事务的隔离级别
isolation = Isolation.READ_COMMITTED,

// readOnly属性:设置事务的只读属性,只能为纯查询操作设置只读
readOnly = false,

// propagation属性:设置事务的传播行为
// 默认值:REQUIRED(要求必须在事务中运行,如果当前线程上检测到已有事务,则在已有事务内运行;如果没有已开事务,则开启新事务,在新事务内运行)
// 建议值:REQUIRES_NEW(要求必须在事务中运行,不论是否存在已开事务都会开启新事务,并在新事务内运行)
propagation = Propagation.REQUIRES_NEW,

// timeout属性:超时属性(测试时先睡觉再执行SQL才能看到效果)
timeout = 5

二、基于 XML 的声明式事务

1、搭建环境

和前面基于注解的一样。

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
<!-- =================下面是和事务相关的配置================= -->
<!-- 配置事务管理器 -->
<bean class="org.springframework.jdbc.datasource.DataSourceTransactionManager" id="transactionManager">
<!-- 装配数据源 -->
<property name="dataSource" ref="druidDataSource"/>
</bean>

<!-- 配置事务切面 -->
<aop:config>
<!-- 配置事务切面的切入点表达式,表达出哪些方法需要事务,即execution(* *..*Service.*(..))-->

<!--第一个*表示权限修饰符+返回值类型,第二个*表示包名任意 -->
<!--..表示包的层次深度任意,第三个*表示类名任意,*Service就表示以Service结尾的类名 -->
<!--最后的(..)表示方法名任意,方法的参数任意 -->
<aop:pointcut id="txPointCut" expression="execution(* *..*Service.*(..))"/>
<!-- 将事务通知和事务切入点表达式关联起来 -->
<aop:advisor advice-ref="txAdvice" pointcut-ref="txPointCut"/>
</aop:config>


<!-- 配置事务通知 -->
<!-- tx:advice标签:配置事务通知 -->
<!-- id属性:设置当前通知的id,便于后面引用 -->
<!-- transaction-manager属性:用于关联事务管理器,默认值是transactionManager -->
<!-- 如果事务管理器的bean的id正好是transactionManager,则transaction-manager属性可以省略 -->
<tx:advice id="txAdvice" transaction-manager="transactionManager">

<!-- 配置事务属性 -->
<tx:attributes>

<!-- 但是!!!,即使配置切入点表达式能够定位到需要事务的方法,仍然需要设置事务属性,否则需要事务的方法将不会被提供事务服务 -->

<!-- 指定具体的事务方法 -->
<tx:method name="get*" read-only="true"/>
<tx:method name="query*" read-only="true"/>
<tx:method name="count*" read-only="true"/>

<!-- tx:method标签:给具体方法配置事务属性 -->
<!-- 增删改方法 -->
<tx:method name="update*" rollback-for="java.lang.Exception" propagation="REQUIRES_NEW"/>
<tx:method name="insert*" rollback-for="java.lang.Exception" propagation="REQUIRES_NEW"/>
<tx:method name="delete*" rollback-for="java.lang.Exception" propagation="REQUIRES_NEW"/>

</tx:attributes>
</tx:advice>

3、注意

虽然切入点表达式已经定位到了所有需要事务的方法,但是在**<tx:attributes>**中还是必须配置事务属性。这两个条件缺一不可。缺少任何一个条件,方法都加不上事务。
另外,tx:advice 导入时需要注意名称空间的值

images

整个后端学习的经纬体系

纵向需要解决的问题,横向的解决办法。

images

三:声明式事务思维导图: