知识点: Spring 声明式事务
-
基于注解和配置类的Spring-jdbc环境搭建
1. 准备项目,pom.xml
<dependencies> <!--spring context依赖--> <!--当你引入Spring Context依赖之后,表示将Spring的基础依赖引入了--> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>6.0.6</version> </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> <!-- spring-jdbc --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-jdbc</artifactId> <version>6.0.6</version> </dependency> <!--日志--> <dependency> <groupId>ch.qos.logback</groupId> <artifactId>logback-classic</artifactId> <version>1.2.3</version> </dependency> <!--lombok--> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.18.26</version> </dependency> <!--junit5测试--> <dependency> <groupId>org.junit.jupiter</groupId> <artifactId>junit-jupiter-api</artifactId> <version>5.3.1</version> </dependency> <!--test测试--> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-test</artifactId> <version>6.0.6</version> <scope>test</scope> </dependency> </dependencies> |
注意:引入logback.xml的日志文件
2. 外部配置文件 jdbc.properties
jdbc.url=jdbc:mysql://localhost:3306/studb?serverTimezone=UTC jdbc.userName=root jdbc.userPwd= jdbc.driverClass=com.mysql.cj.jdbc.Driver |
3.Spring 配置类
@Configuration @ComponentScan("com.bdqn") @PropertySource("classpath:jdbc.properties") public class SpringConfig { //@Value 通常用于注入外部化属性 @Value("${jdbc.url}") private String url; @Value("${jdbc.driverClass}") private String driver; @Value("${jdbc.userName}") private String username; @Value("${jdbc.userPwd}") private String password;
/** * 创建数据源对象:druid连接池 * @Bean 注释用于指示方法实例化、配置和初始化要由 Spring IoC 容器管理的新对象。 * 对于那些熟悉 Spring 的 <beans/> XML 配置的人来说, @Bean 注释与 <bean/> 元素起着相同的作用。 */ @Bean public DataSource dataSource(){ DruidDataSource dataSource=new DruidDataSource(); dataSource.setUrl(url); dataSource.setDriverClassName(driver); dataSource.setUsername(username); dataSource.setPassword(password); return dataSource; }
/** * jdbcTemplate:jdbc模版对象 */ @Bean public JdbcTemplate jdbcTemplate(DataSource dataSource){ JdbcTemplate jdbcTemplate=new JdbcTemplate(); jdbcTemplate.setDataSource(dataSource); return jdbcTemplate; } } |
4.准备实体类
@Data @NoArgsConstructor @AllArgsConstructor public class Student { private int id; private String name; private String gender; private int age; private String classes; } |
5.准备dao层
5.1dao接口
public interface StudentDao { int updateNameById(int id,String name); int updateAgeById(int id,int age); } |
5.2dao实现类
@Repository public class StudentDaoImpl implements StudentDao { @Autowired private JdbcTemplate jdbcTemplate;
public int updateNameById(int id,String name){ String sql = "update student set name = ? where id = ? "; int row= jdbcTemplate.update(sql, name, id); return row; }
public int updateAgeById(int id,int age){ String sql = "update student set age = ? where id = ? "; int row=jdbcTemplate.update(sql,age,id); return row; } } |
- 6.准备service层
- 6.1service接口,提供修改用户信息的方法
public interface StudentService { void changeInfo(); } |
6.2service实现类
@Service public class StudentServiceImpl implements StudentService {
@Autowired private StudentDao studentDao;
/** * 修改学生信息的业务方法 */ public void changeInfo(){ studentDao.updateAgeById(1,28); System.out.println("-----------"); studentDao.updateNameById(1,"张小三"); } } |
7.测试环境搭建
package com.bdqn.test;
@SpringJUnitConfig(SpringConfig.class) public class TestTx { @Autowired private StudentService studentService;
@Test public void test01(){ studentService.changeInfo(); } } |
注意:修改学生信息的业务,需要执行2条DML的SQL语句。如果不采用事务控制,可能会导致一条sql成功,一条sql失败。导致出现数据的不合理。
-
编程式事务
编程式事务是指手动编写程序来管理事务,即通过编写代码的方式直接控制事务的提交和回滚。在 Java 中,通常使用事务管理器(如 Spring 中的 PlatformTransactionManager)来实现编程式事务。
编程式事务的主要优点是灵活性高,可以按照自己的需求来控制事务的粒度、模式等等。但是,编写大量的事务控制代码容易出现问题,对代码的可读性和可维护性有一定影响。
Connection conn = ... ; try { // 开启事务:关闭事务的自动提交 conn.setAutoCommit(false); // 核心操作 // 业务代码 // 提交事务 conn.commit(); }catch(Exception e){ // 回滚事务 conn.rollBack(); }finally{ // 释放数据库连接 conn.close(); } |
编程式的实现方式存在缺陷:
- 细节没有被屏蔽:具体操作过程中,所有细节都需要程序员自己来完成,比较繁琐。
- 代码复用性不高:如果没有有效抽取出来,每次实现功能都需要自己编写代码,代码就没有得到复用。
3.声明式事务
声明式事务是指使用注解或 XML 配置的方式来控制事务的提交和回滚。
开发者只需要添加配置即可,具体事务的实现由第三方框架实现,避免我们直接进行事务操作!
使用声明式事务可以将事务的控制和业务逻辑分离开来,提高代码的可读性和可维护性。
区别:
- 编程式事务需要手动编写代码来管理事务
- 而声明式事务可以通过配置文件或注解来控制事务。
4.Spring事务管理器
1.Spring声明式事务对应依赖
spring-tx: 包含声明式事务实现的基本规范(事务管理器规范接口和事务增强等等) spring-jdbc: 包含DataSource方式事务管理器实现类DataSourceTransactionManager spring-orm: 包含其他持久层框架的事务管理器实现类例如:Hibernate/Jpa等 |
2.Spring声明式事务对应事务管理器接口
我们现在要使用的事务管理器是org.springframework.jdbc.datasource.DataSourceTransactionManager,将来整合 JDBC方式、JdbcTemplate方式、Mybatis方式的事务实现! DataSourceTransactionManager类中的主要方法: doBegin():开启事务 doSuspend():挂起事务 doResume():恢复挂起的事务 doCommit():提交事务 doRollback():回滚事务 |
5.基于事务控制实现
- 引入声明式事务相关的依赖
<!-- 声明式事务依赖--> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-tx</artifactId> <version>6.0.6</version> </dependency> <!--aop依赖--> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-aspects</artifactId> <version>6.0.6</version> </dependency> |
2.配置事务管理器 ,配置数据库相关的配置类
@Configuration @ComponentScan("com.bdqn") @PropertySource("classpath:jdbc.properties") //开启事务管理器 @EnableTransactionManagement public class DataSourceConfig{ //@Value 通常用于注入外部化属性 @Value("${jdbc.url}") private String url; @Value("${jdbc.driverClass}") private String driver; @Value("${jdbc.userName}") private String username; @Value("${jdbc.userPwd}") private String password;
/** * 实例化dataSource加入到ioc容器.创建数据源对象:druid连接池 */ @Bean public DataSource dataSource(){ DruidDataSource dataSource=new DruidDataSource(); dataSource.setUrl(url); dataSource.setDriverClassName(driver); dataSource.setUsername(username); dataSource.setPassword(password); return dataSource; }
/** * jdbcTemplate:jdbc模版对象 */ @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); } } |
3. 使用声明事务注解@Transactional进行事务控制
@Service public class StudentService {
@Autowired private StudentDao studentDao;
//使用声明式事务 @Transactional public void changeInfo(){ studentDao.updateAgeById(2,18); System.out.println("-----------"); studentDao.updateNameById(2,"李小四"); } } |
4. 测试事务效果
@SpringJUnitConfig(DataSourceConfig.class) public class TestTx { @Autowired private StudentService studentService;
@Test public void testTx(){ studentService.changeInfo(); } } |
6.事务属性:只读
1.只读介绍
对一个查询操作来说,如果我们把它设置成只读,就能够明确告诉数据库,这个操作不涉及写操作。这样数据库就能够针对查询操作来进行优化。 |
2.设置方式
//readOnly = true把当前事务设置为只读 默认是false! @Transactional(readOnly = true) |
3.针对DML动作设置只读模式
会抛出下面异常:Caused by: java.sql.SQLException: Connection is read-only. Queries leading to data modification are not allowed |
4. @Transactional注解放在类上
1. 生效原则 如果一个类中每一个方法上都使用了 @Transactional 注解,那么就可以将 @Transactional 注解提取到类上。反过来说:@Transactional 注解在类级别标记,会影响到类中的每一个方法。同时,类级别标记的 @Transactional 注解中设置的事务属性也会延续影响到方法执行时的事务属性。除非在方法上又设置了 @Transactional 注解。 对一个方法来说,离它最近的 @Transactional 注解中的事务属性设置生效。 2. 用法举例 在类级别@Transactional注解中设置只读,这样类中所有的查询方法都不需要设置@Transactional注解了。因为对查询操作来说,其他属性通常不需要设置,所以使用公共设置即可。 然后在这个基础上,对增删改方法设置@Transactional注解 readOnly 属性为 false。 |
@Service @Transactional(readOnly = true) public class StudentServiceImpl implements StudentService {
@Autowired private StudentDao studentDao;
/** * 修改用户信息的业务方法,需要加入事务的控制 */ @Transactional(readOnly = false) public void changeInfo(){ studentDao.updateAgeById(2,18); System.out.println("-----------"); studentDao.updateNameById(2,"李小四");
}
/** * 获取学生信息 * readOnly = true把当前事务设置为只读 默认是false! */ @Transactional(readOnly = true) public Student getStudent(int id){ Student stu=studentDao.selectById(id); return stu; } } |
7.事务属性:超时时间
1.需求
事务在执行过程中,有可能因为遇到某些问题,导致程序卡住,从而长时间占用数据库资源。而长时间占用资源,大概率是因为程序运行出现了问题(可能是Java程序或MySQL数据库或网络连接等等)。 此时这个很可能出问题的程序应该被回滚,撤销它已做的操作,事务结束,把资源让出来,让其他正常程序可以执行。 概括来说就是一句话:超时回滚,释放资源。 |
2.设置超时时间
@Service public class StudentService { @Autowired private StudentDao studentDao;
/** * 使用声明式事务 * timeout设置事务超时时间,单位秒! 默认: -1 永不超时,不限制事务时间! */ @Transactional(readOnly = false,timeout = 3) public void changeInfo(){ studentDao.updateAgeById(3,15); //休眠4秒,等待方法超时! try { Thread.sleep(4000); } catch (InterruptedException e) { throw new RuntimeException(e); } System.out.println("-----------"); studentDao.updateNameById(3,"王小五"); } } |
3.测试超时效果 :执行抛出事务超时异常
8.事务属性:事务异常
1. 默认情况
默认只针对运行时异常回滚,编译时异常不回滚。情景模拟代码如下:
@Service public class StudentService { @Autowired private StudentDao studentDao; /** * 使用声明式事务 * timeout设置事务超时时间,单位秒! 默认: -1 永不超时,不限制事务时间! * 默认只针对运行时异常回滚,编译时异常不回滚 * rollbackFor = 指定哪些异常才会回滚,默认是 RuntimeException and Error 异常方可回滚! * noRollbackFor = 指定哪些异常不会回滚, 默认没有指定,如果指定,应该在rollbackFor的范围内! */ @Transactional(readOnly = false,timeout = 3) public void changeInfo() throws FileNotFoundException { studentDao.updateAgeById(6,56); //主动抛出一个检查异常,测试! 发现不会回滚,因为不在rollbackFor的默认范围内! new FileInputStream("xxxx"); System.out.println("-----------"); studentDao.updateNameById(6,"赵小六"); } } |
2.设置回滚异常
rollbackFor属性:指定哪些异常类才会回滚,默认是 RuntimeException and Error 异常方可回滚!
/** * 使用声明式事务 * timeout设置事务超时时间,单位秒! 默认: -1 永不超时,不限制事务时间! * 默认只针对运行时异常回滚,编译时异常不回滚 * rollbackFor = 指定哪些异常才会回滚,默认是 RuntimeException and Error 异常方可回滚! * noRollbackFor = 指定哪些异常不会回滚, 默认没有指定,如果指定,应该在rollbackFor的范围内! */ @Transactional(readOnly = false,timeout = 3,rollbackFor = Exception.class) public void changeInfo() throws FileNotFoundException { studentDao.updateAgeById(6,56); //主动抛出一个检查异常,测试! 发现会回滚,因为在rollbackFor的指定范围内! new FileInputStream("xxxx"); System.out.println("-----------"); studentDao.updateNameById(6,"赵小六"); } |
3.设置不回滚的异常
在默认设置和已有设置的基础上,再指定一个异常类型,碰到它不回滚。
noRollbackFor属性:指定哪些异常不会回滚, 默认没有指定,如果指定,应该在rollbackFor的范围内!
/** * 使用声明式事务 * timeout设置事务超时时间,单位秒! 默认: -1 永不超时,不限制事务时间! * 默认只针对运行时异常回滚,编译时异常不回滚 * rollbackFor = 指定哪些异常才会回滚,默认是 RuntimeException and Error 异常方可回滚! * noRollbackFor = 指定哪些异常不会回滚, 默认没有指定,如果指定,应该在rollbackFor的范围内! */ @Transactional(readOnly = false,timeout = 3,rollbackFor = Exception.class,noRollbackFor = FileNotFoundException.class) public void changeInfo() throws FileNotFoundException { studentDao.updateAgeById(6,78); //主动抛出一个检查异常,测试! 发现不会回滚,因为在noRollbackFor的指定范围内! new FileInputStream("xxxx"); System.out.println("-----------"); studentDao.updateNameById(6,"赵小六"); } |
9.事务属性:事务隔离级别
1.事务隔离级别
数据库事务的隔离级别是指在多个事务并发执行时,数据库系统为了保证数据一致性所遵循的规定。常见的隔离级别包括: 1. 读未提交(Read Uncommitted):事务可以读取未被提交的数据,容易产生脏读、不可重复读和幻读等问题。实现简单但不太安全,一般不用。 2. 读已提交(Read Committed):事务只能读取已经提交的数据,可以避免脏读问题,但可能引发不可重复读和幻读。 3. 可重复读(Repeatable Read):在一个事务中,相同的查询将返回相同的结果集,不管其他事务对数据做了什么修改。可以避免脏读和不可重复读,但仍有幻读的问题。 4. 串行化(Serializable):最高的隔离级别,完全禁止了并发,只允许一个事务执行完毕之后才能执行另一个事务。可以避免以上所有问题,但效率较低,不适用于高并发场景。 不同的隔离级别适用于不同的场景,需要根据实际业务需求进行选择和调整。 |
2.事务隔离级别设置
/** * 使用声明式事务 * timeout设置事务超时时间,单位秒! 默认: -1 永不超时,不限制事务时间! * 默认只针对运行时异常回滚,编译时异常不回滚 * rollbackFor = 指定哪些异常才会回滚,默认是 RuntimeException and Error 异常方可回滚! * noRollbackFor = 指定哪些异常不会回滚, 默认没有指定,如果指定,应该在rollbackFor的范围内! * isolation = 设置事务的隔离级别,mysql默认是repeatable read! */ @Transactional(readOnly = false,timeout = 3,rollbackFor = Exception.class, noRollbackFor = FileNotFoundException.class, isolation = Isolation.REPEATABLE_READ) public void changeInfo() throws FileNotFoundException { studentDao.updateAgeById(6,78); new FileInputStream("xxxx"); System.out.println("-----------"); studentDao.updateNameById(6,"赵小六"); } |
10.事务属性:事务传播行为
1.事务传播行为要研究的问题
- 举例代码
@Transactional public void MethodA(){ // ... MethodB(); // ... } //在被调用的子方法中设置传播行为,代表如何处理调用的事务! 是加入,还是新事务等! @Transactional(propagation = Propagation.REQUIRES_NEW) public void MethodB(){ // ... } |
3. propagation属性
@Transactional 注解通过 propagation 属性设置事务的传播行为。它的默认值是:
Propagation propagation() default Propagation.REQUIRED; |
propagation 属性的可选值由 Propagation 枚举类提供:
1. Propagation.REQUIRED:如果当前存在事务,则加入当前事务,否则创建一个新事务。 2. Propagation.REQUIRES_NEW:创建一个新事务,并在新事务中执行。如果当前存在事务,则挂起当前事务,即使新事务抛出异常,也不会影响当前事务。 3. Propagation.NESTED:如果当前存在事务,则在该事务中嵌套一个新事务,如果没有事务,则与Propagation.REQUIRED一样。 4. Propagation.SUPPORTS:如果当前存在事务,则加入该事务,否则以非事务方式执行。 5. Propagation.NOT_SUPPORTED:以非事务方式执行,如果当前存在事务,挂起该事务。 6. Propagation.MANDATORY:必须在一个已有的事务中执行,否则抛出异常。 7. Propagation.NEVER:必须在没有事务的情况下执行,否则抛出异常。 |
4. 测试
1.声明两个业务方法
@Service public class StudentService { @Autowired private StudentDao studentDao; /** * 声明两个独立修改数据库的事务业务方法 */ @Transactional(propagation = Propagation.REQUIRED) public void changeAge(){ studentDao.updateAgeById(1,99); } @Transactional(propagation = Propagation.REQUIRED) public void changeName(){ studentDao.updateNameById(1,"小张"); int i = 1/0; } } |
2. 声明一个整合业务方法
@Service public class TopService { @Autowired private StudentService studentService; @Transactional public void topService(){ studentService.changeAge(); studentService.changeName(); } } |
3.添加传播行为测试
@SpringJUnitConfig(classes = AppConfig.class) public class TxTest { @Autowired private TopService topService; @Test public void testTx() throws FileNotFoundException { topService.topService(); } } |
**注意:**
在同一个类中,对于@Transactional注解的方法调用,事务传播行为不会生效。这是因为Spring框架中使用代理模式实现了事务机制,在同一个类中的方法调用并不经过代理,而是通过对象的方法调用,因此@Transactional注解的设置不会被代理捕获,也就不会产生任何事务传播行为的效果。