Bootstrap

深入理解事务:原理与示例代码详解

1. 什么是事务?

事务是数据库操作的最小单元,用于保证一系列操作要么全部成功,要么全部失败。


2. 事务的四大特性(ACID)

  • 原子性 (Atomicity):事务作为最小单元,全部成功或全部失败(通过 undo log 实现)。
  • 一致性 (Consistency):事务执行后,数据保持一致性。
  • 隔离性 (Isolation):事务之间互不干扰。
  • 持久性 (Durability):事务提交后,即使数据库崩溃,数据仍然可恢复(通过 redo log 实现)。

3. 隔离级别及问题解决

1. 读未提交(Read Uncommitted)
  • 定义:事务可以读取其他事务未提交的数据。
  • 问题:可能造成脏读幻读不可重复读
  • 场景示例
    • 事务A修改了一条数据,但未提交;
    • 事务B读取了这条未提交的数据;
    • 如果事务A回滚,事务B读取到的就是无效数据,这就是脏读问题。
2. 读已提交(Read Committed)
  • 定义:事务只能读取其他事务已提交的数据。
  • 问题:避免了脏读,但可能出现幻读不可重复读
  • 场景示例
    • 事务A读取一条数据;
    • 在事务A未结束时,事务B修改并提交了这条数据;
    • 事务A再次读取时,获取的是事务B提交后的数据,这就是不可重复读
3. 可重复读(Repeatable Read)
  • 定义:保证在同一事务内多次读取同一字段的结果是一致的(前提是数据未被该事务本身修改)。
  • 问题:避免了脏读不可重复读,但可能出现幻读
  • 场景示例
    • 事务A读取了一张表中的所有记录;
    • 事务B在事务A未结束时,插入了新记录并提交;
    • 事务A再次读取时,新记录不会显示,但该隔离级别并不能锁住插入操作,因此可能产生幻读
4. 串行化(Serializable)
  • 定义:最高的隔离级别,所有事务按顺序执行,完全隔离。
  • 问题:完全避免了脏读不可重复读幻读,但性能最差。
  • 场景示例
    • 事务A读取一张表中的所有记录;
    • 事务B必须等事务A完成后,才能执行插入、修改或删除操作,保证事务A读取的数据不会因其他事务而改变。

隔离级别的选择
  1. 读未提交:适用于对数据一致性要求较低、允许读取未提交数据的场景,例如日志分析。
  2. 读已提交:适用于大多数常见场景,如在线交易系统。
  3. 可重复读:适用于需要一致性高于性能的场景,例如银行账户余额查询。
  4. 串行化:适用于对数据一致性要求极高的场景,例如金融系统中的高价值交易。

4. 使用 Spring Boot 配置事务

4.1 引入依赖
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
</dependency>
4.2 配置数据源与 JPA

application.yml 配置:

spring:
  datasource:
    url: jdbc:mysql://localhost:3306/demo
    username: root
    password: root
  jpa:
    hibernate:
      ddl-auto: update
    show-sql: true

5. 事务隔离级别示例代码

@Service
public class TransactionService {

    @Autowired
    private UserRepository userRepository;

    @Transactional(isolation = Isolation.READ_COMMITTED) // 设置事务隔离级别
    public void updateUserData() {
        // 模拟读操作
        User user = userRepository.findById(1L).orElseThrow();
        System.out.println("User before update: " + user);

        // 模拟写操作
        user.setName("Updated Name");
        userRepository.save(user);

        // 模拟异常,验证事务回滚
        if (true) {
            throw new RuntimeException("Rollback transaction example");
        }
    }
}
解释
  • @Transactional 注解用于声明事务。
  • isolation 属性设置事务的隔离级别。
  • 如果出现异常,事务将会回滚。

6. MVCC

MVCC(Multi-Version Concurrency Control,多版本并发控制)是一种无锁的并发控制技术,是为了解决事务操作中多线程并发安全问题,主要用于提升数据库系统的并发性能。它通过为事务分配单向增长的时间戳,并为每次数据修改保存一个版本,实现对数据库的高效并发访问。在编程语言中,MVCC 也被用于实现事务内存。即便在存在读写冲突的情况下,MVCC 仍能做到不加锁、非阻塞的并发读。


MVCC 的核心思想是:
  • 多版本:每次数据修改都会生成一个新的数据版本,同时保留旧版本。
  • 无锁并发:读取操作无需加锁,避免阻塞写操作;写操作也无需阻塞读取操作。

为什么需要 MVCC?

事务并发访问会导致以下问题:

  1. 读读并发:
    • 多个事务同时读取数据时不会产生问题,不需要特别的并发控制。
  2. 读写并发:
    • 可能引发事务隔离性问题,例如脏读、幻读和不可重复读。
  3. 写写并发:
    • 可能导致数据更新丢失的问题。

通过 MVCC,可以解决读写并发问题,提高数据库的并发性能,同时保证数据一致性。


MVCC 的解决方案
  1. 无锁并发读写
    • 读操作无需阻塞写操作,写操作无需阻塞读操作,显著提高并发性能。
  2. 实现一致性读取
    • 确保读取的版本数据在事务内保持一致,解决脏读、幻读和不可重复读问题。
  3. 写写冲突的解决
    • 使用乐观锁或悲观锁,避免数据更新丢失,同时最大程度提升性能。

快照读 vs 当前读
  1. 快照读

    • 定义:不加锁的非阻塞读取,读取的是历史版本数据。

    • 特点

      • 基于 MVCC 的实现。
      • 事务读取的是某个时间点的快照数据,而非实时最新的数据。
      • 快照读只能在隔离级别为 读已提交可重复读 下生效;在串行化隔离级别下,快照读会退化为当前读。
    • 示例

      SELECT * FROM table_name WHERE ...; -- 普通 SELECT 查询
      
  2. 当前读

    • 定义:加锁的最新版本读取。

    • 特点

      • 当前读涉及加锁操作,如共享锁或排他锁。
      • 当前读读取的是实时最新的数据,同时阻止其他事务对该数据进行修改。
    • 示例

      SELECT * FROM table_name WHERE ... FOR UPDATE; -- 加锁查询
      

MVCC 的实现机制

MVCC 的核心依赖于隐藏的系统字段

  1. DB_TRX_ID(事务 ID):
    • 标识最近一次修改该行数据的事务 ID。
  2. DB_ROLL_PTR(回滚指针):
    • 指向上一个版本数据的回滚段,用于记录历史版本。
  3. DB_ROW_ID(行 ID):
    • 唯一标识数据行。

操作流程

  1. 读取数据
    • 根据事务的快照视图判断哪些版本的数据可见,忽略不可见的数据。
  2. 修改数据
    • 创建新的数据版本,同时保留旧版本,通过 DB_ROLL_PTR 指针链接。
  3. 回滚操作
    • 若事务回滚,利用 DB_ROLL_PTR 恢复到旧版本。

MVCC 的优缺点

优点

  • 提高并发性能,减少锁竞争。
  • 实现一致性读取,保证数据的隔离性。

缺点

  • 需要额外的存储空间保存多个版本的数据。
  • 数据的垃圾回收(清理无用的历史版本)可能增加系统开销。

7. 验证事务的四大特性

测试代码
@Transactional
public void testTransaction() {
    try {
        transactionService.updateUserData();
    } catch (Exception e) {
        System.out.println("Transaction rolled back due to: " + e.getMessage());
    }

    // 验证原子性:确保所有操作回滚
    User user = userRepository.findById(1L).orElseThrow();
    System.out.println("User after rollback: " + user);
}

8.详细讲解 undo logredo log

在 MySQL 的事务实现中,undo logredo log 是核心组件,用于分别实现事务的 原子性持久性


1. 什么是 undo log
  • undo log 是一种逻辑日志,用来记录事务在修改数据之前的原始状态。
  • 主要用于 回滚操作,即当事务失败或被主动回滚时,利用 undo log 恢复数据到修改前的状态。
  • 类似于 “撤销” 的功能。

undo log 的实现机制
  1. 记录数据的原始状态:
    • 在修改一条记录之前,将原始数据保存到 undo log
  2. 回滚时恢复原始状态:
    • 如果事务失败,数据库会利用 undo log 将数据恢复到原始状态。

示例:undo log 在事务中的应用

假设数据库有如下数据:

CREATE TABLE accounts (
    id INT PRIMARY KEY,
    balance DECIMAL(10, 2)
);

INSERT INTO accounts (id, balance) VALUES (1, 1000.00);

我们使用事务更新账户余额:

START TRANSACTION;
UPDATE accounts SET balance = balance - 200 WHERE id = 1;

-- 假设事务失败,回滚操作
ROLLBACK;
undo log 的记录
  1. 修改前,undo log 保存了 balance=1000.00

  2. 回滚时,通过

    undo log
    

    恢复数据:

    UPDATE accounts SET balance = 1000.00 WHERE id = 1;
    

2. 什么是 redo log
  • redo log 是一种物理日志,用来记录事务对数据的最新修改。
  • 主要用于 恢复操作,即当数据库崩溃时,通过 redo log 确保已经提交的事务的修改可以恢复。
  • 类似于 “重做” 的功能。

redo log 的实现机制
  1. 预写日志 (Write-Ahead Logging, WAL):
    • 数据修改之前,先将操作记录写入 redo log,然后再实际写入磁盘。
  2. 事务提交后,数据进入持久化状态:
    • 即使数据库崩溃,redo log 也可以用来重做操作。

示例:redo log 在事务中的应用

还是以账户余额更新为例:

START TRANSACTION;
UPDATE accounts SET balance = balance - 200 WHERE id = 1;

-- 提交事务
COMMIT;
redo log 的记录
  1. 在事务提交之前,

    redo log
    

    记录如下:

    BEGIN
    UPDATE accounts SET balance = 800.00 WHERE id = 1;
    COMMIT
    
  2. 即使数据库在事务提交后崩溃,重启时 redo log 会重新应用以上修改,确保事务的持久性。


3. undo logredo log 协同工作
  • 事务未提交时:
    • 修改的数据存储在内存中,同时 undo log 记录原始数据。
    • 如果事务被回滚,利用 undo log 恢复数据。
  • 事务提交时:
    • redo log 写入磁盘。
    • 即使数据库崩溃,redo log 也可以重做提交的事务。

9. 总结:

  1. InnoDB 与 SQL 标准的差异
    InnoDB 存储引擎在可重复读(Repeatable Read)事务隔离级别下采用 Next-Key Lock(行锁和间隙锁的组合)算法,从而避免幻读问题。这与其他数据库系统(如 SQL Server)不同。因此,InnoDB 的可重复读隔离级别已经能够完全满足事务的隔离性要求,相当于 SQL 标准中的可串行化隔离级别。由于隔离级别越低,请求的锁越少,InnoDB 默认使用可重复读并不会带来性能损失。在分布式事务中,InnoDB 通常会选择可串行化隔离级别。
  2. 脏读、幻读、不可重复读的解决方案
    在使用 InnoDB 引擎时,只需将隔离级别设置为 可重复读(Repeatable Read),即可通过 MVCC 技术解决脏读、幻读和不可重复读问题。
  3. InnoDB 的并发控制与事务隔离优势
    InnoDB 存储引擎通过结合 Next-Key LockMVCC 技术,不仅提升了并发性能,还能在默认的可重复读隔离级别下,保障高效、安全的数据访问。借助 MVCC,无需额外加锁即可实现非阻塞读操作,从而在读写并发环境中表现优越。同时,对于需要更高隔离性的场景,例如分布式事务,可串行化隔离级别依然是可靠的选择。
;