1. 声明式事务概念
1.1 编程式事务
编程式事务是指通过编写代码的方式直接控制事务的提交和回滚,通常使用事务管理器(如 Spring 中的 PlatformTransactionManager )来实现。
相比声明式事务,编程式事务的优点是灵活性高,可以按照自己的需求来控制事务的粒度、模式等等。但是,编写大量的事务控制代码容易出现问题,对代码的可读性和可维护性有一定影响。
因此,在实际开发中,应该根据具体情况选择使用声明式事务还是编程式事务。
Connection conn = null;
try {
conn = dataSource.getConnection();
// 开启事务:关闭事务的自动提交
conn.setAutoCommit(false);
// 核心操作
// 业务代码
// 提交事务
conn.commit();
} catch (SQLException e) {
// 回滚事务
if (conn != null) {
try {
conn.rollback();
} catch (SQLException ex) {
ex.printStackTrace();
}
}
e.printStackTrace();
} finally {
// 释放数据库连接
if (conn != null) {
try {
conn.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
编程式事务的实现方式存在一些缺陷:
-
细节没有被屏蔽:编写编程式事务的代码需要考虑很多细节,如事务的开启、提交、回滚等,这些细节都需要程序员自己来完成,比较繁琐。
-
代码复用性不高:如果没有有效抽取出来,每次实现功能都需要自己编写代码,代码就没有得到复用,增加了代码的冗余度和维护成本。
相比之下,声明式事务通过 AOP 技术将事务管理与业务逻辑分离,将事务管理的细节屏蔽起来,使得代码更加简洁、易读、易维护,同时也提高了代码的复用性。
1.2 声明式事务
声明式事务是指使用注解或 XML 配置的方式来控制事务的提交和回滚。具体的事务实现由第三方框架来完成,开发者只需要添加配置即可,避免了直接进行事务操作。相比之下,编程式事务需要手动编写代码来管理事务。
使用声明式事务可以将事务的控制和业务逻辑分离开来,提高代码的可读性和可维护性。在实际开发中,我们可以根据具体情况选择使用注解或 XML 配置来实现声明式事务。
声明式事务的优点在于可以将事务管理的细节屏蔽起来,使得代码更加简洁、易读、易维护,同时也提高了代码的复用性。此外,声明式事务还可以通过 AOP 技术实现,使得事务管理与业务逻辑分离,更加符合面向对象的设计原则。
1.3 Spring事务管理器
1.Spring声明式事务对应依赖
-
spring-tx:包含声明式事务实现的基本规范,如事务管理器规范接口和事务增强等等。
-
spring-jdbc:包含 DataSource 方式事务管理器实现类 DataSourceTransactionManager。
-
spring-orm:包含其他持久层框架的事务管理器实现类,如 HibernateTransactionManager、JpaTransactionManager 等。
![](https://i-blog.csdnimg.cn/blog_migrate/91278d24cad76adbcc28a8b0d5c8c5a2.png)
DataSourceTransactionManager 是 Spring 框架中用于管理 JDBC 数据源事务的事务管理器实现类,它提供了以下主要方法:
-
doBegin():开启事务。
-
doSuspend():挂起事务。
-
doResume():恢复挂起的事务。
-
doCommit():提交事务。
-
doRollback():回滚事务。
2 基于注解的声明式事务
2.1 准备工作
依赖
<dependencies>
<!-- Spring Context 依赖 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>6.0.6</version>
</dependency>
<!-- JUnit5 测试依赖 -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>5.3.1</version>
</dependency>
<!-- Spring Test 依赖 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>6.0.6</version>
<scope>test</scope>
</dependency>
<!-- Jakarta Annotation API 依赖 -->
<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>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
<version>6.0.6</version>
</dependency>
<!-- Spring AOP 依赖 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
<version>6.0.6</version>
</dependency>
<!-- Spring AspectJ 依赖 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>6.0.6</version>
</dependency>
</dependencies>
url=jdbc:mysql://localhost:3306/studb
driver=com.mysql.cj.jdbc.Driver
username=root
password=root
@Configuration
@ComponentScan("com.csi")
@PropertySource("classpath:jdbc.properties")
public class JavaConfig {
@Value("${driver}")
private String driver;
@Value("${url}")
private String url;
@Value("${username}")
private String username;
@Value("${password}")
private String password;
// 配置 Druid 连接池
@Bean
public DataSource dataSource() {
DruidDataSource dataSource = new DruidDataSource();
dataSource.setDriverClassName(driver);
dataSource.setUrl(url);
dataSource.setUsername(username);
dataSource.setPassword(password);
return dataSource;
}
// 配置 JdbcTemplate
@Bean
public JdbcTemplate jdbcTemplate(DataSource dataSource) {
JdbcTemplate jdbcTemplate = new JdbcTemplate();
jdbcTemplate.setDataSource(dataSource);
return jdbcTemplate;
}
}
@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
public class StudentService {
@Autowired
private StudentDao studentDao;
public void changeInfo() {
studentDao.updateAgeById(100, 1);
System.out.println("-----------");
studentDao.updateNameById("test1", 1);
}
}
@SpringJUnitConfig(JavaConfig.class)
public class TxTest {
@Autowired
private StudentService studentService;
@Test
public void testTx() {
studentService.changeInfo();
}
}
2.2 基本事务控制
1.配置事务管理器
/**
* projectName: com.csi.config
*
* description: 数据库和连接池配置类
*/
@Configuration
@ComponentScan("com.csi")
@PropertySource(value = "classpath:jdbc.properties")
@EnableTransactionManagement
public class DataSourceConfig {
/**
* 实例化 dataSource 加入到 IOC 容器
* @param url
* @param driver
* @param username
* @param password
* @return
*/
@Bean
public DataSource dataSource(@Value("${url}") String url,
@Value("${driver}") String driver,
@Value("${username}") String username,
@Value("${password}") String password) {
DruidDataSource dataSource = new DruidDataSource();
dataSource.setDriverClassName(driver);
dataSource.setUrl(url);
dataSource.setUsername(username);
dataSource.setPassword(password);
return dataSource;
}
/**
* 实例化 JdbcTemplate 对象,需要使用 IOC 中的 DataSource
* @param dataSource
* @return
*/
@Bean
public JdbcTemplate jdbcTemplate(DataSource dataSource) {
JdbcTemplate jdbcTemplate = new JdbcTemplate();
jdbcTemplate.setDataSource(dataSource);
return jdbcTemplate;
}
/**
* 装配事务管理实现对象
* @param dataSource
* @return
*/
@Bean
public TransactionManager transactionManager(DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
}
/**
* projectName: com.csi.service
*/
@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);
}
}
/**
* projectName: com.csi.test
*
* description:
*/
//@SpringJUnitConfig(locations = "classpath:application.xml")
@SpringJUnitConfig(classes = DataSourceConfig.class)
public class TxTest {
@Autowired
private StudentService studentService;
@Test
public void testTx() {
studentService.changeInfo();
}
}
2.3 事务属性:只读
// readOnly = true把当前事务设置为只读 默认是false!
@Transactional(readOnly = true)
@Service
@Transactional(readOnly = true)
public class EmpService {
// 为了便于核对数据库操作结果,不要修改同一条记录
@Transactional(readOnly = false)
public void updateTwice(……) {
……
}
}
2.4 事务属性:超时时间
事务超时时间是指在事务执行过程中,如果超过了指定的时间,事务就会被回滚并释放资源。这样可以避免因为某些问题导致事务一直占用资源,从而影响其他正常程序的执行。
在 Spring 中,可以通过 `@Transactional` 注解的 `timeout` 属性来设置事务的超时时间,单位为秒。例如:
@Transactional(timeout = 10)
public void someTransactionalMethod() {
// ...
}
上面的代码表示,如果 `someTransactionalMethod` 方法执行的时间超过了 10 秒,事务就会被回滚并释放资源。
需要注意的是,事务超时时间的设置应该根据实际情况进行调整,不宜设置过长或过短。如果事务执行的时间比较长,可以考虑将事务拆分成多个小事务,或者优化事务执行的代码逻辑,减少事务执行的时间。
2.5 事务属性:事务异常
在 Spring 中,事务的异常处理是通过 `@Transactional` 注解的 `rollbackFor` 和 `noRollbackFor` 属性来实现的。
`rollbackFor` 属性用于指定哪些异常需要回滚事务,可以指定一个或多个异常类型。例如:
@Transactional(rollbackFor = {SQLException.class, IOException.class})
public void someTransactionalMethod() {
// ...
}
上面的代码表示,如果 `someTransactionalMethod` 方法抛出了 `SQLException` 或 `IOException` 异常,事务就会被回滚。
`noRollbackFor` 属性用于指定哪些异常不需要回滚事务,同样可以指定一个或多个异常类型。例如:
@Transactional(noRollbackFor = {NullPointerException.class, IllegalArgumentException.class})
public void someTransactionalMethod() {
// ...
}
上面的代码表示,如果 `someTransactionalMethod` 方法抛出了 `NullPointerException` 或 `IllegalArgumentException` 异常,事务不会被回滚。
需要注意的是,如果同时指定了 `rollbackFor` 和 `noRollbackFor` 属性,`noRollbackFor` 属性会覆盖 `rollbackFor` 属性。因此,在使用这两个属性时需要注意避免出现冲突。
2.6 事务属性:事务隔离级别
数据库事务的隔离级别是指在多个事务并发执行时,数据库系统为了保证数据一致性所遵循的规定。常见的隔离级别包括:
- 读未提交(Read Uncommitted):事务可以读取未被提交的数据,容易产生脏读、不可重复读和幻读等问题。实现简单但不太安全,一般不用。
- 读已提交(Read Committed):事务只能读取已经提交的数据,可以避免脏读问题,但可能引发不可重复读和幻读。
- 可重复读(Repeatable Read):在一个事务中,相同的查询将返回相同的结果集,不管其他事务对数据做了什么修改。可以避免脏读和不可重复读,但仍有幻读的问题。
- 串行化(Serializable):最高的隔离级别,完全禁止了并发,只允许一个事务执行完毕之后才能执行另一个事务。可以避免以上所有问题,但效率较低,不适用于高并发场景。
package com.csi.service;
import com.csi.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;
/**
* projectName: com.csi.service
*/
@Service
public class StudentService {
@Autowired
private StudentDAO studentDAO;
/**
* timeout 设置事务超时时间,单位秒!默认:-1 永不超时,不限制事务时间!
* rollbackFor 指定哪些异常才会回滚,默认是 RuntimeException 和 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(100, 1);
// 主动抛出一个检查异常,测试!发现不会回滚,因为不在 rollbackFor 的默认范围内!
new FileInputStream("xxxx");
studentDAO.updateNameById("test1", 1);
}
}
2.7 事务属性:事务传播行为
1. 事务传播行为要研究的问题
在一个应用程序中,可能会存在多个事务,这些事务之间可能会相互调用,形成一个事务调用链。在这种情况下,就需要考虑事务传播行为的问题,即一个事务方法调用另一个事务方法时,如何处理事务。
举例来说,假设有两个事务方法 A 和 B,方法 A 调用方法 B,那么在方法 B 中应该如何处理事务?是加入方法 A 的事务中,还是开启一个新的事务?如果开启一个新的事务,那么方法 A 和方法 B 就会分别运行在不同的事务中,这可能会导致数据不一致的问题。
因此,事务传播行为就是指在多个事务方法相互调用的情况下,如何处理事务的问题。Spring 提供了多种事务传播行为,可以根据实际情况进行选择。
2. 事务传播行为的类型
Spring 中定义了 7 种事务传播行为,分别是:
- REQUIRED:如果当前存在事务,则加入该事务;否则开启一个新的事务。
- SUPPORTS:如果当前存在事务,则加入该事务;否则以非事务方式执行。
- MANDATORY:如果当前存在事务,则加入该事务;否则抛出异常。
- REQUIRES_NEW:开启一个新的事务,如果当前存在事务,则挂起该事务。
- NOT_SUPPORTED:以非事务方式执行,如果当前存在事务,则挂起该事务。
- NEVER:以非事务方式执行,如果当前存在事务,则抛出异常。
- NESTED:如果当前存在事务,则在该事务中嵌套一个新的事务;否则开启一个新的事务。
3. 事务传播行为的使用
在 Spring 中,可以通过 `@Transactional` 注解的 `propagation` 属性来设置事务传播行为。例如:
@Transactional(propagation = Propagation.REQUIRED)
public void someTransactionalMethod() {
// ...
}
上面的代码表示,`someTransactionalMethod` 方法的事务传播行为为 REQUIRED。
需要注意的是,不同的事务传播行为适用于不同的场景,需要根据实际业务需求进行选择和调整。同时,事务传播行为的设置也会影响事务的隔离级别和超时时间等属性,需要综合考虑。