目录
事务产生的背景
开发企业应用时,一个业务操作往往是对数据库读写多步操作的执行集合。数据库操作在顺序执行的过程中,任何一步都有可能发生异常,异常会导致后续操作无法完成。此时由于业务逻辑并未正确且完整地执行,之前成功写入的数据就变得不可靠。
如果要让业务正确完整地执行下去,通常有两种应对方式:
- 记录失败的位置,问题修复之后,从上一次执行失败的位置开始继续执行后面的业务逻辑;
- 在执行失败的时候,回退本次执行的所有过程,让操作恢复到原始状态,待问题修复之后,再重新执行原来的业务逻辑。
事务就是针对方式 2 的实现:要么全部成功提交,要么全部回滚,不存在中间态。
事务的概念与特点
事务(Transaction)就是一组操作数据库的动作集合,是现代数据库理论中的核心概念之一。如果一组处理步骤或者全部发生、或者一步也不执行,我们称该组处理步骤为一个事务:
- 当所有的步骤像一个操作一样被完整地执行,称该事务被提交;
- 由于其中的一部分或多步执行失败,导致没有步骤被提交,事务必须回滚到最初的系统状态。
事务共有 4 个特点(ACID):
| 特点 | 说明 |
|---|---|
| 原子性(Atomicity) | 整个事务是不可分割的最小工作单位,要么全部执行成功,要么全部不执行;任何一条语句失败都会导致事务回滚 |
| 一致性(Consistency) | 数据库总是从一个一致性状态转变成另一个一致性状态;这里指语义上的一致性,并非语法上的一致性 |
| 隔离性(Isolation) | 一个事务的执行不受其他事务干扰,并发执行的事务之间互不干扰 |
| 持久性(Durability) | 数据一旦提交,结果就是永久性的,不应因宕机等情况丢失,一般理解为写入硬盘保存成功 |
事务的实现方式
一、MySQL 层面
- 原子性和持久性利用 redo log(重做日志)实现;
- 一致性利用 undo log(回滚日志)实现;
- 隔离性利用锁来实现。
二、Spring Boot 层面
Spring 为事务管理提供了丰富的功能支持,分为编程式和声明式两种方式:
- 编程式事务管理:使用
TransactionTemplate或者直接使用底层的PlatformTransactionManager,Spring 推荐使用TransactionTemplate; - 声明式事务管理:建立在 AOP 之上,对方法前后进行拦截,在目标方法开始之前创建或加入一个事务,执行完目标方法之后根据执行情况提交或回滚事务。
声明式事务管理不需要入侵代码,更快捷而且简单,推荐使用。它又有两种配置方式:
| 方式 | 说明 |
|---|---|
| XML 配置 | 在配置文件中做相关的事务规则声明 |
@Transactional 注解 |
注解配置是目前流行的使用方式,推荐使用 |
在 Spring Boot 项目中,只要导入了 spring-boot-starter-jdbc 依赖包,框架会自动默认注入 DataSourceTransactionManager,因此不需要任何额外配置,就可以直接用 @Transactional 注解使用事务。
快速入门:不加事务测试
在单元测试类中使用 peopleMapper 连续插入 3 条数据,并人为制造异常:将 People 表的 name 字段设为 NOT NULL,这样第三条插入语句的 name 为 null 时就会触发异常。
@Test
public void creatMoreEntity() {
peopleMapper.insert(new PeopleEntity().setName("A").setAge(0));
peopleMapper.insert(new PeopleEntity().setName("B").setAge(10));
peopleMapper.insert(new PeopleEntity().setName(null).setAge(100));
}
执行测试用例,控制台抛出 DataIntegrityViolationException 异常。此时查询数据库中的 People 表:前 2 条数据正确插入,第 3 条数据没有成功插入。
也就是说,测试用例执行到第三条时因为异常中断,但前两条数据已经落库——如果业务要求这 3 条数据要么全部成功、要么全部失败,这种情况就产生了脏数据。
加 @Transactional 注解测试
做法非常简单,只需要在测试函数上添加 @Transactional 注解即可:
@Test
@Transactional
public void creatMoreEntity() {
peopleMapper.insert(new PeopleEntity().setName("A").setAge(0));
peopleMapper.insert(new PeopleEntity().setName("B").setAge(10));
peopleMapper.insert(new PeopleEntity().setName(null).setAge(100));
}
再次执行测试用例,控制台同样抛出异常,但多了一行关键日志,表明事务已被回滚:
2023-05-15 16:18:45.606 INFO 16236 --- [ main]
o.s.t.c.transaction.TransactionContext : Rolled back transaction for test:
[DefaultTestContext@7f132176 testClass = PeopleMapperTest,
testInstance = com.example.connectmysqldemo.mapper.PeopleMapperTest@66226ab,
testMethod = creatMoreEntity@PeopleMapperTest,
testException = org.springframework.dao.DataIntegrityViolationException:
再看数据库,People 表中没有新增任何数据,前两条插入也一并被回滚了——因为第三条新增操作违反了数据库 name 非空的约束,整个事务自动回滚,成功保证了「要么全部成功、要么全部失败」。
进阶用法与小结
一、单元测试中的 @Rollback
单元测试中为了保证每个测试之间的数据独立,通常会配合 @Rollback 注解,让每个单元测试结束时都回滚数据。
二、Service 层的事务声明
实际开发业务逻辑时,通常在 service 层接口中使用 @Transactional 对各个业务逻辑进行事务管理配置:
public interface PeopleService extends IService<PeopleEntity> {
@Transactional
PeopleEntity updateByName(String name);
}
三、指定事务管理器
上面的例子使用了默认的事务配置,可以满足基本的事务需求。但当项目较大较复杂时(比如存在多个数据源),需要在声明事务时通过 value 属性指定不同的事务管理器:
@Transactional(value = "transactionManagerPrimary")
除了指定事务管理器,@Transactional 还能对事务的隔离级别和传播行为进行控制,这些将在后续文章中逐一介绍。
小结
- 事务针对的是「失败后整体回退」的场景,靠 ACID 四个特性保证数据可靠;
- MySQL 用 redo log / undo log / 锁分别实现原子性持久性、一致性、隔离性;
- Spring Boot 推荐声明式事务:引入
spring-boot-starter-jdbc后零配置使用@Transactional; - 单元测试加
@Transactional(可配合@Rollback),多数据源用value指定事务管理器。