MySQL 事务管理全解:从 ACID 特性、隔离级别到 MVCC 底层原理 前言大家好我是深耕 MySQL 内核与实战优化的博主。在业务开发中事务是保证数据安全与一致性的核心基石小到银行转账、火车票售票大到电商订单系统都离不开事务的支撑。但很多人对事务的理解只停留在begin/commit/rollback对 ACID 特性、隔离级别、MVCC 底层原理一知半解最终导致线上出现超卖、数据不一致、脏写等严重问题。一. 开篇没有事务会出现什么问题我们先从一个经典的火车票售票系统场景切入这也是所有业务并发场景的缩影有一张火车票库存表tickets数据如下idnamenums10西安 - 兰州1此时有两个客户端同时抢这张票执行逻辑完全一致检查票数是否大于 0如果有票执行卖票逻辑将票数 - 1在没有事务控制的情况下会出现如下时序问题客户端 A 检查票数发现还有 1 张票准备执行更新但还没写入数据库客户端 B 同时检查票数也看到了 1 张票同样进入卖票逻辑客户端 A 执行update tickets set nums nums-1 where id10票数变为 0客户端 B 也执行更新最终票数变为 - 1同一张票被卖了两次出现超卖问题。要解决这个问题就需要让整个抢票流程满足 4 个核心要求抢票的整个过程必须是原子的要么全成要么全败两个抢票的操作不能互相干扰卖票成功后数据必须永久生效不能因为宕机丢失卖票前后库存数据的状态必须是一致的不能出现负数库存。而这 4 个要求正是 MySQL 事务的四大核心特性 ——ACID。二. 什么是事务事务是由一组逻辑相关的 DML 语句组成的执行单元这组语句要么全部执行成功要么全部执行失败回滚是一个不可分割的整体。MySQL 提供了完整的事务机制来保证我们的业务逻辑在并发场景下的数据安全。举个最贴合的例子毕业时学校教务系统要删除你的所有信息需要同时删除你的个人基本信息、各科成绩、在校表现、论坛发帖记录等这多条 SQL 语句组合起来就构成了一个完整的事务 —— 要么所有信息全部删除成功要么一条都不删绝不会出现只删了一半的情况。所以一个完整的事务绝对不是简单的 sql 集合还需要满足如下四个属性原子性一个事务transaction中的所有操作要么全部完成要么全部不完成不会结束在中间某个环节。事务在执行过程中发生错误会被回滚Rollback到事务开始前的状态就像这个事务从来没有执行过一样。一致性在事务开始之前和事务结束以后数据库的完整性没有被破坏。这表示写入的资料必须完全符合所有的预设规则这包含资料的精确度、串联性以及后续数据库可以自发性地完成预定的工作。隔离性数据库允许多个并发事务同时对其数据进行读写和修改的能力隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致。事务隔离分为不同级别包括读未提交Read uncommitted、读提交read committed、可重复读repeatable read和串行化Serializable持久性事务处理结束后对数据的修改就是永久的即便系统故障也不会丢失。上面四个属性可以简称为ACID。原子性Atomicity或称不可分割性 一致性Consistency 隔离性Isolation又称独立性 持久性Durability。三. 事务的四大核心特性ACIDACID 是事务的灵魂也是面试必问的核心考点四个特性环环相扣缺一不可。3.1 原子性Atomicity原子性指的是一个事务中的所有操作要么全部完成要么全部不完成不会结束在中间某个环节。事务在执行过程中发生错误会被回滚Rollback到事务开始前的状态就像这个事务从来没有执行过一样。就像银行转账A 扣钱和 B 加钱必须同时成功只要有一步失败两边的钱都要恢复到初始状态。3.2 一致性Consistency一致性指的是事务开始之前和事务结束以后数据库的完整性约束没有被破坏。这里的完整性包括数据的精确度、串联性、业务规则约束。比如转账场景中转账前后 A 和 B 的账户总金额必须保持一致售票场景中库存不能出现负数。一致性是事务的最终归宿原子性、隔离性、持久性都是为了保证数据库的一致性。3.3 隔离性Isolation隔离性指的是数据库允许多个并发事务同时对数据进行读写和修改隔离性可以防止多个事务并发执行时因为交叉执行导致的数据不一致。多个事务同时操作同一张表、同一行数据时就像多个同学同时在一个教室学习隔离性就是给每个同学拉上了帘子保证大家的学习互不干扰。MySQL 提供了 4 种隔离级别来适配不同的业务并发场景后面会详细拆解。3.4 持久性Durability持久性指的是事务处理结束后对数据的修改就是永久的即便系统故障、数据库宕机修改的数据也不会丢失。事务一旦提交成功数据就会被持久化到磁盘中不会因为任何故障回滚。比如你在 ATM 机取了钱交易提交成功后哪怕银行机房断电你的账户扣款记录也不会消失。四. MySQL 事务的版本支持不是所有 MySQL 引擎都支持事务只有 InnoDB 引擎支持完整的事务特性这也是 InnoDB 成为 MySQL 默认引擎的核心原因之一而 MyISAM 引擎完全不支持事务。4.1 查看数据库支持的引擎我们可以通过以下命令查看当前 MySQL 支持的所有引擎以及事务支持情况-- 表格形式展示 show engines; -- 行形式展示查看更清晰 show engines \G执行后核心结果如下*************************** 1. row *************************** Engine: InnoDB Support: DEFAULT Comment: Supports transactions, row-level locking, and foreign keys Transactions: YES XA: YES Savepoints: YES *************************** 5. row *************************** Engine: MyISAM Support: YES Comment: MyISAM storage engine Transactions: NO XA: NO Savepoints: NO可以清晰看到InnoDB 的Transactions字段为YES支持事务而 MyISAM 为NO不支持事务。五. 事务的提交方式MySQL 事务有两种提交方式自动提交和手动提交我们可以通过参数控制。5.1 查看当前提交方式MySQL 事务有两种提交方式自动提交和手动提交我们可以通过参数控制。show variables like autocommit;默认结果Variable_nameValueautocommitONautocommitON表示开启自动提交默认情况下我们执行的每一条 DML 语句都会被 MySQL 自动封装成一个事务执行完成后自动提交。5.2 修改自动提交模式-- 关闭自动提交 set autocommit0; -- 开启自动提交 set autocommit1;注意autocommit0关闭自动提交后你执行的所有 DML 语句都不会立即生效需要手动执行commit才会提交执行rollback会回滚只要我们执行begin/start transaction手动开启一个事务无论autocommit是开启还是关闭都必须手动commit才会提交。六. 事务的核心操作含完整实操案例我们先创建一张银行账户测试表后续所有演示都基于这张表-- 创建账户表必须使用InnoDB引擎 create table if not exists account ( id int primary key, name varchar(50) not null default , blance decimal(10,2) not null default 0.0 ) engineinnodb default charsetutf8;6.1 事务基础操作语法操作语句作用说明begin;/start transaction;手动开启一个事务推荐使用begin更简洁commit;提交事务将事务中所有修改永久生效rollback;回滚事务撤销事务中所有未提交的修改恢复到事务开启前的状态savepoint 保存点名;创建事务保存点用于部分回滚rollback to savepoint 保存点名;回滚到指定的保存点不影响保存点之前的操作6.2 正常演示事务的开启、保存点、回滚与提交-- 1. 查看自动提交状态默认是开启的 show variables like autocommit; -- 2. 开启一个事务 begin; -- 3. 创建第一个保存点 savepoint save1; -- 4. 插入第一条记录 insert into account values (1, 张三, 100); -- 5. 创建第二个保存点 savepoint save2; -- 6. 插入第二条记录 insert into account values (2, 李四, 10000); -- 7. 查看数据两条记录都存在 select * from account; -- 8. 回滚到save2撤销第二条插入语句 rollback to save2; -- 9. 再次查看只剩张三的记录 select * from account; -- 10. 直接回滚到事务开启前撤销所有操作 rollback; -- 11. 最终查看表内无数据 select * from account;6.3 异常场景验证事务的原子性与持久性通过 4 个核心实验验证了事务的核心特性这里完整还原SQL 全小写实验 1未 commit客户端崩溃MySQL 自动回滚-- 终端A操作 select * from account; -- 表内无数据 show variables like autocommit; -- autocommitON begin; -- 开启事务 insert into account values (1, 张三, 100); select * from account; -- 能看到插入的数据此时未commit -- 此时强制终止终端ctrl\ 异常退出 -- 终端B操作 -- 终端A崩溃前能读到未提交的数据读未提交隔离级别下 select * from account; -- 终端A崩溃后再次查询数据自动回滚表内无数据 select * from account;结论事务未提交时客户端异常崩溃MySQL 会自动回滚整个事务保证原子性。实验 2commit 后客户端崩溃数据永久生效-- 终端A操作 show variables like autocommit; -- autocommitON select * from account; -- 表内无数据 begin; insert into account values (1, 张三, 100); commit; -- 提交事务 -- 强制终止终端ctrl\ 异常退出 -- 终端B操作 -- 终端A崩溃后查询数据依然存在不会回滚 select * from account;结论事务一旦 commit 提交数据就会持久化到磁盘无论客户端是否崩溃数据都不会丢失符合持久性特性。实验 3begin 会自动忽略 autocommit 设置-- 终端A操作 select * from account; -- 已有张三的记录 show variables like autocommit; -- autocommitON set autocommit0; -- 关闭自动提交 show variables like autocommit; -- autocommitOFF begin; -- 开启事务 insert into account values (2, 李四, 10000); select * from account; -- 能看到李四的记录 -- 强制终止终端 -- 终端B操作 -- 终端A崩溃后数据自动回滚只剩张三的记录李四的记录消失 select * from account;结论只要执行begin手动开启事务无论autocommit是开启还是关闭都必须手动commit才会生效未提交的异常退出会自动回滚。实验 4单条 SQL 与自动提交的关系-- 实验一关闭autocommit单条SQL未commit崩溃后回滚 -- 终端A set autocommit0; insert into account values (2, 李四, 10000); select * from account; -- 能看到数据 -- 异常退出终端 -- 终端B查询数据回滚消失 -- 实验二开启autocommit单条SQL自动提交崩溃后不回滚 -- 终端A set autocommit1; insert into account values (2, 李四, 10000); select * from account; -- 能看到数据 -- 异常退出终端 -- 终端B查询数据依然存在已持久化结论autocommitON时InnoDB 会把每一条单 SQL 都封装成事务自动提交autocommitOFF时所有 SQL 都需要手动 commit 才会生效。6.4 事务操作注意事项如果没有设置保存点也可以执行rollback只能回滚到事务开启前的状态前提是事务还没有提交事务一旦执行commit提交就不能再执行rollback回滚了可以选择回滚到任意一个已创建的保存点只有 InnoDB 引擎支持事务和保存点MyISAM 不支持开启事务推荐使用begin或start transaction语义更清晰。七. 事务隔离级别解决并发读写的核心隔离性是 ACID 中最复杂的特性MySQL 通过 4 种隔离级别让我们可以在「数据安全性」和「并发性能」之间做平衡。7.1 并发事务带来的 3 个核心问题在讲解隔离级别之前我们先搞懂并发事务会带来的 3 个经典问题这也是面试必考点问题名称定义说明脏读一个事务读到了另一个事务未提交的修改数据。如果另一个事务回滚读到的数据就是无效的脏数据。不可重复读同一个事务内多次执行相同的 select 语句读到的结果不一样。核心原因是其他事务对数据做了修改 / 删除并提交。幻读同一个事务内多次执行相同的 select 语句第二次读到了第一次没有的新增记录就像出现了幻觉。核心原因是其他事务做了新增并提交。7.2 MySQL 的 4 种隔离级别MySQL 提供了 4 种隔离级别从低到高分别是读未提交read uncommitted、读已提交read committed、可重复读repeatable read、串行化serializable。其中可重复读repeatable read简称 RR是 MySQL 的默认隔离级别。4 种隔离级别对问题的解决能力隔离级别脏读不可重复读幻读加锁读读未提交read uncommitted会发生会发生会发生不加锁读已提交read committed不会发生会发生会发生不加锁可重复读repeatable read不会发生不会发生不会发生MySQL 通过 Next-Key 锁解决不加锁串行化serializable不会发生不会发生不会发生全加锁7.3 隔离级别的查看与设置-- 查看全局隔离级别 select global.tx_isolation; -- 查看当前会话的隔离级别 select session.tx_isolation; select tx_isolation; -- 默认查看当前会话 -- 设置当前会话的隔离级别 set session transaction isolation level 隔离级别名称; -- 设置全局的隔离级别重启客户端后生效 set global transaction isolation level 隔离级别名称; -- 示例设置全局隔离级别为读未提交 set global transaction isolation level read uncommitted; -- 示例设置当前会话隔离级别为串行化 set session transaction isolation level serializable;7.4 4 种隔离级别实操演示所有演示均基于account表初始数据truncate table account; insert into account values (1, 张三, 100.00), (2, 李四, 10000.00);读未提交read uncommitted这是最低的隔离级别几乎没有隔离性会出现脏读生产环境严禁使用-- 先设置全局隔离级别为读未提交重启两个终端生效 set global transaction isolation level read uncommitted; -- 终端A开启事务更新数据不提交 begin; update account set blance123.00 where id1; -- 不执行commit -- 终端B开启事务查询数据 begin; select * from account; -- 直接读到了终端A未提交的123.00出现脏读核心问题脏读读到了其他事务未提交的数据一旦其他事务回滚数据就失效了。读已提交read committed这是大多数数据库Oracle 、SQL Server的默认隔离级别解决了脏读但会出现不可重复读。-- 设置全局隔离级别为读已提交重启两个终端生效 set global transaction isolation level read committed; -- 终端A开启事务 begin; update account set blance321.00 where id1; -- 此时不commit -- 终端B开启事务第一次查询 begin; select * from account where id1; -- 读到的是100.00没有脏读 -- 终端A提交事务 commit; -- 终端B同一个事务内第二次查询 select * from account where id1; -- 读到的是321.00同一个事务内两次查询结果不同出现不可重复读核心问题不可重复读同一个事务内相同的查询语句两次读到的结果不一样。可重复读repeatable readMySQL 默认隔离级别解决了脏读和不可重复读同时通过 Next-Key 锁间隙锁 行锁解决了幻读问题。-- 设置全局隔离级别为可重复读重启两个终端生效 set global transaction isolation level repeatable read; -- 终端A开启事务 begin; update account set blance4321.00 where id1; -- 不commit -- 终端B开启事务第一次查询 begin; select * from account where id1; -- 读到100.00 -- 终端Acommit提交事务 -- 终端B同一个事务内第二次查询 select * from account where id1; -- 还是读到100.00解决了不可重复读 -- 终端B提交事务后再次查询才会读到4321.00 commit; select * from account where id1;幻读验证-- 终端A开启事务 begin; insert into account values (3, 王五, 5432.00); -- 不commit -- 终端B开启事务第一次查询 begin; select * from account; -- 只有2条记录没有王五 -- 终端Acommit提交 -- 终端B同一个事务内第二次查询 select * from account; -- 还是只有2条记录没有读到新增的王五解决了幻读 -- 终端B提交事务后才会读到3条记录 commit; select * from account;串行化serializable最高的隔离级别强制所有事务串行执行完全解决了脏读、不可重复读、幻读但并发性能极差生产环境几乎不使用。-- 设置全局隔离级别为串行化重启两个终端生效 set global transaction isolation level serializable; -- 终端A开启事务执行查询 begin; select * from account; -- 终端B开启事务执行更新 begin; update account set blance1.00 where id1; -- 直接被阻塞直到终端A的事务提交/回滚 -- 终端Acommit提交事务 commit; -- 终端B的update才会执行核心特点所有读写操作都会加锁事务只能串行执行安全性最高但并发性能最低。八. 一致性事务的最终归宿我们前面讲了原子性、隔离性、持久性最终都是为了保证一致性。一致性分为两个层面数据库层面的一致性事务执行前后数据库的完整性约束主键、外键、唯一索引、非空约束等不会被破坏比如主键不会重复、库存不会出现负数。业务层面的一致性这是由我们的业务代码决定的比如转账前后两个账户的总金额不变、订单创建后库存必须扣减、用户下单后优惠券必须标记为已使用。MySQL 的事务机制为我们提供了保证一致性的技术基础原子性、隔离性、持久性但最终的业务一致性还是需要我们的业务代码来保证。一句话总结通过 AID原子性、隔离性、持久性来保证 C一致性。九. 进阶原理MVCC 多版本并发控制很多同学会好奇MySQL 的可重复读隔离级别没有加锁为什么还能解决不可重复读和幻读为什么多个事务同时读写数据不会互相阻塞答案就是MVCCMulti-Version Concurrency Control多版本并发控制它是 MySQL 用来解决「读 - 写冲突」的无锁并发控制机制也是 InnoDB 高性能的核心。9.1 MVCC 解决了什么问题数据库的并发场景分为三种读 - 读不存在任何问题不需要并发控制写 - 写有线程安全问题需要加锁控制会出现更新丢失读 - 写有线程安全问题会出现脏读、不可重复读、幻读传统方案是加锁会导致性能大幅下降。而 MVCC 就是为了解决「读 - 写冲突」实现了读操作不会阻塞写操作写操作也不会阻塞读操作大幅提升了数据库的并发读写性能同时解决了脏读、不可重复读、幻读等隔离性问题。9.2 MVCC 的三大前置知识要理解 MVCC必须先搞懂三个核心基础3 个隐藏字段、undo 日志、Read View。InnoDB 行记录的 3 个隐藏字段我们创建的每一行记录除了我们定义的字段InnoDB 会自动添加 3 个隐藏字段隐藏字段名长度作用说明DB_TRX_ID6 字节最近一次修改插入 / 更新这条记录的事务 ID事务 ID 是单向递增的事务开启时分配。DB_ROLL_PTR7 字节回滚指针指向这条记录的上一个历史版本所有历史版本都保存在 undo 日志中通过这个指针形成版本链。DB_ROW_ID6 字节隐藏的自增主键如果我们的表没有定义主键InnoDB 会自动以这个字段生成聚簇索引如果表有主键这个字段就不会存在。除此之外还有一个隐藏的删除标记字段记录被更新或删除时并不是真的从磁盘删除而是把这个标记置为删除后续由 purge 线程清理。undo 日志undo 日志简单理解就是MySQL 的历史数据快照缓冲区。当我们对一条记录执行update/delete时InnoDB 会先把这条记录的原始数据拷贝到 undo 日志中然后再修改当前记录同时把当前记录的DB_ROLL_PTR指向 undo 日志中的历史版本。多次修改后undo 日志中就会形成一条基于链表的历史版本链事务的回滚、MVCC 的快照读都是基于这个版本链实现的。举个例子插入一条记录insert into student (name,age) values (张三,28);此时事务 ID 为 10undo 日志中没有历史版本事务 11 执行update student set name李四 where id1;先把原始数据拷贝到 undo 日志修改当前记录为李四DB_TRX_ID设为 11DB_ROLL_PTR指向 undo 日志中的张三版本事务 12 执行update student set age38 where id1;再次拷贝当前记录到 undo 日志修改 age 为 38DB_TRX_ID设为 12DB_ROLL_PTR指向李四的版本最终形成「张三→李四→最新记录」的版本链。Read View读视图Read View 是事务执行快照读时生成的一个读视图它记录了生成 Read View 的那一刻MySQL 中当前活跃的已开启但未提交事务 ID 列表。简单理解Read View 就是事务快照读的那一刻给数据库的活跃事务拍了一张照片后续通过这张照片判断版本链中的哪个数据版本对当前事务是可见的。Read View 的 4 个核心字段字段名作用说明m_ids生成 Read View 时系统中所有活跃的未提交事务 ID 列表m_up_limit_id低水位m_ids中最小的事务 ID小于这个 ID 的事务都是已提交的对当前事务可见m_low_limit_id高水位生成 Read View 时系统尚未分配的下一个事务 ID大于等于这个 ID 的事务对当前事务都不可见m_creator_trx_id创建这个 Read View 的当前事务 ID9.3 版本可见性判断规则有了版本链和 Read ViewMySQL 就能判断哪个历史版本对当前事务是可见的规则如下按优先级判断如果版本的DB_TRX_ID m_creator_trx_id说明是当前事务自己修改的可见如果版本的DB_TRX_ID m_up_limit_id说明这个版本的事务在生成 Read View 前就已经提交了可见如果版本的DB_TRX_ID m_low_limit_id说明这个版本的事务是生成 Read View 后才开启的不可见如果版本的DB_TRX_ID在m_up_limit_id和m_low_limit_id之间如果DB_TRX_ID在m_ids列表中说明事务还未提交不可见如果不在m_ids列表中说明事务已经提交可见。如果当前版本不可见就顺着DB_ROLL_PTR找到下一个历史版本重复上面的判断直到找到可见的版本如果所有版本都不可见就查询不到这条记录。9.4 当前读 vs 快照读MVCC 中把 select 查询分为两种类型这是理解 MVCC 的关键快照读读取的是记录的历史版本快照不加锁普通的select * from table就是快照读。MVCC 的无锁并发就是基于快照读实现的。当前读读取的是记录的最新版本会加锁。insert/update/delete以及select ... lock in share mode共享锁、select ... for update排他锁都是当前读。9.5 RC 与 RR 隔离级别的本质区别为什么 RC 级别会出现不可重复读而 RR 级别不会核心原因就是 Read View 的生成时机不同。读已提交RC级别事务中每一次快照读都会生成一个全新的 Read View。所以每次 select都会拿到最新的已提交事务的修改同一个事务内两次 selectRead View 不一样读到的结果就不一样出现不可重复读。可重复读RR级别事务中只有第一次快照读才会生成 Read View后续所有的快照读都复用这个 Read View。所以整个事务生命周期内看到的都是同一个快照无论其他事务怎么修改、提交当前事务读到的结果都一致完美解决了不可重复读同时也解决了幻读。十. 总结与面试重点本文完整覆盖了 MySQL 事务的所有核心知识点从入门到进阶这里给大家总结面试和开发中必须掌握的重点ACID 四大特性原子性、一致性、隔离性、持久性必须能清晰解释每个特性的含义事务的基础操作begin/commit/rollback/savepoint的使用autocommit的作用并发事务的三个问题脏读、不可重复读、幻读必须能说清定义和区别四种隔离级别每个级别能解决什么问题、不能解决什么问题MySQL 的默认级别是什么MVCC 底层原理三个隐藏字段、undo 日志、Read View、可见性规则、RC 与 RR 的本质区别InnoDB 与 MyISAM 的区别核心就是 InnoDB 支持事务、行锁、外键MyISAM 不支持。事务是 MySQL 开发的基础也是面试的重中之重只有吃透了事务的原理才能写出高并发、高可靠的业务代码避免线上数据不一致的严重事故。