云计算百科
云计算领域专业知识百科平台

MySQL基础篇之事务

我们常说磁盘 / 内存的管理颗粒是 “块 / 页”,那 MySQL 数据库的执行与管理颗粒是什么?答案是:事务(Transaction)。

在没有学习事务之前,你可以认为之前的每一条单句 SQL 都自成一个事务。而事务的本质,是一组逻辑上不可分割的 SQL 语句集合 —— 要么全部执行成功,要么全部失败回滚,是数据库保证数据可靠性、并发正确性的核心机制。

事务的四大核心特性,简称 ACID:

  • 原子性(Atomicity)
  • 一致性(Consistency)
  • 隔离性(Isolation)
  • 持久性(Durability)
  • 前置说明:MySQL 中只有 InnoDB 存储引擎 完整支持事务,MyISAM 不支持事务,仅擅长只读查询场景。因此下文所有讲解均基于 InnoDB 引擎。


    前置:统一测试表

    下文所有演示均基于这张学生表:

    CREATE TABLE `student` (
    `id` int NOT NULL AUTO_INCREMENT,
    `ename` varchar(10) NOT NULL,
    `age` int NOT NULL DEFAULT '18',
    PRIMARY KEY (`id`)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3;


    一、原子性(Atomicity)

    1.1 核心概念

    原子性指一个事务是不可分割的最小执行单元,最终只会呈现两种状态:要么全部执行完成,要么完全没有执行,不会存在 “执行了一半” 的中间状态。

    1.2 自动提交(autocommit)

    自动提交是 MySQL 的默认行为,也是理解事务的前提。

    • 查看自动提交状态:

      SHOW VARIABLES LIKE 'autocommit';

    • 设置自动提交状态:

      SET autocommit = OFF; — 关闭自动提交,0 等价于 OFF
      SET autocommit = ON; — 开启自动提交,1 等价于 ON

    核心规则
    • autocommit = ON(默认):每一条 SQL 语句都会被自动包裹成一个独立事务,语句执行完自动提交。对用户来说,几乎感知不到事务的存在。
    • autocommit = OFF:当前会话会隐式开启一个事务,所有 SQL 语句都在这个事务内,直到用户手动执行 COMMIT 提交或 ROLLBACK 回滚。
    • 手动开启事务(START TRANSACTION / BEGIN):一旦手动执行 BEGIN 或 START TRANSACTION,无论 autocommit 是开是关,都会开启一个新的显式事务,必须手动提交 / 回滚,不再触发自动提交。

    1.3 事务基础操作与保存点

    — 开启一个显式事务
    START TRANSACTION; — 等价于 BEGIN;

    — 执行DML操作
    INSERT INTO student (ename, age) VALUES ('孙权', 19), ('吕布', 20);

    — 设置保存点 save1
    SAVEPOINT save1;

    INSERT INTO student (ename, age) VALUES ('刘备', 19), ('刘禅', 20);

    SAVEPOINT save2;

    INSERT INTO student (ename, age) VALUES ('赵云', 19), ('张飞', 20);

    SAVEPOINT save3;

    此时查询可以看到 6 条记录。

    部分回滚(回滚到保存点)

    ROLLBACK TO save2;

    执行后,save2 之后的操作(赵云、张飞的插入)全部撤销,表中只剩下 4 条记录。

    • 保存点是事务内部的 “进度锚点”,允许事务内的部分回滚,不破坏事务的原子性;
    • 保存点只能在当前事务内使用,事务提交或回滚后自动失效。
    全部回滚

    ROLLBACK;

    直接回滚整个事务,事务所做的所有修改全部撤销,事务结束,数据回到事务开始前的状态。

    提交事务

    COMMIT;

    提交后,事务所做的所有修改永久生效,事务结束。

    1.4 原子性的本质

    原子性的核心保障是:事务执行过程中发生任何异常(MySQL 崩溃、操作系统崩溃、断电),未提交的事务都会被全部回滚,不会留下半完成的数据。最终事务只有两种结果:提交成功(全部生效)、回滚 / 失败(全部不生效)。


    二、持久性(Durability)

    2.1 核心概念

    持久性指事务一旦提交,其对数据库的修改就是永久的,后续发生任何故障(系统崩溃、断电)都不会丢失已提交的数据。

    2.2 实现基础:Redo Log(重做日志)

    很多初学者会混淆 redo log 和 undo log,这里明确区分:

    • Undo Log(回滚日志):记录数据修改前的版本,用于事务回滚和 MVCC 多版本并发控制;
    • Redo Log(重做日志):记录数据修改后的操作,用于崩溃恢复,保证持久性。

    事务执行 DML 时,不是直接修改磁盘上的数据文件,而是先写入 Redo Log Buffer,再按策略刷到磁盘。当数据库崩溃重启时,通过 redo log 把已提交但还没刷到数据文件的操作重做一遍,保证数据不丢。

    2.3 刷盘策略:innodb_flush_log_at_trx_commit

    该参数控制事务提交时 redo log 的刷盘行为,直接决定了持久性与性能的权衡:

    参数值刷盘时机数据丢失风险性能ACID 符合度
    1(默认) 每次事务提交时,立即将 redo log buffer 刷入磁盘,并执行 fsync 落盘 极低(仅磁盘硬件故障可能丢数据) 最差 严格满足持久性
    0 每秒由后台线程统一刷盘,事务提交不触发刷盘 MySQL 进程崩溃时,丢失约 1 秒内已提交的事务 最好 不满足
    2 事务提交时写到操作系统 Page Cache,每秒刷盘 MySQL 进程崩溃不丢;操作系统崩溃 / 断电时,丢失约 1 秒数据 中等 不严格满足

    生产环境如果要求严格的 ACID 持久性,必须保持默认值 1;追求极致性能可酌情调整,但需接受对应的数据丢失风险。


    三、隔离性(Isolation)

    隔离性是事务最复杂、也是考点最多的部分,决定了并发事务之间的数据可见性。

    3.1 核心概念

    隔离性指多个并发事务同时读写同一数据时,保证数据不被干扰的特性。

    并发操作的三种类型
    • 读读并发:仅读取数据,不做修改,不存在数据安全问题,可完全并行。
    • 写写并发:修改同一行数据时,通过行锁串行化;不同行的写操作可以并行,并非所有写操作都串行。
    • 读写并发:读和写同时进行时的可见性问题,是隔离性讨论的核心。

    3.2 四个隔离级别

    SQL 标准定义了四个隔离级别,从低到高分别是:读未提交、读提交、可重复读、串行化。级别越高,隔离性越强,并发性能越差。

    查看隔离级别

    — 查看全局隔离级别
    SELECT @@global.transaction_isolation;

    — 查看当前会话隔离级别
    SELECT @@session.transaction_isolation;

    修改隔离级别

    — 修改当前会话隔离级别(仅对当前会话生效)
    SET SESSION TRANSACTION ISOLATION LEVEL 级别名称;

    — 修改全局隔离级别(对后续新会话生效,当前会话不生效)
    SET GLOBAL TRANSACTION ISOLATION LEVEL 级别名称;

    补充:MySQL 服务启动时会读取配置文件中的全局参数;修改全局参数后,已有会话不会更新,新会话才会继承;重启服务后全局参数会重置为配置文件中的值。


    级别一:读未提交(READ UNCOMMITTED,RU)
    特性

    一个事务还没提交,它做的修改就能被其他事务看到。

    现象:脏读

    读到了其他事务未提交的 “脏数据”,称为脏读。

    演示

    事务 A:

    BEGIN;
    INSERT INTO student (ename, age) VALUES ('诸葛亮', 39), ('典韦', 50);
    — 不提交

    事务 B(RU 级别):

    SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
    BEGIN;
    SELECT * FROM student WHERE ename IN ('诸葛亮', '典韦');

    结果:事务 B 可以查到这两条未提交的数据。

    评价

    隔离级别最低,并发最高,但脏读问题严重,生产业务中几乎不用。


    级别二:读提交(READ COMMITTED,RC)
    特性

    一个事务只能看到其他事务已经提交的修改;未提交的修改对其他事务不可见。

    解决的问题:脏读
    存在的问题:不可重复读

    同一个事务内,两次读取同一行数据,中间有其他事务提交了修改,两次读到的结果不一样。

    演示

    事务 A:

    BEGIN;
    INSERT INTO student (ename, age) VALUES ('孙策', 19), ('孙膑', 20);
    — 不提交

    事务 B(RC 级别):

    SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
    BEGIN;
    SELECT * FROM student WHERE ename IN ('孙策', '孙膑'); — 查不到

    此时事务 A 提交:

    COMMIT;

    事务 B 再次查询:

    SELECT * FROM student WHERE ename IN ('孙策', '孙膑'); — 可以查到

    同一个事务内,两次同样的查询结果不一致,就是 “不可重复读”。


    级别三:可重复读(REPEATABLE READ,RR)
    特性

    MySQL InnoDB 的默认隔离级别。同一个事务内,多次读取同一行数据的结果始终一致,不受其他事务提交的影响。

    解决的问题:脏读、不可重复读
    存在的问题:幻读(InnoDB 通过 Next-Key Lock 很大程度上解决了幻读)

    其他事务在该范围内插入了新行,当前事务再次查询时就会看到这些新增行,从而导致查询结果行数发生变化,

    演示

    事务 A:

    BEGIN;
    INSERT INTO student (ename, age) VALUES ('孙策', 19), ('孙膑', 20);
    COMMIT;

    事务 B(RR 级别):

    SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
    BEGIN;
    SELECT * FROM student WHERE ename IN ('孙策', '孙膑'); — 查不到

    即使事务 A 已经提交,事务 B 再次查询,依然查不到数据。


    级别四:串行化(SERIALIZABLE)
    特性

    最高隔离级别,所有事务串行执行,不允许并发。

    解决的问题:脏读、不可重复读、幻读
    效果

    所有读操作都会加共享锁,写操作加排他锁,读写、写写都互斥,完全串行。并发性能极差,仅在对数据一致性要求极高的场景使用。

    3.3 隔离级别与问题对应表

    隔离级别脏读不可重复读幻读并发性能
    读未提交 RU ✅ 存在 ✅ 存在 ✅ 存在 最高
    读提交 RC ❌ 解决 ✅ 存在 ✅ 存在
    可重复读 RR ❌ 解决 ❌ 解决 部分解决 中等
    串行化 Serializable ❌ 解决 ❌ 解决 ❌ 解决 最低

    四、一致性(Consistency)

    4.1 核心概念

    一致性指事务执行前后,数据库始终保持数据的正确性与完整性 —— 既符合数据库层面的约束定义(主键、唯一键、外键、数据类型),也符合业务逻辑的预期。

    4.2 一致性与其他三大特性的关系

    ACID 中,一致性是最终目的,原子性、隔离性、持久性是实现一致性的手段。

    • 原子性保证事务没有中间状态,不会破坏数据完整性;
    • 隔离性保证并发操作不会互相干扰,不会产生数据异常;
    • 持久性保证提交后的数据不会丢失。

    三者共同保障了数据库始终处于一致的状态。但一致性同时也依赖于业务逻辑的正确性 —— 如果事务本身的业务逻辑错误(比如给工资扣成负数),数据库层面无法保证业务一致性。


    五、深入隔离性:MVCC 底层原理

    隔离性的核心实现机制是 MVCC(多版本并发控制)。InnoDB 通过 undo log 保留数据的历史版本,配合 ReadView 实现 “快照读”,让读写操作不用加锁也能保证隔离,大幅提升并发性能。

    5.1 Undo Log 与版本链

    回滚的两种实现思路
    • 方案一:记录反向操作(比如 INSERT 对应 DELETE),回滚时执行反向操作。缺点是修改复杂时开销大。
    • 方案二:保留数据的历史版本(快照),回滚时直接恢复旧版本。InnoDB 采用的就是这种方案,通过 undo log 存储历史版本。

    5.2 行的隐藏字段

    InnoDB 每行数据除了用户定义的显式字段,还包含 3 个隐式字段和删除标记:

    隐式字段字节数作用
    DB_TRX_ID 6 字节 最后一次修改(插入 / 更新)该行的事务 ID
    DB_ROLL_PTR 7 字节 回滚指针,指向该行上一个版本在 undo log 中的位置
    DB_ROW_ID 6 字节 隐藏主键。当表没有定义主键时,InnoDB 自动生成该字段作为聚簇索引;有主键则不存在此字段

    另外还有删除标记位:标识该行是否被删除。删除操作本质是打标记,不是物理删除,既高效又支持回滚,和文件系统的数据块删除逻辑类似。

    这也解释了一个常见问题:为什么表没有主键时,全表扫描会很慢?因为没有显式主键时,查询条件不会命中隐藏的聚簇索引,只能全表扫描。

    5.3 版本链的形成

    每当对一行数据做修改时,都会把旧版本写入 undo log,然后通过 DB_ROLL_PTR 形成一条从最新版本指向历史版本的链式结构,称为版本链。

    示例

    初始插入一行(事务 ID=1):

    idenameageDB_TRX_IDDB_ROLL_PTR
    19 张三 20 1 NULL

    执行更新 UPDATE student SET age = 30 WHERE ename = '张三'(事务 ID=1):

  • 原来的行作为旧版本写入 undo log,地址为 0xaa;
  • 当前行修改为新值,更新 DB_ROLL_PTR 指向旧版本地址。
  • 新版本行:

    idenameageDB_TRX_IDDB_ROLL_PTR
    19 张三 30 1 0xaa

    版本链的每个节点,都对应一次修改的历史快照;事务回滚时,沿着指针找回旧版本即可。

    5.4 快照读与 ReadView

    什么是快照

    快照,字面意思就是 "快速拍摄的照片"。

    想象你用手机给一个正在变化的场景拍了一张照片 —— 按下快门的那一刻,画面就被定格了。之后场景再怎么变化,照片里的内容都不会变。

    数据库中的快照也是同一个道理:在某个时间点,给数据库中的数据 "拍一张照片",把那一刻的数据状态完整保存下来。之后不管数据怎么被修改、被其他事务提交,这张 "照片" 里的内容始终不变。

    什么是快照读?

    我们平时执行的普通 SELECT 就是快照读:不加锁,直接读取数据的可见版本,基于 ReadView 判断哪个版本可见。

    与之对应的是当前读:SELECT … FOR UPDATE、INSERT、UPDATE、DELETE,读取最新版本并加锁。

    ReadView 结构

    ReadView 是事务执行快照读时生成的 “读视图”,记录了生成视图那一刻系统中活跃事务的状态,用来判断版本链上的哪个版本对当前事务可见。

    核心结构:

    class ReadView {
    private:
    trx_id_t m_up_limit_id; // 低水位:活跃事务中最小的事务ID
    trx_id_t m_low_limit_id; // 高水位:系统下一个待分配的事务ID(当前最大事务ID+1)
    trx_id_t m_creator_trx_id; // 创建该ReadView的事务ID
    ids_t m_ids; // 生成ReadView时,所有活跃(未提交)事务的ID列表
    // …
    };

    各字段含义:

  • m_ids:生成 ReadView 的瞬间,系统中所有正在活跃、未提交的事务 ID 列表(不包含当前事务自己)。
  • m_up_limit_id(低水位):m_ids 列表中最小的事务 ID。
  • m_low_limit_id(高水位):生成 ReadView 时,系统尚未分配的下一个事务 ID,也就是当前已出现过的最大事务 ID + 1。
  • m_creator_trx_id:创建当前 ReadView 的事务自身的 ID。
  • 5.5 可见性判断规则

    对于版本链上的某一个版本(DB_TRX_ID 为修改该版本的事务 ID),按以下规则判断是否可见:

  • 若 DB_TRX_ID < m_up_limit_id 该版本的修改事务,在 ReadView 生成前就已经提交了,可见。
  • 若 DB_TRX_ID >= m_low_limit_id 该版本的修改事务,是在 ReadView 生成之后才开启的,不可见。
  • 若 DB_TRX_ID 在两者之间
    • 如果 DB_TRX_ID 在 m_ids 列表中:说明该事务在 ReadView 生成时还活跃未提交,不可见;
    • 如果 DB_TRX_ID 不在 m_ids 列表中:说明该事务在 ReadView 生成时已经提交,可见。
  • 如果当前版本不可见,就沿着 DB_ROLL_PTR 找上一个版本,重复判断,直到找到可见的版本。

    5.6 RC 与 RR 的本质区别

    RC 和 RR 隔离级别最核心的区别,就是 ReadView 的生成时机不同:

    1. 读提交(RC)

    每次执行快照读(SELECT),都会重新生成一个新的 ReadView。 所以每次查询都能看到最新提交的事务修改,因此会出现不可重复读。

    2. 可重复读(RR)

    只在事务中第一次执行快照读时,生成一个 ReadView,后续所有快照读都复用这个 ReadView。 所以整个事务内看到的数据版本都是一致的,不受其他事务提交的影响,实现了可重复读。

    举例理解

    假设系统中有事务 2、3、4、5 同时活跃:

    • 事务 3 先提交;
    • 事务 4 执行 SELECT。
    • RC 级别:事务 4 每次 SELECT 都生成新的 ReadView。事务 3 提交后,新的 ReadView 的 m_ids 里就没有 3 了,所以能看到事务 3 的修改。
    • RR 级别:事务 4 第一次 SELECT 生成 ReadView 后就不再变化。即使事务 3 提交了,m_ids 列表也不变,所以还是看不到事务 3 的修改。
    赞(0)
    未经允许不得转载:网硕互联帮助中心 » MySQL基础篇之事务
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!