JHHK

欢迎来到我的个人网站
行者常至 为者常成

bug菌_事务管理

目录

事务产生的背景

开发企业应用时,一个业务操作往往是对数据库读写多步操作的执行集合。数据库操作在顺序执行的过程中,任何一步都有可能发生异常,异常会导致后续操作无法完成。此时由于业务逻辑并未正确且完整地执行,之前成功写入的数据就变得不可靠。

如果要让业务正确完整地执行下去,通常有两种应对方式:

  1. 记录失败的位置,问题修复之后,从上一次执行失败的位置开始继续执行后面的业务逻辑;
  2. 在执行失败的时候,回退本次执行的所有过程,让操作恢复到原始状态,待问题修复之后,再重新执行原来的业务逻辑。

事务就是针对方式 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 指定事务管理器。




R
Valine - A simple comment system based on Leancloud.