一、事务属性
1、事务的传播行为
① 概念
事务方法 A 直接或间接调用事务方法 B,事务方法 A 已经开启的事务如何传播给方法 B 来使用。
② 实际开发中的场景举例
调用的目标方法带有事务,后面的 AOP 的通知方法也需要事务,它们是在同一个线程内的,存在事务传播行为。
③ 设置传播行为的属性
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
把 updateTwice()和 updateSingle()这两个方法上都使用下面的设置:
1
| @Transactional(readOnly = false, propagation = Propagation.REQUIRED)
|
效果:两个方法的操作都没有生效,updateSingle()方法回滚,导致 updateTwice()也一起被回滚
因为他们都在 propagationService.testPropagation()方法开启的同一个事务内。
[2]情况 2:测试 REQUIRES_NEW
把 updateTwice()和 updateSingle()这两个方法上都使用下面的设置:
1
| @Transactional(readOnly = false, propagation = Propagation.REQUIRES_NEW)
|
结果:
- updateTwice()没有受影响,成功实现了更新
- updateSingle()自己回滚
原因:上面两个方法各自运行在自己的事务中。
2、事务的隔离级别
① 测试方法说明
② 情景代码补充
[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)
设置方式
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);
}
|
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>
<aop:pointcut id="txPointCut" expression="execution(* *..*Service.*(..))"/> <aop:advisor advice-ref="txAdvice" pointcut-ref="txPointCut"/> </aop:config>
<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 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 导入时需要注意名称空间的值
整个后端学习的经纬体系
纵向需要解决的问题,横向的解决办法。