六、Spring 声明式事务 6.1 声明式事务概念 6.1.1 编程式事务 编程式事务是指手动编写程序来管理事务,即通过编写代码的方式直接控制事务的提交和回滚。在 Java 中,通常使用事务管理器(如 Spring 中的 PlatformTransactionManager
)来实现编程式事务。
编程式事务的主要优点是灵活性高,可以按照自己的需求来控制事务的粒度、模式等等。但是,编写大量的事务控制代码容易出现问题,对代码的可读性和可维护性有一定影响。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 Connection conn = ...; try { conn.setAutoCommit(false ); conn.commit(); }catch (Exception e){ conn.rollBack(); }finally { conn.close(); }
编程式的实现方式存在缺陷:
细节没有被屏蔽:具体操作过程中,所有细节都需要程序员自己来完成,比较繁琐。
代码复用性不高:如果没有有效抽取出来,每次实现功能都需要自己编写代码,代码就没有得到复用。
6.1.2 声明式事务 声明式事务是指使用注解或 XML 配置的方式来控制事务的提交和回滚。
开发者只需要添加配置即可, 具体事务的实现由第三方框架实现,避免我们直接进行事务操作!
使用声明式事务可以将事务的控制和业务逻辑分离开来,提高代码的可读性和可维护性。
区别:
编程式事务需要手动编写代码来管理事务
而声明式事务可以通过配置文件或注解来控制事务。
6.1.3 Spring事务管理器
Spring声明式事务对应依赖
spring-tx: 包含声明式事务实现的基本规范(事务管理器规范接口和事务增强等等)
spring-jdbc: 包含DataSource方式事务管理器实现类DataSourceTransactionManager
spring-orm: 包含其他持久层框架的事务管理器实现类例如:Hibernate/Jpa等
Spring声明式事务对应事务管理器接口
我们现在要使用的事务管理器是org.springframework.jdbc.datasource.DataSourceTransactionManager,将来整合 JDBC方式、JdbcTemplate方式、Mybatis方式的事务实现!
DataSourceTransactionManager类中的主要方法:
- doBegin():开启事务
- doSuspend():挂起事务
- doResume():恢复挂起的事务
- doCommit():提交事务
- doRollback():回滚事务
6.2 基于注解的声明式事务 6.2.1 准备工作
准备项目
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 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 <dependencies > <dependency > <groupId > org.springframework</groupId > <artifactId > spring-context</artifactId > <version > 6.0.6</version > </dependency > <dependency > <groupId > org.junit.jupiter</groupId > <artifactId > junit-jupiter-api</artifactId > <version > 5.3.1</version > </dependency > <dependency > <groupId > org.springframework</groupId > <artifactId > spring-test</artifactId > <version > 6.0.6</version > <scope > test</scope > </dependency > <dependency > <groupId > jakarta.annotation</groupId > <artifactId > jakarta.annotation-api</artifactId > <version > 2.1.1</version > </dependency > <dependency > <groupId > mysql</groupId > <artifactId > mysql-connector-java</artifactId > <version > 8.0.25</version > </dependency > <dependency > <groupId > com.alibaba</groupId > <artifactId > druid</artifactId > <version > 1.2.8</version > </dependency > <dependency > <groupId > org.springframework</groupId > <artifactId > spring-jdbc</artifactId > <version > 6.0.6</version > </dependency > <dependency > <groupId > org.springframework</groupId > <artifactId > spring-tx</artifactId > <version > 6.0.6</version > </dependency > <dependency > <groupId > org.springframework</groupId > <artifactId > spring-aop</artifactId > <version > 6.0.6</version > </dependency > <dependency > <groupId > org.springframework</groupId > <artifactId > spring-aspects</artifactId > <version > 6.0.6</version > </dependency > </dependencies >
外部配置文件
jdbc.properties
1 2 3 4 atguigu.url =jdbc:mysql://localhost:3306/studb atguigu.driver =com.mysql.cj.jdbc.Driver atguigu.username =root atguigu.password =root
spring配置文件
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 @Configuration @ComponentScan("com.atguigu") @PropertySource("classpath:jdbc.properties") public class JavaConfig { @Value("${atguigu.driver}") private String driver; @Value("${atguigu.url}") private String url; @Value("${atguigu.username}") private String username; @Value("${atguigu.password}") private String password; @Bean public DataSource dataSource () { DruidDataSource dataSource = new DruidDataSource (); dataSource.setDriverClassName(driver); dataSource.setUrl(url); dataSource.setUsername(username); dataSource.setPassword(password); return dataSource; } @Bean public JdbcTemplate jdbcTemplate (DataSource dataSource) { JdbcTemplate jdbcTemplate = new JdbcTemplate (); jdbcTemplate.setDataSource(dataSource); return jdbcTemplate; } }
准备dao/service层
dao
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @Repository public class StudentDao { @Autowired private JdbcTemplate jdbcTemplate; public void updateNameById (String name,Integer id) { String sql = "update students set name = ? where id = ? ;" ; int rows = jdbcTemplate.update(sql, name, id); } public void updateAgeById (Integer age,Integer id) { String sql = "update students set age = ? where id = ? ;" ; jdbcTemplate.update(sql,age,id); } }
service
1 2 3 4 5 6 7 8 9 10 11 12 13 @Service public class StudentService { @Autowired private StudentDao studentDao; public void changeInfo () { studentDao.updateAgeById(100 ,1 ); System.out.println("-----------" ); studentDao.updateNameById("test1" ,1 ); } }
测试环境搭建
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @SpringJUnitConfig(JavaConfig.class) public class TxTest { @Autowired private StudentService studentService; @Test public void testTx () { studentService.changeInfo(); } }
6.2.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 46 47 48 49 50 51 52 53 54 55 56 57 58 @Configuration @ComponenScan("com.atguigu") @PropertySource(value = "classpath:jdbc.properties") @EnableTransactionManagement public class DataSourceConfig { @Bean public DataSource dataSource (@Value("${atguigu.url}") String url, @Value("${atguigu.driver}") String driver, @Value("${atguigu.username}") String username, @Value("${atguigu.password}") String password) { DruidDataSource dataSource = new DruidDataSource (); dataSource.setDriverClassName(driver); dataSource.setUrl(url); dataSource.setUsername(username); dataSource.setPassword(password); return dataSource; } @Bean public JdbcTemplate jdbcTemplate (DataSource dataSource) { JdbcTemplate jdbcTemplate = new JdbcTemplate (); jdbcTemplate.setDataSource(dataSource); return jdbcTemplate; } @Bean public TransactionManager transactionManager (DataSource dataSource) { return new DataSourceTransactionManager (dataSource); } }
使用声明事务注解@Transactional
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 @Service public class StudentService { @Autowired private StudentDao studentDao; @Transactional public void changeInfo () { studentDao.updateAgeById(100 ,1 ); System.out.println("-----------" ); int i = 1 /0 ; studentDao.updateNameById("test1" ,1 ); } }
测试事务效果
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 @SpringJUnitConfig(classes = DataSourceConfig.class) public class TxTest { @Autowired private StudentService studentService; @Test public void testTx () { studentService.changeInfo(); } }
6.2.3 事务属性:只读
只读介绍
对一个查询操作来说,如果我们把它设置成只读,就能够明确告诉数据库,这个操作不涉及写操作。这样数据库就能够针对查询操作来进行优化。
设置方式
1 2 @Transactional(readOnly = true)
针对DML动作设置只读模式
会抛出下面异常:
Caused by: java.sql.SQLException: Connection is read-only. Queries leading to data modification are not allowed
@Transactional注解放在类上
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @Service @Transactional(readOnly = true) public class EmpService { @Transactional(readOnly = false) public void updateTwice (……) { …… } public String getEmpName (Integer empId) { …… } }
1. 生效原则如果一个类中每一个方法上都使用了 @Transactional 注解,那么就可以将 @Transactional 注解提取到类上。反过来说:@Transactional 注解在类级别标记,会影响到类中的每一个方法。同时,类级别标记的 @Transactional 注解中设置的事务属性也会延续影响到方法执行时的事务属性。除非在方法上又设置了 @Transactional 注解。对一个方法来说,离它最近的 @Transactional 注解中的事务属性设置生效。
2. 用法举例在类级别@Transactional注解中设置只读,这样类中所有的查询方法都不需要设置@Transactional注解了。因为对查询操作来说,其他属性通常不需要设置,所以使用公共设置即可。然后在这个基础上,对增删改方法设置@Transactional注解 readOnly 属性为 false。
6.2.4 事务属性:超时时间
需求事务在执行过程中,有可能因为遇到某些问题,导致程序卡住,从而长时间占用数据库资源。而长时间占用资源,大概率是因为程序运行出现了问题(可能是Java程序或MySQL数据库或网络连接等等)。此时这个很可能出问题的程序应该被回滚,撤销它已做的操作,事务结束,把资源让出来,让其他正常程序可以执行。概括来说就是一句话:超时回滚,释放资源。
设置超时时间
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 @Service public class StudentService { @Autowired private StudentDao studentDao; @Transactional(readOnly = false,timeout = 3) public void changeInfo () { studentDao.updateAgeById(100 ,1 ); try { Thread.sleep(4000 ); } catch (InterruptedException e) { throw new RuntimeException (e); } studentDao.updateNameById("test1" ,1 ); } }
测试超时效果执行抛出事务超时异常
1 2 3 4 5 6 7 8 org.springframework.transaction.TransactionTimedOutException: Transaction timed out: deadline was Wed May 24 09:10 :43 IRKT 2023 at org.springframework.transaction.support.ResourceHolderSupport.checkTransactionTimeout(ResourceHolderSupport.java:155 ) at org.springframework.transaction.support.ResourceHolderSupport.getTimeToLiveInMillis(ResourceHolderSupport.java:144 ) at org.springframework.transaction.support.ResourceHolderSupport.getTimeToLiveInSeconds(ResourceHolderSupport.java:128 ) at org.springframework.jdbc.datasource.DataSourceUtils.applyTimeout(DataSourceUtils.java:341 ) at org.springframework.jdbc.core.JdbcTemplate.applyStatementSettings(JdbcTemplate.java:1467 )
6.2.5 事务属性:事务异常 Exception ->RuntimeException(运行时异常)和IOException(IO异常)
默认情况默认只针对运行时异常回滚,编译时异常不回滚。情景模拟代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 @Service public class StudentService { @Autowired private StudentDao studentDao; @Transactional(readOnly = false,timeout = 3) public void changeInfo () throws FileNotFoundException { studentDao.updateAgeById(100 ,1 ); new FileInputStream ("xxxx" ); studentDao.updateNameById("test1" ,1 ); } }
设置回滚异常rollbackFor属性:指定哪些异常类才会回滚,默认是 RuntimeException and Error 异常方可回滚!
1 2 3 4 5 6 7 8 9 10 11 12 @Transactional(readOnly = false,timeout = 3,rollbackFor = Exception.class) public void changeInfo () throws FileNotFoundException { studentDao.updateAgeById(100 ,1 ); new FileInputStream ("xxxx" ); studentDao.updateNameById("test1" ,1 ); }
设置不回滚的异常在默认设置和已有设置的基础上,再指定一个异常类型,碰到它不回滚。noRollbackFor属性:指定哪些异常不会回滚, 默认没有指定,如果指定,应该在rollbackFor的范围内!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 @Service public class StudentService { @Autowired private StudentDao studentDao; @Transactional(readOnly = false,timeout = 3,rollbackFor = Exception.class,noRollbackFor = FileNotFoundException.class) public void changeInfo () throws FileNotFoundException { studentDao.updateAgeById(100 ,1 ); new FileInputStream ("xxxx" ); studentDao.updateNameById("test1" ,1 ); } }
6.2.6 事务属性:事务隔离级别
事务隔离级别数据库事务的隔离级别是指在多个事务并发执行时,数据库系统为了保证数据一致性所遵循的规定。常见的隔离级别包括:
读未提交(Read Uncommitted):事务可以读取未被提交的数据,容易产生脏读、不可重复读和幻读等问题。实现简单但不太安全,一般不用。
读已提交(Read Committed):事务只能读取已经提交的数据,可以避免脏读问题,但可能引发不可重复读和幻读。
可重复读(Repeatable Read):在一个事务中,相同的查询将返回相同的结果集,不管其他事务对数据做了什么修改。可以避免脏读和不可重复读,但仍有幻读的问题。
串行化(Serializable):最高的隔离级别,完全禁止了并发,只允许一个事务执行完毕之后才能执行另一个事务。可以避免以上所有问题,但效率较低,不适用于高并发场景。 不同的隔离级别适用于不同的场景,需要根据实际业务需求进行选择和调整。
事务隔离级别设置
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 package com.atguigu.service;import com.atguigu.dao.StudentDao;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Service;import org.springframework.transaction.annotation.Isolation;import org.springframework.transaction.annotation.Transactional;import java.io.FileInputStream;import java.io.FileNotFoundException;@Service public class StudentService { @Autowired private StudentDao studentDao; @Transactional(readOnly = false, timeout = 3, rollbackFor = Exception.class, noRollbackFor = FileNotFoundException.class, isolation = Isolation.REPEATABLE_READ) public void changeInfo () throws FileNotFoundException { studentDao.updateAgeById(100 ,1 ); new FileInputStream ("xxxx" ); studentDao.updateNameById("test1" ,1 ); } }
6.2.7 事务属性:事务传播行为
事务传播行为要研究的问题 举例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 @Transactional public void MethodA () { MethodB(); } @Transactional(propagation = Propagation.REQUIRES_NEW) public void MethodB () { }
propagation属性@Transactional 注解通过 propagation 属性设置事务的传播行为。它的默认值是:
1 2 Propagation propagation () default Propagation.REQUIRED;
propagation 属性的可选值由 org.springframework.transaction.annotation.Propagation 枚举类提供:
名称
含义
REQUIRED
默认值
如果父方法有事务,就加入,如果父方法没有就新建自己独立!
REQUIRES_NEW
不管父方法是否有事务,我都新建事务,都是独立的!
测试
在同一个类中,对于@Transactional注解的方法调用,事务传播行为不会生效。这是因为Spring框架中使用代理模式实现了事务机制,在同一个类中的方法调用并不经过代理,而是通过对象的方法调用,因此@Transactional注解的设置不会被代理捕获,也就不会产生任何事务传播行为的效果。
1. 声明两个业务方法
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 @Service public class StudentService { @Autowired private StudentDao studentDao; @Transactional(readOnly = false, timeout = 3, rollbackFor = Exception.class, noRollbackFor = FileNotFoundException.class, isolation = Isolation.REPEATABLE_READ) public void changeInfo () throws FileNotFoundException { studentDao.updateAgeById(100 ,1 ); new FileInputStream ("xxxx" ); studentDao.updateNameById("test1" ,1 ); } @Transactional(propagation = Propagation.REQUIRED) public void changeAge () { studentDao.updateAgeById(99 ,1 ); } @Transactional(propagation = Propagation.REQUIRED) public void changeName () { studentDao.updateNameById("test2" ,1 ); int i = 1 /0 ; } }
1. 声明一个整合业务方法
1 2 3 4 5 6 7 8 9 10 11 12 13 @Service public class TopService { @Autowired private StudentService studentService; @Transactional public void topService () { studentService.changeAge(); studentService.changeName(); } }
1. 添加传播行为测试
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @SpringJUnitConfig(classes = AppConfig.class) public class TxTest { @Autowired private StudentService studentService; @Autowired private TopService topService; @Test public void testTx () throws FileNotFoundException { topService.topService(); } }
注意:
其他传播行为值(了解)
Propagation.REQUIRED:如果当前存在事务,则加入当前事务,否则创建一个新事务。
Propagation.REQUIRES_NEW:创建一个新事务,并在新事务中执行。如果当前存在事务,则挂起当前事务,即使新事务抛出异常,也不会影响当前事务。
Propagation.NESTED:如果当前存在事务,则在该事务中嵌套一个新事务,如果没有事务,则与Propagation.REQUIRED一样。
Propagation.SUPPORTS:如果当前存在事务,则加入该事务,否则以非事务方式执行。
Propagation.NOT_SUPPORTED:以非事务方式执行,如果当前存在事务,挂起该事务。
Propagation.MANDATORY:必须在一个已有的事务中执行,否则抛出异常。
Propagation.NEVER:必须在没有事务的情况下执行,否则抛出异常。
七、Spring核心掌握总结
核心点
掌握目标
spring框架理解
spring家族和spring framework框架
spring核心功能
ioc/di , aop , tx
spring ioc / di
组件管理、ioc容器、ioc/di , 三种配置方式
spring aop
aop和aop框架和代理技术、基于注解的aop配置
spring tx
声明式和编程式事务、动态事务管理器、事务注解、属性