Spring事务管理
在日常开发过程中,只要涉及数据操作,都不可避免地会涉及事务管理相关内容,而Spring提供了强大的事务管理机制,能够帮助开发者更轻松地处理数据一致性和事务的问题。
一、什么是事务管理
事务(Transaction)是数据库操作的一个执行单元,它要么完全执行成功,要么完全不执行。事务管理就是确保在数据库操作过程中,要么所有操作都成功提交,要么所有操作都不提交,从而保证数据的一致性和完整性。事务管理通常涉及以下几个关键概念:ACID特性、事务管理器、事务传播行为和事务隔离级别等。
- ACID特性
ACID 是事务处理的四个基本特性,它们分别是原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)和持久性(Durability)。这些特性保证了事务在数据库中的可靠性和稳定性。- 原子性(Atomicity):事务是一个不可分割的工作单位,要么全部成功执行,要么全部失败回滚。
- 一致性(Consistency):事务执行后,数据库从一个一致性状态转换到另一个一致性状态,不会破坏数据库的完整性约束。
- 隔离性(Isolation):事务之间相互隔离,一个事务的执行不会影响其他事务的执行结果。
- 持久性(Durability):事务一旦提交,其结果将永久保存在数据库中,即使系统发生故障也不会丢失。
- 事务管理器(Transaction Manager)
事务管理器是负责管理事务的组件,它可以处理事务的开始、提交、回滚和管理事务的隔离级别等。在Java开发中,常见的事务管理器有 PlatformTransactionManager 和 DataSourceTransactionManager 等。- PlatformTransactionManager:Spring框架中定义的事务管理器接口,可以用于管理各种类型的事务。
- DataSourceTransactionManager:Spring提供的用于管理数据库事务的事务管理器,通常与JDBC、Hibernate等数据访问技术一起使用。
- 事务传播行为(Transaction Propagation Behavior)
事务传播行为定义了在方法调用链中如何处理事务的传播。在Spring中,可以使用 @Transactional 注解来指定事务的传播行为,常见的传播行为包括:- REQUIRED:如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新事务。
- REQUIRES_NEW:每次都创建一个新事务,并且暂停当前事务(如果存在的话)。
- NESTED:如果当前存在事务,则在嵌套事务中执行;如果当前没有事务,则创建一个新事务。
- 事务隔离级别(Isolation Level)
事务隔离级别定义了多个事务并发执行时的相互影响程度。在数据库中,常见的事务隔离级别包括:- READ_UNCOMMITTED:允许事务读取未提交的数据,可能导致脏读、不可重复读和幻读等问题。
- READ_COMMITTED:确保一个事务只能读取已提交的数据,避免了脏读问题,但可能存在不可重复读和幻读问题。
- REPEATABLE_READ:确保一个事务在多次读取同一数据时,得到的结果是一致的,避免了不可重复读问题,但可能存在幻读问题。
- SERIALIZABLE:最高的隔离级别,确保多个事务并发执行时不会产生任何问题,但可能导致性能下降。
- 事务的控制
- 事务的开始:事务开始时,数据库记录当前状态,将事务标记为进行中。
- 事务的执行:执行事务中的一组操作,包括读取数据、修改数据等。
- 事务的提交:如果事务执行成功,将事务中的所有操作结果提交给数据库并标记事务为成功。
- 事务的回滚:如果事务执行失败或出现异常,将事务中的操作全部撤销,并回滚到事务开始前的状态。
二、Spring事务管理
在了解事务及事务管理的基本概念之后,需要熟悉如何在代码实现事务的管理。在 Spring中主要有两种方式来管理事务:声明式事务管理和编程式事务管理。
编程式事务管理
编程式事务管理是通过在代码中显式地管理事务的开始、提交和回滚。这种方式提供了更细粒度的控制,但相对来说更繁琐。
- 使用 Spring 的TransactionTemplate或PlatformTransactionManager接口,可以在代码中手动控制事务的边界。
声明式事务管理
声明式事务管理是通过在 Spring 的配置文件或注解中定义事务边界和属性,让 Spring 自动管理事务。这是一种更简洁、方便的方式,通常推荐使用。
- XML 配置文件:通过在<tx>元素中配置事务管理器和事务属性,可以在 Spring 上下文中启用事务管理。
- Java 注解:使用@Transactional注解在类或方法上,可以指定事务的传播行为、隔离级别、回滚规则等,在SpringBoot中,主要用注解的方式更加简洁、方便。
对于大多数简单的场景,推荐使用声明式事务管理,因为它更简洁和易于维护。而在特殊情况下,比如业务逻辑很复杂,需要更细粒度的控制时,则可以考虑使用编程式事务管理。
1. 编程式事务管理
1.1 通过PlatformTransactionManger控制事务
举例:现在要给南京公司的员工涨薪,为了防止执行涨薪因为异常导致数据前后不一致,我们需要来对”涨薪“这个事务进行管理
- 首先需要获取事务管理器,指定数据源
- 定义事务属性(隔离级别、传播特性、超时时间、是否只读等)
- 开启事务,返回事务状态
- 编写业务逻辑代码
- 提交事务
- 若出现异常,则回滚事务
@Service
@Slf4j
public class TransactionPractice {
// 1.获取事务管理器,指定数据源
@Autowired
private PlatformTransactionManager transactionManager;
// 2.定义事务属性(隔离级别、传播特性、超时时间、是否只读等)
@Autowired
private TransactionDefinition transactionDefinition;
@Autowired
private EmployeeService employeeService;
public void ProgrammaticTransactionManagementFirst() {
// 3.开启事务,返回事务状态
TransactionStatus transactionStatus = null;
try {
List<Employee> employeesOri = getCompanyEmployee("南京公司");
System.out.println("--------涨薪前-------");
employeesOri.forEach(System.out::println);
employeesOri.forEach(o -> o.setSalary(o.getSalary() + 1000));
transactionStatus = transactionManager.getTransaction(transactionDefinition);
employeeService.updateBatchById(employeesOri);
// 4.提交事务
transactionManager.commit(transactionStatus);
} catch (Exception e) {
log.error("涨薪异常:(");
// 5.回滚事务
if (transactionStatus != null) {
log.info("涨薪事务回滚!");
transactionManager.rollback(transactionStatus);
}
}
List<Employee> employeesLater = getCompanyEmployee("南京公司");
System.out.println("--------涨薪后-------");
employeesLater.forEach(System.out::println);
}
public List<Employee> getCompanyEmployee(String company) {
LambdaQueryWrapper<Employee> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(Employee::getSubCompany, "南京公司");
List<Employee> employees = employeeService.list(queryWrapper);
return employees;
}
}
正常情况输出如下:
--------涨薪前-------
Employee(id=4, name=翠花, age=27, subCompany=南京公司, department=测试一部, salary=3000)
Employee(id=5, name=玲玲, age=31, subCompany=南京公司, department=测试二部, salary=4000)
--------涨薪后-------
Employee(id=4, name=翠花, age=27, subCompany=南京公司, department=测试一部, salary=4000)
Employee(id=5, name=玲玲, age=31, subCompany=南京公司, department=测试二部, salary=5000)
如果执行事务过程中出现异常,例如在更新数据之后,插入一条编码为空的数据(DDL限制code不能为空),执行插入操作时报异常
此时执行结果如下:可以看到,由于更新过程中出现异常,事务发生回滚,数据未发生变化
--------涨薪前-------
Employee(id=4, name=翠花, age=27, subCompany=南京公司, department=测试一部, salary=4000)
Employee(id=5, name=玲玲, age=31, subCompany=南京公司, department=测试二部, salary=5000)
2024-03-31 16:17:38.589 ERROR 14324 --- [ main] o.practice.service.TransactionPractice : 涨薪异常:(
2024-03-31 16:17:38.589 INFO 14324 --- [ main] o.practice.service.TransactionPractice : 涨薪事务回滚!
--------涨薪后-------
Employee(id=4, name=翠花, age=27, subCompany=南京公司, department=测试一部, salary=4000)
Employee(id=5, name=玲玲, age=31, subCompany=南京公司, department=测试二部, salary=5000)
1.2 通过TransactionTemplate控制事务
这种方式通过重写TransactionTemplate.execute()方法,实现对事务的控制。
@Service
@Slf4j
public class TransactionPractice {
@Autowired
private TransactionTemplate transactionTemplate;
@Autowired
private EmployeeService employeeService;
public void ProgrammaticTransactionManagementSecond() {
List<Employee> employeesOri = getCompanyEmployee("南京公司");
System.out.println("--------涨薪前-------");
employeesOri.forEach(System.out::println);
employeesOri.forEach(o -> o.setSalary(o.getSalary() + 1000));
// 使用TransactionTemplate执行事务操作
transactionTemplate.execute(transactionStatus -> {
try {
// 更新数据
employeeService.updateBatchById(employeesOri);
// 新增一条员工数据
//Employee emptyEmployee = new Employee();
//emptyEmployee.setName("小刚").setAge(23).setSubCompany("南京公司").setJob("测试").setDepartment("测试一部").setSalary(4000);
//employeeService.save(emptyEmployee);
} catch (Exception e) {
log.error("涨薪异常:( 涨薪事务回滚!");
transactionStatus.setRollbackOnly();
}
return null;
});
List<Employee> employeesLater = getCompanyEmployee("南京公司");
System.out.println("--------涨薪后-------");
employeesLater.forEach(System.out::println);
}
public List<Employee> getCompanyEmployee(String company) {
LambdaQueryWrapper<Employee> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(Employee::getSubCompany, "南京公司");
List<Employee> employees = employeeService.list(queryWrapper);
return employees;
}
}
正常情况下执行结果:
--------涨薪前-------
Employee(id=4, name=翠花, age=27, subCompany=南京公司, department=测试一部, salary=3000)
Employee(id=5, name=玲玲, age=31, subCompany=南京公司, department=测试二部, salary=4000)
--------涨薪后-------
Employee(id=4, name=翠花, age=27, subCompany=南京公司, department=测试一部, salary=4000)
Employee(id=5, name=玲玲, age=31, subCompany=南京公司, department=测试二部, salary=5000)
同样让代码执行报异常,结果如下:
--------涨薪前-------
Employee(id=4, name=翠花, age=27, subCompany=南京公司, department=测试一部, salary=4000)
Employee(id=5, name=玲玲, age=31, subCompany=南京公司, department=测试二部, salary=5000)
2024-03-31 16:38:22.098 ERROR 8520 --- [ main] o.practice.service.TransactionPractice : 涨薪异常:( 涨薪事务回滚!
--------涨薪后-------
Employee(id=4, name=翠花, age=27, subCompany=南京公司, department=测试一部, salary=4000)
Employee(id=5, name=玲玲, age=31, subCompany=南京公司, department=测试二部, salary=5000)
查看TransactionTemplate源码,可以看到TransactionTemplate其实是对PlatformTransactionManager的进一步封装,底层逻辑是一致的
@Nullable
public <T> T execute(TransactionCallback<T> action) throws TransactionException {
Assert.state(this.transactionManager != null, "No PlatformTransactionManager set");
if (this.transactionManager instanceof CallbackPreferringPlatformTransactionManager) {
return ((CallbackPreferringPlatformTransactionManager)this.transactionManager).execute(this, action);
} else {
TransactionStatus status = this.transactionManager.getTransaction(this);
Object result;
try {
result = action.doInTransaction(status);
} catch (Error | RuntimeException var5) {
this.rollbackOnException(status, var5);
throw var5;
} catch (Throwable var6) {
this.rollbackOnException(status, var6);
throw new UndeclaredThrowableException(var6, "TransactionCallback threw undeclared checked exception");
}
this.transactionManager.commit(status);
return result;
}
}
2. 声明式事务管理
2.1 基本使用
使用@Transactional实现事务管理前,需要在启用类上添加@EnableTransactionManagement。在添加这个注解后,在项目启动时,Spring在扫描Bean时,检查是否有@Transactional注解(类、父类、接口或方法),如果存在,则会通过AOP给对应的Bean生成代理对象,并拦截public方法(只对),完成事务相关操作(开启、提交或回滚事务)。需要注意:
- 该注解只对public方法有效
- 如果放在接口上,接口所有实现类的public方法都会加上事务
- 如果放在类上,则当前类及子类的public方法都会加上事务
- 如果放在public方法上,只会对该方法加上事务
还是上面涨薪的例子,改用@Transactional实现如下
@Service
@Slf4j
public class TransactionPractice {
@Autowired
private EmployeeService employeeService;
@Transactional(rollbackFor = Exception.class)
public void DeclarativeTransactionManagement() {
List<Employee> employeesOri = getCompanyEmployee("南京公司");
System.out.println("--------涨薪前-------");
employeesOri.forEach(System.out::println);
employeesOri.forEach(o -> o.setSalary(o.getSalary() + 1000));
// 更新数据
employeeService.updateBatchById(employeesOri);
// 新增一条员工数据
Employee emptyEmployee = new Employee();
emptyEmployee.setName("小刚").setAge(23).setSubCompany("南京公司").setJob("测试").setDepartment("测试一部").setSalary(4000);
employeeService.save(emptyEmployee);
List<Employee> employeesLater = getCompanyEmployee("南京公司");
System.out.println("--------涨薪后-------");
employeesLater.forEach(System.out::println);
}
public List<Employee> getCompanyEmployee(String company) {
LambdaQueryWrapper<Employee> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(Employee::getSubCompany, "南京公司");
List<Employee> employees = employeeService.list(queryWrapper);
return employees;
}
}
2.2 参数
可以看到,使用@Transactional进行事务管理的方式非常简洁、方便。@Transactional提供了多个参数来针对不同的使用场景进行配置
- propagation
- 作用:指定事务的传播行为,即方法被调用时,当前方法如何处理事务。
- 取值:
- Propagation.REQUIRED:如果当前存在事务,则加入当前事务;如果当前没有事务,则创建一个新的事务。
- Propagation.REQUIRES_NEW:每次都创建一个新的事务,如果当前存在事务,则将当前事务挂起。
- Propagation.SUPPORTS:支持当前事务,如果当前没有事务,则以非事务方式执行。
- Propagation.NOT_SUPPORTED:以非事务方式执行操作,如果当前存在事务,则将当前事务挂起。
- Propagation.MANDATORY:支持当前事务,如果当前没有事务,则抛出异常。
- Propagation.NEVER:以非事务方式执行操作,如果当前存在事务,则抛出异常。
- Propagation.NESTED:如果当前存在事务,则在嵌套事务中执行;如果当前没有事务,则创建一个新的事务。
- isolation
- 作用:指定事务的隔离级别,即事务并发时对数据的读取、写入等操作的隔离程度。
- 取值:
- Isolation.DEFAULT:默认的隔离级别,使用底层数据源的默认隔离级别。
- Isolation.READ_UNCOMMITTED:允许读取未提交的数据,最低的隔离级别,可能会导致脏读、不可重复读和幻读。
- Isolation.READ_COMMITTED:只能读取已提交的数据,避免了脏读,但可能会出现不可重复读和幻读。
- Isolation.REPEATABLE_READ:保证在同一个事务中多次读取同一数据时结果一致,避免了脏读和不可重复读,但可能会出现幻读。
- Isolation.SERIALIZABLE:最高的隔离级别,保证事务串行执行,避免了脏读、不可重复读和幻读,但性能较低。
- timeout
- 作用:指定事务的超时时间,即事务执行的最大时间限制,超过时间会自动回滚事务。
- 取值:以秒为单位的整数值,例如 timeout = 30 表示事务执行时间不超过 30 秒。
- readOnly
- 作用:指定事务是否为只读事务,即方法中是否允许进行写操作。
- 取值:true 表示只读事务,false 表示可读写事务,默认为 false。
- rollbackFor 和 noRollbackFor
- 作用:指定哪些异常会触发事务回滚,哪些异常不会触发事务回滚。
- 取值:可以指定异常类的数组,例如 rollbackFor = {RuntimeException.class, SQLException.class} 表示遇到 RuntimeException 或 SQLException 异常时会触发事务回滚。
- value
- 作用:用于指定应该应用事务管理的方法,可以指定多个方法名或通配符。
- 取值:字符串数组,例如 value = {“save*”, “update*”} 表示应用于所有以 save 或 update 开头的方法。