MySQL InnoDB存储引擎详细介绍之事务

文章目录

  • 一、事务的概念
    • [1. 原子性(Atomicity)](#1. 原子性(Atomicity))
    • [2. 一致性(Consistency)](#2. 一致性(Consistency))
    • [3. 隔离性(Isolation)](#3. 隔离性(Isolation))
    • [4. 持久性(Durability)](#4. 持久性(Durability))
  • 二、事务的分类
    • [1. 扁平事务(Flat Transactions)](#1. 扁平事务(Flat Transactions))
    • [2. 带有保存点的扁平事务(Flat Transactions with Savepoints)](#2. 带有保存点的扁平事务(Flat Transactions with Savepoints))
    • [3. 链事务(Chained Transactions)](#3. 链事务(Chained Transactions))
    • [4. 嵌套事务(Nested Transactions)](#4. 嵌套事务(Nested Transactions))
    • [5. 分布式事务(Distributed Transactions)](#5. 分布式事务(Distributed Transactions))
  • 三、事务的实现
    • [1. redo log](#1. redo log)
      • [1.1 基本概念](#1.1 基本概念)
      • [1.2 redo log block](#1.2 redo log block)
      • [1.3 redo log group](#1.3 redo log group)
      • [1.4 LSN](#1.4 LSN)
    • [2. undo log](#2. undo log)
      • [2.1 基本概念](#2.1 基本概念)
      • [2.2 undo log的参数配置](#2.2 undo log的参数配置)
      • [2.3 undo log 结构组成](#2.3 undo log 结构组成)
      • [2.4 undo log 信息查询](#2.4 undo log 信息查询)
    • [3. purge](#3. purge)
      • [3.1 Purge 线程的核心清理对象](#3.1 Purge 线程的核心清理对象)
      • [3.2 purge核心工作流程](#3.2 purge核心工作流程)
      • [3.3 purge核心参数](#3.3 purge核心参数)
    • [4. group commit](#4. group commit)
  • 四、事务控制语句
    • [1. 开始事务](#1. 开始事务)
    • [2. 提交事务](#2. 提交事务)
    • [3. 回滚事务](#3. 回滚事务)
    • [4. 设置事务隔离级别](#4. 设置事务隔离级别)
    • [5. 保存点(Savepoint)](#5. 保存点(Savepoint))
    • [6. 自动提交控制](#6. 自动提交控制)
    • [7. 核心参数completion_type](#7. 核心参数completion_type)
  • 五、隐式提交SQL
    • [1. DDL语句(数据定义语言)](#1. DDL语句(数据定义语言))
    • [2. DCL语句(数据控制语言)](#2. DCL语句(数据控制语言))
    • [3. 管理语句](#3. 管理语句)
    • [4. 锁与事务控制语句](#4. 锁与事务控制语句)
    • [4. 其他隐式提交场景](#4. 其他隐式提交场景)
  • 六、事务的隔离级别
    • [1. 隔离级别类型](#1. 隔离级别类型)
    • [2. 设置隔离级别](#2. 设置隔离级别)
    • [3. 查看隔离级别](#3. 查看隔离级别)
  • 七、分布式事务
    • [1. XA协议](#1. XA协议)
    • [2. InnoDB对XA事务的支持](#2. InnoDB对XA事务的支持)
    • [3. InnoDB XA分布式事务的实现](#3. InnoDB XA分布式事务的实现)
    • [4. InnoDB XA事务的内部实现](#4. InnoDB XA事务的内部实现)
    • [5. InnoDB XA事务使用Java JTA实现](#5. InnoDB XA事务使用Java JTA实现)
  • 八、长事务
    • [1. 长事务产生原因](#1. 长事务产生原因)
    • [2. 长事务带来的问题](#2. 长事务带来的问题)
      • [2.1. 锁资源长期占用,引发并发阻塞](#2.1. 锁资源长期占用,引发并发阻塞)
      • [2.2. 回滚日志(Undo Log)膨胀,占用存储与性能](#2.2. 回滚日志(Undo Log)膨胀,占用存储与性能)
      • [2.3. 主从延迟(Replication Delay)](#2.3. 主从延迟(Replication Delay))
      • [2.4. 崩溃恢复时间延长](#2.4. 崩溃恢复时间延长)
      • [2.5. 资源耗尽风险](#2.5. 资源耗尽风险)
    • [3. 长事务的监控](#3. 长事务的监控)
      • [3.1. 通过系统表查询活跃事务](#3.1. 通过系统表查询活跃事务)
      • [3.2. 查看锁等待关系](#3.2. 查看锁等待关系)
      • [3.3. 监控InnoDB状态](#3.3. 监控InnoDB状态)
    • [4. 长事务优化](#4. 长事务优化)
      • [4.1. 应用层优化](#4.1. 应用层优化)
      • [4.2. 数据库配置优化](#4.2. 数据库配置优化)
      • [4.3. 监控与告警](#4.3. 监控与告警)
      • [4.4. 特殊场景处理](#4.4. 特殊场景处理)
    • [5. 长事务的典型案例](#5. 长事务的典型案例)
  • 总结

InnoDB存储引擎是MySQL中支持事务的核心引擎,其事务机制通过ACID特性确保数据的一致性和可靠性,同时利用行级锁、MVCC(多版本并发控制)等技术实现高并发性能。以下是InnoDB事务的详细介绍:

一、事务的概念

事务可以由一条简单的SQL语句,也可以由一组复杂的SQL语句组成。事务是访问并更新数据库中各种数据项的一个程序执行单元。在同一个事务中的操作,要么都修改,要么都不做,这就是事务的目的,也是事务模型区别于文件系统的重要特性之一。

事务有严格的定义,他必须同时满足ACID四大特性。在InnoDB存储引擎的默认事务隔离级别为READ REPEATABLE,完全遵循和满足事务的ACID特性,以下是关于事务ACID特性:

1. 原子性(Atomicity)

  • 定义:事务是不可分割的工作单元,所有操作要么全部成功,要么全部失败回滚。
  • 实现机制:通过Undo Log(回滚日志)记录事务修改前的数据状态。若事务失败,InnoDB会利用Undo Log将数据恢复到事务开始前的状态。
  • 示例:在转账操作中,若扣款成功但转账失败,Undo Log会撤销扣款,确保原子性。

2. 一致性(Consistency)

  • 定义:事务将数据库从一个一致性状态转移到另一个一致性状态,确保数据完整性约束(如唯一键、外键)不被破坏。
  • 实现机制:原子性、隔离性和持久性的共同作用。例如,事务不能破坏表中唯一键的约束。

3. 隔离性(Isolation)

  • 定义:并发事务的操作相互隔离,避免干扰。InnoDB支持四种隔离级别,通过锁和MVCC实现。
  • 实现技术
    • 锁机制:共享锁(S锁,读锁)、排他锁(X锁,写锁)、间隙锁(Gap Lock)、Next-Key Lock(记录锁+间隙锁)。
    • MVCC:通过读视图(Read View)和事务ID机制,为每个事务提供数据的一致性快照,实现非锁定读。

4. 持久性(Durability)

  • 定义:事务提交后,结果永久保存,即使系统崩溃也能恢复。
  • 实现机制:通过**Redo Log(重做日志)**记录事务对数据页的物理修改。事务提交时,Redo Log先写入磁盘,再修改内存中的数据页(脏页),最后由后台线程异步刷盘。
  • 崩溃恢复:重启时,InnoDB通过重放Redo Log恢复未写入磁盘的数据,确保持久性。

二、事务的分类

InnoDB存储引擎中的事务可根据操作结构和应用场景分为扁平事务、带有保存点的扁平事务、链事务、嵌套事务及分布式事务五类,其核心特性与适用场景如下:

1. 扁平事务(Flat Transactions)

  • 特点:最简单的事务类型,所有操作处于同一层次,从BEGIN WORK开始,到COMMIT WORK或ROLLBACK WORK结束,要么全部执行成功,要么全部回滚。
  • 适用场景:适用于操作简单、无需中途回滚部分操作的环境,如简单的数据增删改查。
  • 限制 :无法提交或回滚事务的一部分,或分步骤提交。

2. 带有保存点的扁平事务(Flat Transactions with Savepoints)

  • 特点:在扁平事务的基础上增加保存点(Savepoint),允许事务执行过程中回滚到同一事务中的较早状态。
  • 适用场景:适用于需要中途回滚部分操作,但不想放弃整个事务的场景,如复杂的业务流程中需要撤销某一步操作。
  • 限制:系统崩溃时,所有保存点消失,事务需从开始处重新执行。

通过ROLLBACK WORK :2 命令回滚到保存点2,即回滚到当前事务的第2个Savepoint的位置。

3. 链事务(Chained Transactions)

  • 特点:提交一个事务时,释放不需要的数据对象,将必要的处理上下文隐式地传给下一个要开始的事务。提交上一个事务和开始下一个事务合并为一个原子操作。

  • 适用场景:适用于需要连续执行多个事务,且每个事务的结果对后续事务可见的场景,如订单处理流程中的多个步骤。

  • 与带有保存点的扁平事务的区别

    • 带有保存点的扁平事务能回滚到任意正确的保存点,而链事务中的回滚仅限于当前事务,即只能恢复到最近一个保存点。
    • 链事务在执行COMMIT后即释放了当前事务所持有的锁,而带有保存点的扁平事务不影响迄今为止所持有的锁。

4. 嵌套事务(Nested Transactions)

  • 特点:由一个顶层事务控制着各个层次的事务,形成层次结构框架。子事务的提交操作并不马上生效,除非其父事务已经提交。

  • 适用场景:适用于需要分解复杂事务为多个子事务,且子事务的执行依赖于父事务的场景,如大型业务流程中的多个子流程。

  • 限制

    • 子事务的提交并不马上生效,需等待顶层事务提交后才真正提交。
    • 任何子事务的回滚都会导致其所有子事务一同回滚,子事务不具有持久性。

5. 分布式事务(Distributed Transactions)

  • 特点:在分布式环境下运行的扁平事务,需要根据数据所在位置访问网络中的不同节点。
  • 适用场景:适用于需要跨多个数据库或服务执行事务的场景,如跨银行的转账操作。
  • 实现方式:通常使用两阶段提交(2PC)协议来确保所有参与的事务资源要么都提交,要么都回滚。
  • 场景示例 :假设一个用户在ATM转账操作,从农行转账100到工行。我们可以把ATM机视为节点A,农行数据库视为节点B,工行数据库视为节点C,整个转账的过程拆解如下:
    • 节点A发出转账命令。
    • 节点B执行卡余额减去100。
    • 节点C执行卡余额增加100。
    • 节点A通知用户转账成功或转账失败。

在InnoDB存储引擎中支持扁平事务、带有保存点的事务、链事务、分布式事务,而对于嵌套事务并不原生支持。但是用户可以通过带有保存点的事务来模拟串行的嵌套事务。


三、事务的实现

InnoDB存储引擎事务的隔离性依靠锁来实现,而事务的原子性、一致性和持久性则通过数据库的redo log和undo log来实现。其中,InnoDB存储引擎通过Force Log at Commit机制实现事务的持久性,即当事务提交(COMMIT)时,必须先将该事务的所有日志写入到重做日志文件进行持久化,等事务的COMMIT操作完成才算完成。这里记录的的日志包括redo log和undo log两部分。redo log是物理日志,记录的是页的物理修改操作,用来保证事务的持久性。undo log是逻辑日志,根据每行记录进行记录,用来帮助事务回滚及MVCC的功能。

1. redo log

1.1 基本概念

重做日志(redo log)包含两部分组成:一是内存中的重做日志缓冲(redo log buffer),其是易丢失的;二是重做日志文件(redo log file),其是持久的。

InnoDB存储引擎在每次事务提交时,先将日志写入重做日志缓冲,再将重做日志写入磁盘的重做日志文件,最后调用一次fsync操作确认写入磁盘成功。这里fsync操作的效率取决于磁盘的性能,同时决定了事务提交的性能,也就是数据库的性能。对此,InnoDB存储引擎提供了参数innodb_flush_log_at_trx_commit来控制fsync操作刷新磁盘的策略,具体参数值如下:

行为描述 持久性 性能影响 适用场景
0 每次事务提交时,不写入重做日志文件操作,由master thread执行每秒一次的fsync()操作。 最低(可能丢失最近 1 秒的事务数据) 最高(减少磁盘 I/O) 报表系统、数据分析系统、对数据丢失容忍度较高的系统 。
1(默认) 每次事务提交时,立即刷新日志到磁盘,并调用fsync()确保数据落盘。 最高(完全符合 ACID 规范) 最低(频繁磁盘 I/O) 金融交易、订单处理等对数据一致性要求极高的场景。
2 每次事务提交时,写入重做日志文件,但仅写入文件操作系统的缓存中,不进行fsync()操作。 中等(操作系统崩溃时可能丢失 1 秒数据) 中等(减少部分磁盘 I/O) 电商、社交应用、对数据丢失容忍度为1秒以上的系统。

重做日志(redo log)是物理格式日志,记录了对每个页的的修改,每个事务可能会产生多条日志记录,他们在事务开始时不断的被写入,并且写入过程是并发的。

1.2 redo log block

在InnoDB存储引擎中,重做日志都是以512字节进行存储的,我们将这512字节大小称为一个重做日志块(redo log block)。如果一个页中产生的重做日志数量大于512字节,那么就需要分割为多个重做日志进行存储,这也是为什么一个事务可能需要多次写入重做日志的原因。

重做日志块的大小为什么选择512字节?这是因为磁盘扇区的大小一般为512字节,因此日志的写入可以保证原子性,不需要doublewrite技术。

Redo LogBuffer由重做日志块组成,在内部类似一个数组。而重做日志块由日志块头(log block header)、日志内容(log body)、日志块尾(log block tailer)三部分组成:

  • 日志块头(log block header) :占用12字节,日志块头位于每个日志块的起始位置,包含以下字段:

    • LOG_BLOCK_HDR_NO(4字节):日志块在redo log buffer中的唯一标识符,用于区分不同的日志块。
    • LOG_BLOCK_HDR_DATA_LEN(2字节) :记录当前日志块中已使用的字节数。初始值为12(仅包含块头),随着日志内容的写入逐渐增加,最大为512(满块)。
    • LOG_BLOCK_FIRST_REC_GROUP(2字节) :指向当前日志块中第一个MTR(Mini-Transaction)生成的第一条redo日志的偏移量。若LOG_BLOCK_FIRST_REC_GROUP与LOG_BLOCK_HDR_DATA_LEN值相等,则表示当前日志块中无新日志。若当前日志块中无日志,则值为0。
    • LOG_BLOCK_CHECKPOINT_NO(4字节):记录检查点(checkpoint)的LSN(Log Sequence Number)值。用于标识该日志块是否属于某个检查点范围。
  • 日志内容(log body) :占用492字节,日志内容是日志块的核心部分,用于存储实际的redo日志记录。每条redo日志记录包含以下信息:

    • 日志类型(type) :标识日志的操作类型,InnoDB1.2支持56种格式。例如:
      • MLOG_REC_INSERT:插入一条非紧凑行格式的记录。
      • MLOG_REC_DELETE:删除一条非紧凑行格式的记录。
      • MLOG_COMP_REC_DELETE:删除一条紧凑行格式的记录。
      • MLOG_1BYTE:修改1字节数据。
      • MLOG_2BYTE:修改2字节数据。
      • MLOG_WRITE_STRING:写入字符串数据。
    • 表空间ID(space):标识数据所属的表空间ID。
    • 页号(page_no):标识数据所在的页。
    • 偏移量(offset):标识数据在页内的具体位置。
    • 数据结构体(data) :不同重做日志类型对应不同的日志数据结构,保存着不同重做日志类型的实际存储内容。如下图,展示的是插入和删除的日志结构:
  • 日志块尾(log block tailer) :占用4字节,日志块尾位于日志块的末尾,包含以下字段:

    • LOG_BLOCK_TRL_NO(4字节):日志块的校验和,用于验证日志块的完整性。通过校验和可以检测日志块在写入或读取过程中是否发生损坏。值与LOG_BLOCK_HDR_NO相同,在函数log_block_init中被初始化。

1.3 redo log group

重做日志组(redo log group)是InnoDB存储引擎中redo日志的逻辑容器,由多个相同大小的redo日志文件组成。它不是物理文件的简单集合,而是一个环形缓冲区,用于高效管理redo日志的写入和回收。

1.3.1 redo log group的组成结构

redo log block由多个相同大小的redo log file文件构成,每个redo log file由多个redo log block构成。不同的是,在第一个redo log file文件中存储了2KB大小的与redo log group相关的关键元数据信息。而其他的redo log file文件只预留了2KB的结构,没有存储数据。因此每次更新redo log file时,都会同时更新前2KB部分的信息,这些信息为数据库恢复起到关键作用。下图为redo log group结构:

1.3.2 redo log group的相关参数

以下是redo log group相关的常用参数:

参数 默认值 说明 调整建议
innodb_log_files_in_group 2 指定redo log group中redo log file的数量,默认值为2。即ib_logfile0ib_logfile1 保持默认值,不要轻易修改
innodb_log_file_size 128M 指定每个redo log file的大小,默认值为48MB。 根据系统内存调整
innodb_log_group_home_dir . 指定redo log file的存放位置,默认为InnoDB数据目录所在路径。 通常保持默认
innodb_log_buffer_size 16M 指定日志缓冲区的大小 innodb_log_file_size配合优化

redo log group的总大小由innodb_log_files_in_groupinnodb_log_file_size两个参数共同决定,即总大小 = innodb_log_files_in_group * innodb_log_file_size。在InnoDB 1.2版本之前,总大小需小于4GB;从InnoDB 1.2版本开始,最大支持512GB。

1.3.3 redo log group的环形缓冲区机制

redo log block通过追加写入方式写入redo log file的最后,当一个redo log file被写满时,会接着写入下一个redo log file,如此循环的写入,其核心流程如下:

  1. 初始化:MySQL启动时,根据配置创建redo log group日志文件
  2. 写入 :从ib_logfile0开始写入
  3. 切换 :当ib_logfile0写满,切换到ib_logfile1
  4. 覆盖 :当ib_logfile1写满,且检查点已推进到ib_logfile0的开头 ,则覆盖ib_logfile0
  5. 循环:持续循环写入

1.4 LSN

LSN是Log Sequence Number的缩写,指的是日志序列号。在InnoDB存储引擎中,LSN占用8字节,并且单调递增。LSN主要有以下作用:

  • 记录重做日志写入的总量:记录在重做日志中的LSN表示事务写入重做日志的字节的总量。比如当前重做日志的LSN为1000,这时有一个事务T1写入了100字节的重做日志,那么LSN就编程1100,若又有一个T2写入200字节,那LSN就变成1300。
  • 记录checkpoint的位置:记录已持久化到磁盘的数据页的最大LSN,存储在重做日志文件头部。
  • 标记页的版本 :LSN不仅记录在重做日志中,在每个页的头部的FIL_PAGE_LSN字段中也记录了该页的LSN。在页中的LSN表示该页最后刷新时LSN的大小,用来判断是否需要进行恢复操作。比如页P1的LSN为1000,当数据库启动时检测到重做日志中的LSN为1300,并且该事务已经提交,所以数据库执行恢复时将重做日志应用到P1页中。

用户可以通过SHWO ENGINE INNODB STATUS;查看LSN的情况:

  • Log sequence number:表示当前的LSN。
  • Log flushed up to:表示刷新到重做日志文件的LSN。
  • Pages flushed up to:表示下一次即将做checkpoint的LSN。
  • Last checkpoint at:表示已经刷新到磁盘的LSN。

为什么上图中的Last checkpoint point不等于Pages flushed up to?

因为在没有新数据的写入的情况下,执行checkpoint时redo日志还会写入日志类型为MLOG_CHECKPOINT的日志,而MLOG_CHECKPOINT占用9个字节,所以会出现Pages flushed up to-last checkpoint point=9。

2. undo log

回滚日志(undo log)是通过构建不同事务下行记录的历史版本链,后提交的事务总是在undo log最后。当事务执行失败或用户主动回滚(ROLLBACK)时,InnoDB通过undo log将数据恢复到事务开始前的状态。

除了回滚操作,undo log的另一个作用就是MVCC,当事务读取数据时,若数据被其他事务修改但未提交,InnoDB通过undo log的版本链找到符合当前事务可见性的旧版本数据,避免阻塞写入操作,实现读写并发。

undo log在执行时会产生redo log日志,也需要持久性的保护。

2.1 基本概念

undo log存储在表空间中,由特殊的回滚段(Rollback Segment)构成。

  • Rollback Segment(回滚段)

    • undo log以回滚段(Rollback Segment)为组织单位,每个回滚段包含1024个undo log segment(槽位),每个事务在执行时分配一个或多个undo log segment,用于存储undo log条目。
  • Undo Log Page(回滚页)

    • undo log条目存储在固定大小的undo页(默认16KB)中,每个undo页包含页头(File Header)、undo日志头(Undo Log Header)和多个undo日志条目(Undo Log Entry)。
  • Undo Log Record(回滚记录)

    • 每条数据记录包含两个隐藏列:DB_TRX_ID(事务ID)和DB_ROLL_PTR(回滚指针)。
    • 每次修改数据时,InnoDB会生成一条undo log,并通过DB_ROLL_PTR将新旧版本串联成链表,形成版本链。例如:
      • 事务A插入数据后,DB_ROLL_PTR为NULL。
      • 事务B修改数据时,生成undo log记录旧值,并将当前记录的DB_ROLL_PTR指向该undo log。
      • 事务C再次修改时,重复上述过程,形成版本链。

2.2 undo log的参数配置

InnoDB存储引擎使用特殊的回滚段(Rollback Segment)来管理undo log,每个回滚段中记录了1024个undo log segment(槽位),通过每个undo log segment(槽位)进行undo页的申请。

在InnoDB1.1版本以前只有1个回滚段,说明只能同时支持1*1024个事务在线。在InnoDB1.1之后支持了128个回滚段,可以同时支持128*1024个事务在线。从InnoDB1.2版本开始,可以通过如下参数对回滚段进一步控制配置:

参数 默认值 作用 调整建议
innodb_undo_directory . 指定undo表空间存放路径,默认值'.'表示当前数据目录 一般使用默认值
innodb_undo_logs 128 指定回滚段的数量 高并发场景可增至256
innodb_undo_tablespaces 0 设置独立undo表空间数量 ,默认0,表示使用系统表空间。 MySQL 5.7+:设为4
innodb_undo_log_truncate OFF 是否自动截断undo表空间,需配合innodb_max_undo_log_size设置单文件最大大小 强烈建议设为ON
innodb_max_undo_log_size 1G 单个undo表空间最大值 根据业务调整

当事务提交时,InnoDB存储引擎会做以下两件事情:

  • 将undo log放入列表中,供之后的purge操作。
    • 事务提交后不能立马删除undo log及undo log页,而是保存到链表中,因为可能还有其他事务需要通过undo log来得到行记录之前的历史版本,是否可以删除由purge线程来判断。
  • 判断undo log所在的页是否可以重用,若可以则分配给下个事务使用。
    • 当事务提交时,先将undo log放入链表中,然后判断undo页的使用空间是否小于3/4,若是则表示undo页可以被重用,之后新的undo log记录在当前的undo log页后面。

2.3 undo log 结构组成

在InnoDB存储引擎中,undo log分为insert undo logupdate undo log

2.3.1 insert undo log

insert undo log是指在insert操作中产生的undo log,因为insert操作记录只对当前事务本身可见,对其他事务不可见,所以insert undo log可以在当前事务提交后直接删除,不需要进行purge操作。下图是insert undo log的格式,其中*表示对存储的字段进行了压缩:

  • next:占2字节,记录的是下一个undo log的位置,通过该next的字节可以知道一个undo log所占的空间字节数。
  • type_cmpl :占1字节,记录的是undo的操作类型。
    • 11:TRX_UNDO_INSERT_REC,对于insert undo log值固定为11,表示新增插入数据。
  • undo_no:记录的是当前事务的ID。
  • table_id:记录的是undo log所对应的表对象。
  • n_unique_index:记录了所有主键的列和值。在进行rollback操作时,根据这些值可以定位到具体的记录,然后进行删除即可。
  • start:占2字节,记录的是undo log的开始位置。

2.3.2 update undo log

update undo log记录的是对delete和update操作产生的undo log。该undo log可能需要提供MVCC机制,因此不能在事务提交时进行删除,而是在事务提交时放入undo log链表,等待purge线程进行最后的删除。下图是update undo log的格式,其中*表示对存储的字段进行了压缩:

  • next:占2字节,记录的是下一个undo log的位置,通过该next的字节可以知道一个undo log所占的空间字节数。
  • type_cmpl :占1字节,记录的是undo的操作类型。
    • 12:TRX_UNDO_UPD_EXIST_REC,表示更新未被标记为删除的记录,即普通更新操作。
    • 13:TRX_UNDO_UPD_DEL_REC,表示将已标记为删除的记录重新标记为未删除,即"取消删除"操作。
    • 14:TRX_UNDO_DEL_MARK_REC,表示将记录标记为删除,即普通删除操作。
  • undo_no:记录的是当前事务的ID。
  • table_id:记录的是undo log所对应的表对象。
  • DATA_TRX_ID:记录的是MVCC旧版本数据的事务ID。
  • DATA_ROLL_PTR:记录的是MVCC旧版本数据的回滚指针。
  • n_unique_index:记录了所有主键的列和值。在进行rollback操作时,根据这些值可以定位到具体的记录,然后进行回滚更新。
  • update_vector:记录的是更新时所有更新列的内容,可能还包括索引列的更新。
  • start:占2字节,记录的是undo log的开始位置。

2.4 undo log 信息查询

2.4.1 undo log的状态查询

查询当前系统链表中undo log的数量,History list length就表示的是链表中undo log的数量。

sql 复制代码
SHOW ENGINE INNODB STATUS;

输出示例

复制代码
------------
TRANSACTIONS
------------
Trx id counter 3844
Purge done for trx's n:o < 0 undo n:o < 0 state: running but idle
History list length 0
LIST OF TRANSACTIONS FOR EACH SESSION:
---TRANSACTION 421806767397600, not started
0 lock struct(s), heap size 1136, 0 row lock(s)
---TRANSACTION 421806767396688, not started
0 lock struct(s), heap size 1136, 0 row lock(s)

2.4.2 查看Undo表空间信息

查询显示当前系统中所有Undo表空间的状态,ACTIVE表示可用,INACTIVE表示已被标记为可回收。

sql 复制代码
SELECT NAME, STATE 
FROM INFORMATION_SCHEMA.INNODB_TABLESPACES 
WHERE NAME LIKE '%undo%';

输出示例

复制代码
+------------------+--------+
| NAME             | STATE  |
+------------------+--------+
| undo_001         | ACTIVE |
| undo_002         | ACTIVE |
+------------------+--------+

2.4.3 查看Undo Log的使用情况

查询显示当前系统中所有undo log的状态,包括活跃的和已提交的。

sql 复制代码
SELECT * FROM information_schema.INNODB_UNDO_LOGS;

关键字段说明

  • TRX_ID:事务ID
  • UNDO_LOG_TYPE:undo log类型(0=Insert,1=Update)
  • TRX_STATE:事务状态
  • ACTIVE:是否活跃(1=活跃,0=已提交)
  • UNDO_NO:undo log序号

输出示例

复制代码
+---------+----------------+----------------+-----------+----------+
| TRX_ID  | UNDO_LOG_TYPE  | TRX_STATE      | ACTIVE    | UNDO_NO  |
+---------+----------------+----------------+-----------+----------+
| 1234567 | 0              | ROLLING_BACK   | 1         | 0        |
| 1234568 | 1              | PREPARED       | 0         | 1        |
+---------+----------------+----------------+-----------+----------+

查询到事务TRX_ID后可以再查询INFORMATION_SCHEMA.INNODB_TRX,查询事务运行的具体状况。

2.4.4 查看Undo表空间大小

查询显示每个Undo表空间的大小,单位为字节。

sql 复制代码
SELECT 
  TABLESPACE_NAME, 
  FILE_NAME, 
  FILE_SIZE 
FROM INFORMATION_SCHEMA.INNODB_SYS_DATAFILES 
WHERE TABLESPACE_NAME LIKE 'undo%';

输出示例

复制代码
+----------------+------------------+-----------+
| TABLESPACE_NAME| FILE_NAME        | FILE_SIZE |
+----------------+------------------+-----------+
| undo_001       | undo_001.ibu     | 1073741824|
| undo_002       | undo_002.ibu     | 1073741824|
+----------------+------------------+-----------+

2.4.5 查看Undo相关的系统变量

sql 复制代码
SHOW VARIABLES LIKE 'innodb_undo%';

输出示例

复制代码
+-------------------------------------+-------+
| Variable_name                       | Value |
+-------------------------------------+-------+
| innodb_undo_directory               | .     |
| innodb_undo_logs                    | 128   |
| innodb_undo_tablespaces             | 2     |
| innodb_undo_log_truncate            | ON    |
| innodb_max_undo_log_size            | 1073741824 |
+-------------------------------------+-------+

说明:这些系统变量控制Undo Log的行为:

  • innodb_undo_logs:回滚段数量(默认128)
  • innodb_undo_tablespaces:undo表空间数量(默认0,5.7+默认2)
  • innodb_undo_log_truncate:是否自动截断undo表空间(默认OFF,5.7+推荐ON)
  • innodb_max_undo_log_size:单个undo表空间最大值(默认1GB)

2.4.6 查看Undo Log清理状态

sql 复制代码
SELECT 
  NAME, 
  VALUE 
FROM INFORMATION_SCHEMA.GLOBAL_STATUS 
WHERE NAME LIKE '%undo%';

关键状态变量

  • Innodb_undo_log_truncate:是否正在执行truncate
  • Innodb_undo_log_truncate_pending:是否有pending的truncate
  • Innodb_undo_log_truncate_size:当前truncate的大小

3. purge

Purge 线程是 InnoDB 后台核心线程之一,核心目标是异步清理过期的update undo log、物理删除标记为删除的数据行,释放 undo log空间并完成数据的最终清理,同时保障 MVCC 多版本并发控制的正确性。

3.1 Purge 线程的核心清理对象

Purge 线程仅处理两类过期数据:

  1. 标记为删除的数据行 :InnoDB 的 DELETE/UPDATE(生成新版本)是"逻辑删除",仅给数据行打 delete flag 标记,需 Purge 物理删除;
  2. 过期的 update undo log:事务提交后保留的 update undo log,当所有依赖该版本的快照读事务都已提交,该 undo 日志即过期,可清理。

insert undo log 事务提交后可直接释放,会被立即删除,无需 Purge 介入。

3.2 purge核心工作流程

步骤1:线程初始化与启动(InnoDB 启动阶段)

InnoDB 引擎启动时,根据 innodb_purge_threads 配置创建指定数量的 Purge 线程(单线程/多线程模式),其核心准备如下:

  1. 绑定到指定的回滚段(Rollback Segment):每个 Purge 线程负责部分回滚段的清理,避免竞争;
  2. 初始化 Purge LSN(日志序列号):记录上一次 Purge 清理到的 undo log 位置,作为本次清理的起点;
  3. 注册到 InnoDB 线程调度器:由主线程协调,避免与 redo log 刷盘、脏页刷新等操作过度竞争资源。

步骤2:等待触发条件(空闲/唤醒机制)

Purge 线程并非持续运行,而是"休眠-唤醒"循环,触发唤醒的条件包括:

  1. 事务提交触发 :当有事务提交并生成 update undo log 时,触发 Purge 线程唤醒,将事务的Undo log标记为"可清理",trx->purge = TRUE标记事务需要清理。
  2. 定时唤醒:InnoDB 每隔固定时间(约 1 秒)主动唤醒 Purge 线程,避免过期数据堆积;
  3. 空间阈值触发 :当 undo 表空间使用率超过阈值(如 innodb_undo_log_truncate 配置的阈值),强制唤醒 Purge 线程;
  4. 休眠机制 :无清理任务时,Purge 线程进入休眠(通过条件变量 srv_purge_event 等待),减少 CPU 占用。

步骤3:确定 Purge 安全边界(核心:Read View 判定)

这是 Purge 线程的核心前置步骤,目的是避免清理仍被活跃事务依赖的 undo 日志/数据版本:

  1. 获取全局最小活跃事务 ID(min_trx_id)

    InnoDB 维护"活跃事务链表",Purge 线程遍历该链表,找到当前所有未提交事务的最小事务 ID(min_trx_id);

  2. 确定 Purge 边界

    c 复制代码
    bool can_purge(trx_id_t trx_id) {
      // 条件1:事务已提交(trx_id <= trx_sys->max_trx_id)
      // 条件2:所有活跃事务ID > 该事务ID(trx_id < trx_sys->low_limit_id)
      return (trx_id <= trx_sys->max_trx_id) && 
             (trx_id < trx_sys->low_limit_id);
    }
    • 关键判断trx_id < trx_sys->low_limit_id
      • trx_sys->low_limit_id:当前活跃事务ID的最小值。
      • 只有当所有活跃事务ID都大于待清理事务ID时,才能安全清理。

    举例:若当前最小活跃事务 ID 是 1000,则事务 ID 999 及以下生成的 undo log 均可清理。

  3. 生成 Purge View :基于 min_trx_id 生成专用的 Read View(Purge View),作为本次清理的判定依据。

步骤4:扫描 undo 日志(遍历回滚段+undo 页)

Purge 线程从上次清理的 Purge LSN 开始,按顺序扫描目标回滚段的 undo log:

  1. 定位 undo 段与 undo 页
    回滚段包含多个 undo log 段(undo segment),每个段对应多个 16KB 的 undo 页;Purge 线程按 LSN 顺序遍历 undo 页,通过 undo 页头的 n_bytes_below 字段定位页内有效 undo 记录;
  2. 筛选 undo 记录类型
    • 跳过 insert undo log(事务提交后已释放,无清理价值);
    • 仅处理 update undo log,并通过 Purge View 判定是否过期:
      • 过期:undo 记录的事务 ID < min_trx_id → 标记为可清理;
      • 未过期:undo 记录仍被活跃事务依赖 → 停止当前 undo 段的扫描(后续 undo 记录事务 ID 更大,必然未过期)。
  3. 串联版本链:若一条数据行有多个 update undo 记录(多次更新),通过 undo 记录的「回滚指针(roll pointer)」遍历版本链,找到最旧的过期版本。

步骤5:构建清理队列

Purge线程将待清理的Undo Log加入purge队列:

c 复制代码
void purge_queue_add(trx_id_t trx_id) {
  // 检查是否已存在
  if (trx_id > trx_sys->purge_trx_no) {
    // 添加到清理队列
    trx_sys->purge_queue[trx_sys->purge_queue_size++] = trx_id;
  }
}
  • 队列机制:按事务ID排序,确保先清理旧事务。
  • 同页优先扫描:当判断到该undo log记录可以被清理时,会同时扫描该undo log页其他记录是否可以清理。
  • 批量处理 :每次处理innodb_purge_batch_size个事务。

步骤6:清理过期数据(核心执行阶段)

清理过期数据分两步完成"数据行物理删除 + update undo log 清理",保证原子性:

  • 数据行物理删除

    1. 对 undo log 关联的、打了 delete flag 的数据行加轻量级锁(避免与读写操作冲突);
    2. 从 B+ 树中物理移除该数据行(释放页内空间,更新 B+ 树节点的空闲空间链表);
    3. 若数据行所在页的空闲空间达到阈值,标记为"可复用页",供后续插入/更新使用。
  • update undo log 清理

    1. 对判定为过期的 update undo log记录,若清理的是 undo 页末尾的记录:直接更新 undo 页头的 n_bytes_below 字段(减去被清理记录的长度),释放空间;
    2. 若清理的是 undo 页中间的记录:标记该区域为"碎片空间"(通过 undo 页头的 free_bits 字段),后续写入新 undo 记录时优先复用;
    3. 若整页 undo 记录都已过期:重置 n_bytes_below 为 0,将该 undo 页标记为"空白页",完全复用。

步骤7:undo log表空间维护与空间复用

  • 碎片整理:批量清理多个 undo 页后,合并页内碎片空间,提升后续 undo 记录写入的连续性;
  • 自动截断(Truncate) :若开启 innodb_undo_log_truncate = ON(默认开启),当 undo 表空间大小超过 innodb_undo_truncate_log_size 阈值时:
    1. 将当前 undo 表空间标记为"待截断";
    2. 切换到备用 undo 表空间(如 undo002)承接新的 undo 记录;
    3. 截断旧 undo 表空间至初始大小(默认 10MB),释放磁盘空间。

步骤8:更新 Purge 进度与触发检查点

  1. 更新 Purge LSN :将本次清理到的 undo log 位置更新到 Purge LSN(持久化到 redo log 和系统表空间),作为下次清理的起点;
  2. 触发轻量检查点:通知 InnoDB 主线程触发小型 checkpoint,将"已清理 undo 日志"的信息刷盘,避免恢复时重复处理;
  3. 更新统计信息 :将清理的 undo 记录数、释放的空间大小等统计到 INFORMATION_SCHEMA.INNODB_METRICS 中,供监控使用。

步骤9:负载控制与退避(资源保护)

为避免 Purge 线程占用过多 CPU/IO 资源,影响前台业务:

  1. 批量清理限制 :每次清理的 undo 记录数不超过 innodb_purge_batch_size(默认 300),达到阈值则暂停,下次唤醒继续;
  2. CPU 退避:若系统 CPU 使用率超过阈值(如 80%),Purge 线程主动休眠一段时间(毫秒级);
  3. IO 避让:若 redo log 刷盘、脏页刷新等高优先级 IO 操作正在执行,Purge 线程延迟清理,避免 IO 竞争。

步骤10:循环执行/休眠

  • 若本次清理未完成所有过期数据(如达到批量限制、系统负载高):保留当前 Purge LSN,直接进入步骤 2(等待下一次唤醒);
  • 若本次清理完成所有过期数据:更新 Purge LSN 后,进入深度休眠,等待下一次触发。

3.3 purge核心参数

以下是purge线程相关的核心参数:

参数名 作用说明
innodb_purge_threads Purge 线程数量(默认 4),数量越多清理速度越快,适合高并发场景
innodb_purge_batch_size 单次批量清理的 undo 记录数(默认 300),越大清理效率越高,但瞬时资源占用越多
innodb_undo_log_truncate 开启 undo 表空间自动截断(默认 ON),清理后收缩 undo 文件
innodb_undo_tablespaces 独立 undo 表空间数量(建议 2~3),避免单表空间膨胀,提升 Purge 并行度
innodb_purge_rseg_truncate_frequency 回滚段截断频率(默认 128),控制 undo 段的复用速度

4. group commit

InnoDB存储引擎每次提交事务都需要进行一次fsync操作,保证重做日志安全写入磁盘。同时为了提高磁盘fsync操作的效率,数据库都提供了group commit功能,即一次fsync操作可以同时刷新多个事务日志被写入文件。

group commit事务提交两阶段

对于InnoDB存储引擎来说,事务提交时会进行两个阶段的操作:

a. 修改内存中事务对应的信息,并且将日志写入重做日志缓冲。

b. 调用fsync将确保日志都从重做日志缓冲写入磁盘。

步骤a操作支持同时多个不同事务并发执行,当多个事务提交完成后再进行步骤b操作,将多个事务的重做日志通过一次fsync操作刷新到磁盘,减少磁盘压力,提升数据库性能。

group commit与二进制日志问题

在InnoDB1.2版本之前会开启二进制日志,为了保证存储引擎中事务和数据库二进制日志的一致性,二者之间使用了两阶段事务,其步骤如下:

  1. 当事务提交时,InnoDB存储引擎进行prepare操作。
  2. MySQL数据库上层写入二进制日志。
  3. InnoDB存储引擎将日志写入重做日志文件:
    • a. 修改内存中事务对应的信息,并且将日志写入重做日志缓冲。
    • b. 调用fsync将确保日志都从重做日志缓冲写入磁盘。

上述流程中当步骤2操作完成后就表示事务提交完成,无论步骤3是否完成,每个步骤都需要进行一次fsync操作才能保证上下两层数据的一致性。

二进制的写入顺序和事务提交顺序是数据库恢复的关键,所以为了保证二者的顺序一致,MySQL数据库使用了prepare_commit_mutex锁,但是该锁开启后,步骤3中的步骤b执行时,就无法同时执行其他线程的步骤a,变成了单线程执行,导致group commit失效。

步骤2的fsync参数由sync_binlog控制

步骤3的fsync由参数innodb_flush_log_at_trx_commit控制。

BLGC的实现

Binary Log Group Commit简称BLGC,BLGC的实现是在数据库上层提交时将事务按顺序放入一个队列中,队列的第一个事务称为leader,其他事务称为follower,由leader控制follower的行为,将事务的提交按以下三个阶段步骤来完成:

  1. Flush阶段:将每个事务的二进制日志写入内存中。
  2. Sync阶段:将内存中的二进制日志刷新到磁盘,若队列中有多个事务,那么仅一次fsync操作就完成了二进制日志的写入,这就是BLGC。
  3. Commit阶段 :leader根据顺序调用存储引擎事务的提交,InnoDB存储引擎本身支持group commit,因此修复了原先由于prepare_commit_mutex锁导致的group commit失效问题。

BLGC移除了prepare_commit_mutex锁,优化了二进制日志导致group commit失效问题,使数据库二进制日志和InnoDB存储引擎事务日志都是group commit的。

参数binlog_max_flush_queue_time用来控制Flush阶段中等待的时间,即使之前的一组事务完成提交,当前一组的事务也不会马上进入Sync阶段,而是要等待该配置时间,默认值为0。


四、事务控制语句

1. 开始事务

START TRANSACTIONBEGIN :显式开启一个事务。之后的所有SQL操作会被纳入该事务,直到提交或回滚。

示例:

sql 复制代码
START TRANSACTION;
-- 或
BEGIN;

存储过程只能使用START TRANSACTION来开启一个事务,因为存储过程中的BEGIN会被自动识别为BEGIN···END语句。

2. 提交事务

COMMITCOMMIT WORK :将事务中的所有操作永久保存到数据库。

示例:

sql 复制代码
COMMIT;             -- 提交事务,永久保存更改
-- 或
COMMIT WORK;        -- 与COMMIT等效

3. 回滚事务

ROLLBACKROLLBACK WORK :撤销事务中的所有未提交操作,恢复事务开始前的状态。

示例:

sql 复制代码
ROLLBACK;           -- 回滚事务,撤销所有未提交的更改
-- 或
ROLLBACK WORK;      -- 与ROLLBACK等效

4. 设置事务隔离级别

MySQL支持四种隔离级别(从低到高):

  • READ UNCOMMITTED(读未提交)
  • READ COMMITTED(读已提交)
  • REPEATABLE READ(可重复读,MySQL默认级别)
  • SERIALIZABLE(串行化)

设置方式

  • 全局级别 (影响所有新连接):

    sql 复制代码
    SET GLOBAL TRANSACTION ISOLATION LEVEL READ COMMITTED;
  • 会话级别 (仅当前连接):

    sql 复制代码
    SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
  • 事务内设置 (在START TRANSACTION前):

    sql 复制代码
    SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
    START TRANSACTION;

5. 保存点(Savepoint)

允许在事务中创建标记点,支持多个保存点,支持部分回滚。

  • 创建保存点 :在事务中设置一个标记点,允许部分回滚。

    sql 复制代码
    SAVEPOINT savepoint_name;
  • 回滚到保存点 :将事务回滚到指定的保存点,而不是全部回滚。

    sql 复制代码
    ROLLBACK TO SAVEPOINT savepoint_name;
  • 释放保存点 (释放后无法再回滚到该点):删除一个已设置的保存点,保存点不存在时会抛出异常。

    sql 复制代码
    RELEASE SAVEPOINT savepoint_name;

6. 自动提交控制

MySQL默认启用自动提交(autocommit=1),每条SQL单独作为一个事务。

  • 关闭自动提交 (手动管理事务):

    sql 复制代码
    SET autocommit = 0; -- 后续操作需显式提交或回滚
  • 恢复自动提交

    sql 复制代码
    SET autocommit = 1;

7. 核心参数completion_type

completion_type是MySQL中一个重要的系统变量,用于控制事务提交后的行为。它决定了在执行COMMIT/COMMIT WORK/ROLLBACK/ROLLBACK WORK后,MySQL如何处理后续的事务。具体枚举说明如下:

  • completion_type=0:提交事务后,当前事务结束,但不会自动开启新事务或断开连接。
  • completion_type=1:提交事务后,自动开启一个相同隔离级别的新事务(相当于 COMMIT AND CHAIN)。若链式事务中某条语句失败,后续语句仍在同一事务内,需显式 ROLLBACK 撤销全部操作。
  • completion_type=2:提交事务后,自动断开当前客户端与服务器的连接(相当于 COMMIT AND RELEASE)。

五、隐式提交SQL

隐式提交是指在MySQL中,某些SQL语句执行后自动触发事务提交 ,无需显式使用COMMIT命令。这意味着在执行这些语句后,事务会立即提交,无法再回滚。

1. DDL语句(数据定义语言)

这些语句用于修改数据库结构,属于DDL,会触发隐式提交:

  • CREATEALTERDROP(表/数据库/索引/视图/存储过程/触发器/事件等)。
  • TRUNCATE TABLERENAME TABLE

注意TRUNCATE TABLE虽然是清空表的操作,和DELETE FROM table效果相似,但它是DDL语句,不能被回滚,这是与SQL Server的主要区别。

2. DCL语句(数据控制语言)

这些语句用于权限管理,涉及用户权限或账户管理的操作会隐式提交,确保权限变更立即生效:
GRANTREVOKE(权限管理)、CREATE USERDROP USERRENAME USERSET PASSWORD

3. 管理语句

这些语句用于数据库维护,通常涉及系统表或元数据更新,需隐式提交保证一致性:

  • ANALYZE TABLECACHE INDEXCHECK TABLELOAD INDEX INTO CACHEOPTIMIZE TABLEREPAIR TABLE(表维护操作)。
  • FLUSH TABLESFLUSH LOGS(刷新表或日志)。

4. 锁与事务控制语句

  • LOCK TABLESUNLOCK TABLES(表级锁操作,可能隐式提交)。
  • SET autocommit = 1(显式开启自动提交,后续操作自动提交)。
  • START TRANSACTION(在已存在事务时,隐式提交前一个事务)。

4. 其他隐式提交场景

  • 临时表操作CREATE TEMPORARY TABLEDROP TEMPORARY TABLE在某些情况下可能不提交事务,但ALTER TABLE等操作仍会提交。
  • 数据加载LOAD DATA(针对NDB引擎)会隐式提交。
  • 复制控制START REPLICA/SLAVESTOP REPLICA等复制相关语句。
  • 客户端断开:连接断开时,未提交事务自动提交。

六、事务的隔离级别

事务隔离级别是数据库管理系统中用于控制并发事务之间可见性和影响程度的机制。它在保证数据一致性的同时,也影响着系统的并发性能。

1. 隔离级别类型

1.1 READ UNCOMMITTED(读未提交)

  • 行为:事务可以读取其他事务未提交的修改(脏读)。

  • 特点

    • 最高并发性,但数据一致性最差。
    • 可能出现脏读 (Dirty Read)、不可重复读 (Non-repeatable Read)、幻读(Phantom Read)。
  • 适用场景:对数据一致性要求极低,允许脏读的场景(如统计类操作)。

  • 示例

    sql 复制代码
    -- 事务A
    START TRANSACTION;
    UPDATE accounts SET balance = 100 WHERE id = 1; -- 未提交
    
    -- 事务B(READ UNCOMMITTED)
    SELECT balance FROM accounts WHERE id = 1; -- 可能读到100(脏读)

1.2 READ COMMITTED(读已提交)

  • 行为:事务只能读取其他事务已提交的修改。

  • 特点

    • 避免脏读 ,但仍可能出现不可重复读幻读
    • Oracle默认级别 ,MySQL通过innodb_locks_unsafe_for_binlog参数模拟(但InnoDB默认使用多版本并发控制MVCC)。
  • 适用场景:需要避免脏读,但对不可重复读和幻读容忍度较高的场景(如银行转账确认)。

  • 示例

    sql 复制代码
    -- 事务A
    START TRANSACTION;
    UPDATE accounts SET balance = 100 WHERE id = 1;
    COMMIT; -- 提交后事务B才能读到100
    
    -- 事务B(READ COMMITTED)
    SELECT balance FROM accounts WHERE id = 1; -- 读到100(无脏读)

1.3 REPEATABLE READ(可重复读,InnoDB默认级别)

  • 行为:事务内多次读取同一数据结果一致(基于MVCC实现)。

  • 特点

    • 避免脏读不可重复读 ,但可能仍出现幻读(InnoDB通过间隙锁Gap Lock解决部分幻读问题)。
    • MVCC机制:通过保存数据快照实现一致性读。
  • 适用场景:大多数业务场景,如订单处理、库存管理等需要事务内数据一致性的操作。

  • 示例

    sql 复制代码
    -- 事务A
    START TRANSACTION;
    SELECT balance FROM accounts WHERE id = 1; -- 读到50
    -- 事务B在此期间提交修改balance为100
    SELECT balance FROM accounts WHERE id = 1; -- 仍读到50(无不可重复读)

1.4 SERIALIZABLE(串行化)

  • 行为:所有事务串行执行,完全隔离。

  • 特点

    • 避免所有并发问题(脏读、不可重复读、幻读),但并发性能最低。
    • 通过**共享锁(S锁)排他锁(X锁)**实现严格隔离。
  • 适用场景:对数据一致性要求极高且并发量低的场景(如金融核心系统)。

  • 示例

    sql 复制代码
    -- 事务A
    START TRANSACTION;
    SELECT * FROM accounts WHERE balance > 100 FOR UPDATE; -- 加X锁
    -- 事务B尝试读取或修改被锁定的数据会被阻塞

2. 设置隔离级别

  • 全局级别 (影响所有新连接):

    sql 复制代码
    SET GLOBAL TRANSACTION ISOLATION LEVEL READ COMMITTED;
  • 会话级别 (仅当前连接):

    sql 复制代码
    SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
  • 事务内设置 (在START TRANSACTION前):

    sql 复制代码
    SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
    START TRANSACTION;

3. 查看隔离级别

3.1 查看当前会话的隔离级别

MySQL 5.7~8.0:

sql 复制代码
SELECT @@transaction_isolation;

MySQL 8.0+:

sql 复制代码
SELECT @@session.transaction_isolation;

MySQL(5.7及以下):

sql 复制代码
SELECT @@tx_isolation;

3.2 查看全局隔离级别

MySQL 5.7+:

sql 复制代码
SELECT @@global.transaction_isolation;

MySQL(5.7及以下):

sql 复制代码
SELECT @@global.tx_isolation;

七、分布式事务

分布式事务是指跨越多个数据库实例、资源管理器或服务的事务。它需要确保所有参与节点要么全部提交,要么全部回滚,以保证数据一致性。

1. XA协议

XA是分布式事务处理的标准协议,由X/Open组织定义。它定义了事务管理器(TM)和资源管理器(RM)之间的接口。以下是XA协议的核心组件:

组件 作用 在MySQL中的实现
事务管理器(TM) 协调分布式事务 MySQL服务器
资源管理器(RM) 管理单个资源(如数据库) InnoDB存储引擎
事务协调器 确保两阶段提交 MySQL内部实现

2. InnoDB对XA事务的支持

  • innodb_support_xa :该参数控制InnoDB是否支持XA事务,默认值为ON,表示支持XA事务。可以通过SHOW VARIABLES LIKE 'innodb_support_xa%';命令查看是否开启。
  • 事务隔离级别:在使用分布式事务时,InnoDB的事务隔离级别必须设置为SERIALIZABLE,以确保事务的严格隔离性。

3. InnoDB XA分布式事务的实现

InnoDB通过实现XA协议支持分布式事务,主要通过以下方式:

3.1. XA事务的SQL接口

命令 作用 说明
XA START xid 开始一个分布式事务 xid是全局唯一事务标识
XA END xid 结束事务,准备提交 标记事务进入准备阶段
XA PREPARE xid 准备提交 将事务状态设置为"预提交"
XA COMMIT xid 提交事务 确认事务提交
XA ROLLBACK xid 回滚事务 撤销事务更改

3.2. 两阶段提交(2PC)实现

InnoDB使用两阶段提交协议来实现分布式事务:
事务管理器(MySQL) 资源管理器1(InnoDB) 资源管理器2(InnoDB) XA START xid 执行SQL操作 XA START xid 执行SQL操作 XA END xid XA END xid XA PREPARE xid XA PREPARE xid XA COMMIT xid XA COMMIT xid XA ROLLBACK xid XA ROLLBACK xid alt [所有RM都准备成功] [有RM准备失败] 事务管理器(MySQL) 资源管理器1(InnoDB) 资源管理器2(InnoDB)

3.3. InnoDB的两阶段提交过程

  • 第一阶段(准备阶段)

    • 所有资源管理器执行事务操作
    • 将事务写入redo log(确保崩溃恢复)
    • 不提交(不更新事务系统表)
    • 返回"准备成功"状态
  • 第二阶段(提交/回滚阶段)

    • 如果所有资源管理器都准备成功,事务管理器发送XA COMMIT
    • 资源管理器将事务提交到磁盘
    • 如果有资源管理器准备失败,事务管理器发送XA ROLLBACK
    • 资源管理器回滚所有更改

4. InnoDB XA事务的内部实现

内部XA事务用于同一实例下跨多引擎事务,由Binlog作为协调者。例如,在一个存储引擎提交时,需要将提交信息写入二进制日志,这就是一个分布式内部XA事务。

4.1. 关键数据结构

数据结构 说明 作用
trx->xid 事务的全局标识 标识分布式事务
trx->state 事务状态 0=活跃,1=准备,2=提交
trx_sys->xid_list XA事务列表 存储所有准备中的XA事务

4.2. XA事务的提交流程

c 复制代码
void innodb_xa_commit(trx_t* trx) {
  /* 1. 检查事务是否为XA事务 */
  if (trx->xid != NULL) {
    /* 2. 执行两阶段提交 */
    if (trx->state == TRX_STATE_PREPARED) {
      /* 3. 写入XID到事务日志 */
      trx->xid->log();
      
      /* 4. 提交事务 */
      trx->state = TRX_STATE_COMMITTED;
      
      /* 5. 通知资源管理器提交 */
      rm_commit(trx);
    }
  }
}

4.3. XA事务的恢复机制

InnoDB在崩溃恢复时,会检查trx_sys->xid_list

c 复制代码
void innodb_xa_recover() {
  for (each xid in trx_sys->xid_list) {
    if (xid->state == PREPARED) {
      /* 1. 检查是否需要提交 */
      if (xid->can_commit) {
        /* 2. 执行XA COMMIT */
        xid->commit();
      } else {
        /* 3. 执行XA ROLLBACK */
        xid->rollback();
      }
    }
  }
}

5. InnoDB XA事务使用Java JTA实现

一般多数据库的XA分布式事务实现需要在应用端作为协调者参与实现,以下是在Java中使用JTA实现分布式事务的代码:

java 复制代码
import javax.transaction.xa.*;
import javax.sql.XADataSource;
import java.sql.Connection;
import java.sql.PreparedStatement;

public class DualDataSourceXaDemo {

    public static void main(String[] args) throws Exception {
        // 1. 配置两个XA数据源(模拟两个独立数据库)
        XADataSource xaDataSource1 = createXaDataSource("jdbc:mysql://localhost:3306/db1", "root", "password1");
        XADataSource xaDataSource2 = createXaDataSource("jdbc:mysql://localhost:3307/db2", "root", "password2");

        // 2. 获取XA连接和资源
        XAResource xaResource1 = getXaResource(xaDataSource1);
        XAResource xaResource2 = getXaResource(xaDataSource2);

        // 3. 创建全局事务ID(需唯一)
        Xid xid = new GlobalXid("tx123"); // 自定义Xid实现

        try {
            // 4. 启动两个分支事务
            xaResource1.start(xid, XAResource.TMNOFLAGS);
            xaResource2.start(xid, XAResource.TMNOFLAGS);

            // 5. 执行数据库操作
            executeUpdate(xaResource1, xaDataSource1, 
                "UPDATE accounts SET balance = balance - 100 WHERE id = 1");
            executeUpdate(xaResource2, xaDataSource2, 
                "UPDATE accounts SET balance = balance + 100 WHERE id = 2");

            // 6. 两阶段提交准备
            int prepare1 = xaResource1.prepare(xid);
            int prepare2 = xaResource2.prepare(xid);

            if (prepare1 == XAResource.XA_OK && prepare2 == XAResource.XA_OK) {
                // 7. 提交事务
                xaResource1.commit(xid, false);
                xaResource2.commit(xid, false);
                System.out.println("跨数据库事务提交成功!");
            } else {
                throw new XAException("准备阶段失败");
            }
        } catch (Exception e) {
            // 8. 回滚所有操作
            xaResource1.rollback(xid);
            xaResource2.rollback(xid);
            System.out.println("事务回滚: " + e.getMessage());
        }
    }

    // 创建XA数据源
    private static XADataSource createXaDataSource(String url, String user, String password) {
        com.mysql.cj.jdbc.MysqlXADataSource xaDataSource = new com.mysql.cj.jdbc.MysqlXADataSource();
        xaDataSource.setURL(url);
        xaDataSource.setUser(user);
        xaDataSource.setPassword(password);
        return xaDataSource;
    }

    // 获取XAResource(简化版,实际需处理连接池)
    private static XAResource getXaResource(XADataSource xaDataSource) throws Exception {
        return xaDataSource.getXAConnection().getConnection().unwrap(XAResource.class);
    }

    // 执行SQL操作
    private static void executeUpdate(XAResource xaResource, XADataSource xaDataSource, String sql) throws Exception {
        try (Connection conn = xaDataSource.getXAConnection().getConnection()) {
            PreparedStatement ps = conn.prepareStatement(sql);
            ps.executeUpdate();
        }
    }

    // 自定义全局事务ID
    static class GlobalXid implements Xid {
        private final byte[] globalId;
        private final byte[] branchId;

        public GlobalXid(String transactionId) {
            this.globalId = transactionId.getBytes(); // 全局事务ID
            this.branchId = "branch".getBytes();      // 分支限定符
        }

        @Override
        public int getFormatId() { return 1; } // MySQL使用Format ID=1

        public byte[] getGlobalTransactionId() { return globalId; }

        public byte[] getBranchQualifier() { return branchId; }
    }
}

八、长事务

长事务指在数据库中运行时间过长、未及时提交或回滚的事务。在InnoDB引擎中,长事务通常指:

  • 事务执行时间超过阈值(如1分钟以上)。
  • 未及时执行COMMITROLLBACK
  • 事务中包含大量数据操作或阻塞操作。

1. 长事务产生原因

  1. 应用层设计问题

    • 事务中包含网络调用(如支付接口、第三方API)。
    • 事务中包含用户交互(如等待用户输入)。
    • 未正确处理事务边界(忘记提交)。
  2. 业务逻辑问题

    • 大批量数据处理未分批提交。
    • 事务范围过大(如一个事务处理百万级数据)。
  3. 系统配置问题

    • innodb_lock_wait_timeout设置过长。
    • 事务隔离级别设置不当。

2. 长事务带来的问题

2.1. 锁资源长期占用,引发并发阻塞

行锁/间隙锁持续持有

  • 长事务执行UPDATEDELETE等操作时,会持有行锁或间隙锁。
  • 其他事务对相同数据的操作将被阻塞,导致:
    • 写阻塞写:其他事务修改同一行数据时需等待。
    • 写阻塞读:在REPEATABLE READ隔离级别下,其他事务的SELECT ... FOR UPDATE或普通读可能被阻塞。

元数据锁(MDL)影响

  • 长事务持有SHARED_READ MDL锁,其他线程的EXCLUSIVE MDL锁请求(如ALTER TABLE)会排队。
  • 极端情况导致整个表的DDL操作长时间阻塞,影响运维变更。

2.2. 回滚日志(Undo Log)膨胀,占用存储与性能

Undo Log持续增长

  • InnoDB用Undo Log实现事务回滚和MVCC。
  • 长事务运行时,会生成大量Undo Log。
  • 未提交事务的Undo Log无法被清理,导致:
    • Undo段(如ibdata1文件或独立Undo表空间)持续膨胀。
    • 即使事务提交,Undo Log需等到purge线程清理(依赖快照过旧判断),影响其他事务的MVCC读性能。

2.3. 主从延迟(Replication Delay)

主从复制影响

  • 长事务在主库执行时间越长,从库应用这些事件的时间也越滞后。
  • 特别是当事务包含大量写操作时,从库需要串行执行,进一步放大延迟。
  • 极端情况下,可能导致从库无法及时恢复,影响故障切换能力。

2.4. 崩溃恢复时间延长

崩溃恢复影响

  • 事务越长,产生的redo日志和undo日志越多。
  • 系统崩溃后,MySQL重启需要进行回滚或重做这些事务。
  • 长事务会显著延长恢复过程,影响数据库可用性。
  • 特别是在大事务提交前崩溃,回滚过程可能非常耗时。

2.5. 资源耗尽风险

内存与连接资源

  • 长时间运行的事务会持续占用会话资源。
  • 包括内存、打开的表、缓存等。
  • 多个长事务并发存在,可能造成:
    • 内存压力上升,触发OOM(内存溢出)。
    • 连接长时间不释放,达到max_connections上限。

3. 长事务的监控

3.1. 通过系统表查询活跃事务

查询超过60秒的长事务。

sql 复制代码
-- 查询超过60秒的长事务
SELECT
    trx_id,
    trx_state,
    trx_started,
    trx_mysql_thread_id,
    trx_query,
    TIME_TO_SEC(TIMEDIFF(NOW(), trx_started)) AS duration_sec
FROM information_schema.innodb_trx
WHERE TIME_TO_SEC(TIMEDIFF(NOW(), trx_started)) > 60
ORDER BY duration_sec DESC;

3.2. 查看锁等待关系

查看当前锁等待关系。

sql 复制代码
-- 查看当前锁等待关系
SELECT 
    r.trx_id waiting_trx_id,
    r.trx_mysql_thread_id waiting_thread,
    r.trx_query waiting_query,
    b.trx_id blocking_trx_id,
    b.trx_mysql_thread_id blocking_thread,
    b.trx_query blocking_query
FROM information_schema.innodb_lock_waits w
INNER JOIN information_schema.innodb_trx b ON b.trx_id = w.blocking_trx_id
INNER JOIN information_schema.innodb_trx r ON r.trx_id = w.requesting_trx_id;

3.3. 监控InnoDB状态

在输出中关注TRANSACTIONS部分,查看活跃事务和锁等待信息。

sql 复制代码
-- 查看InnoDB引擎状态
SHOW ENGINE INNODB STATUS\G;

4. 长事务优化

4.1. 应用层优化

避免在事务中执行阻塞操作

  • 不要在事务中调用外部API(如支付接口、短信服务)。
  • 避免在事务中执行用户交互(如等待用户输入)。

拆分大事务

  • 将大事务拆分为多个小事务,每批处理少量数据。

  • 示例:批量更新百万数据,每5000条提交一次。

    java 复制代码
    // 伪代码示例
    for (int i = 0; i < total; i += batchSize) {
        updateBatch(i, batchSize);
        connection.commit(); // 每批提交
    }

合理设置事务边界

  • 仅在必要时使用事务。
  • 优先使用autocommit=1,避免隐式事务。

4.2. 数据库配置优化

调整关键参数

sql 复制代码
# 增大redo log文件大小,支持更大事务连续写入
innodb_log_file_size = 2G

# 增大redo log缓冲区,减少磁盘I/O
innodb_log_buffer_size = 64M

# 增大缓冲池,减少物理I/O
innodb_buffer_pool_size = 32G  # 通常设置为物理内存的50%-75%

# 适当设置锁等待超时时间
innodb_lock_wait_timeout = 30  # 默认50秒,可适当调小

设置合理的隔离级别

  • 如业务允许,将隔离级别设为READ COMMITTED,减少间隙锁使用。
  • 避免不必要的REPEATABLE READ隔离级别。

4.3. 监控与告警

建立长事务监控机制

sql 复制代码
-- 创建监控脚本,定期检查长事务
CREATE EVENT check_long_transactions
ON SCHEDULE EVERY 1 MINUTE
DO
BEGIN
    SELECT * FROM information_schema.innodb_trx
    WHERE TIME_TO_SEC(TIMEDIFF(NOW(), trx_started)) > 60;
END;

设置告警阈值

  • 长事务超过阈值(如1分钟)自动告警。
  • 通过Prometheus、Grafana等监控工具可视化展示。

4.4. 特殊场景处理

大事务处理

  • 对批量操作进行分批处理,每批控制在几千到一万条。

  • 使用LIMIT和休眠间隔,避免长时间阻塞。

    sql 复制代码
    -- 分批删除示例
    DELETE FROM orders WHERE status = 'expired' LIMIT 5000;
    -- 休眠500ms
    DO SLEEP(0.5);

使用innodb_undo_log_truncate

  • 启用自动清理过期Undo日志。

    ini 复制代码
    innodb_undo_log_truncate = ON
    innodb_undo_tablespaces = 4

5. 长事务的典型案例

案例1:电商平台订单状态更新

  • 问题

    • 一个事务更新百万级订单状态。
    • 未分批提交,导致事务执行时间长达10分钟。
    • 引发大量锁等待,系统响应变慢。
  • 解决方案

    1. 拆分事务为每5000条提交一次。
    2. 设置innodb_lock_wait_timeout = 30
    3. 添加监控脚本,及时发现长事务。
  • 结果

    • 事务执行时间从10分钟缩短至5分钟。
    • 系统并发能力提升40%。
    • 锁等待错误减少90%。

案例2:报表系统数据导出

  • 问题

    • 一个事务执行复杂查询并导出数据。
    • 事务包含大量SELECT ... FOR UPDATE,导致锁持有时间过长。
    • 业务高峰期频繁出现"Lock wait timeout exceeded"错误。
  • 解决方案

    1. 将事务拆分为数据查询和导出两部分。
    2. 数据查询使用READ COMMITTED隔离级别。
    3. 为查询添加LIMIT,分页获取数据。
  • 结果

    • 事务执行时间从15分钟缩短至2分钟。
    • 锁等待错误完全消除。
    • 系统稳定性大幅提升。

总结

InnoDB作为MySQL默认存储引擎,通过ACID特性实现高效事务管理。其核心机制包括:

  • 事务隔离:支持4种隔离级别,默认使用可重复读(REPEATABLE READ),结合MVCC(多版本并发控制)与行锁/间隙锁,避免脏读、不可重复读及幻读问题。
  • 日志系统
    • Redo Log:记录物理修改,保障崩溃恢复与数据持久性。
    • Undo Log:支持事务回滚及MVCC快照读,控制历史版本可见性。
  • 并发控制:通过行级锁减少资源冲突,提升并发性能。
  • 分布式事务 :兼容XA协议,支持跨数据库的两阶段提交(2PC),需事务管理器协调。
  • 长事务优化 :建议拆分大事务,合理设置innodb_lock_wait_timeout等参数,避免锁竞争与Undo日志膨胀。
相关推荐
Knight_AL2 小时前
MySQL STORED 生成列(Generated Column)详解:让 SQL 变快的秘密武器
数据库·sql·mysql
请为小H留灯2 小时前
Java实际开发@常用注解(附实战场景)
java·后端·个人开发
煎蛋学姐2 小时前
SSM社区疫苗接种预约系统hulgj(程序+源码+数据库+调试部署+开发环境)带论文文档1万字以上,文末可获取,系统界面在最后面
数据库·ssm 框架
老华带你飞2 小时前
在线教育|基于springboot + vue在线教育系统(源码+数据库+文档)
java·开发语言·数据库·vue.js·spring boot·后端
路边草随风2 小时前
java操作cosn使用
java·大数据·hadoop
TT哇2 小时前
【项目】玄策五子——匹配模块
java·spring boot·websocket·spring·java-ee·maven
认真敲代码的小火龙2 小时前
【JAVA项目】基于JAVA的医院管理系统
java·开发语言·课程设计
Predestination王瀞潞2 小时前
Java EE开发技术 (报错解决 兼容问题 及 Jakara EE Web 官方手册提供的API接口聚合包)
java·java-ee·jstl·jakara背景
断剑zou天涯2 小时前
【算法笔记】Manacher算法
java·笔记·算法