MySQL事务:如何保证ACID?MVCC到底如何工作?
文章目录
- 一、事务的概念
- 二、事务的定义与核心需求
- 1. CURD需满足的核心属性
- 2. 一致性的理解
- 三、事务的ACID属性
- 四、事务的版本支持
- 五、事务的提交方式
- 1. 两种提交方式
- 2. 查看与修改提交方式
- 六、事务的常见操作演示
- 1. 准备工作
- 2. 事务的开始与回滚
- 3. 非正常演示:事务的自动回滚与持久性
- 演示1:未commit时客户端崩溃,MySQL自动回滚
- 演示2:commit后客户端崩溃,数据持久化
- 4. 关键结论
- 七、事务隔离级别
- 1. 隔离级别的核心作用
- 2. 四种隔离级别详情
- (1)读未提交(Read Uncommitted)
- (2)读提交(Read Committed)
- (3)可重复读(Repeatable Read)
- (4)串行化(Serializable)
- 3. 隔离级别的查看与设置
- 总结
- 隔离级别对比表
- 八、多版本并发控制(MVCC)
- 事务ID
- undo log
- 模拟MVCC:
- **Read View**:
- RR 级别下的快照读特性:
- RC 级别下的快照读特性:
一、事务的概念
什么是事务?事务应用于那些场景? 简单说,事务就是把数据库里一组相关的操作(比如改数据、删数据的操作)捆成一个“整体”,这个整体要么完完全全做完,要么一点都不做,绝不会出现“做了一半卡住”的情况——就像你网购付款,要么钱扣了、订单成了,要么钱没扣、订单也没成,不会有“钱扣了但订单没生成”这种尴尬情况。
举两个例子,特别好理解:
-
火车票售票:本来只剩1张票,要是不搞事务,A查到有票准备卖(还没改数据库),B又查到有票也卖,最后就会把同1张票卖给两个人。但用事务把“查票数+改票数(减1)”捆成一个整体后,要么A的这整套操作全做完(查完直接改,B再查就没票了),要么A操作失败(比如网络断了),票数回到原来的样子,绝不会出现“卖重复”的问题。
-
删学生信息:学校要删一个毕业学生的所有数据,不只是删他的姓名、电话这些基本信息,还要删他的成绩、在校表现、论坛发帖记录。这一堆删数据的操作要是分开做,万一删完基本信息后数据库崩了,就会剩下“没删的成绩”——这就乱了。但用事务把这些删操作捆一起,要么所有信息全删干净,要么数据库崩了之后,之前删的基本信息也会恢复(就像没删过一样),不会留下半截数据。
总结:事务 (Transaction) 是数据库中一组不可分割的操作集合,这组操作要么全部成功执行,要么全部失败回滚,最终保证数据从一个一致性状态切换到另一个一致性状态,而不会出现其他未被定义的状态。
二、事务的定义与核心需求
1. CURD需满足的核心属性
- 原子性(Atomicity):事务中的所有操作要么全做,要么全不做(例如转账时 “扣钱” 和 “加钱” 必须同时成功或同时失败)。
- 一致性(Consistency):事务执行前后,数据必须满足预设的业务规则(例如转账后总金额不变)。
- 隔离性(Isolation):多个事务并发执行时,彼此的操作应相互隔离,避免干扰(例如防止 “脏读”“不可重复读” 等问题)。
- 持久性(Durability):事务一旦提交,其结果会永久保存到数据库(即使宕机也不会丢失)。
2. 一致性的理解
一致性的核心定义是“状态的合法跃迁”:事务执行的结果,必须让数据库从一个合法的一致性状态,转变为另一个合法的一致性状态。
- 一致性与 “业务逻辑强相关”
一致性的 “合法性” 是由用户的业务规则定义的,数据库本身无法凭空判断业务是否合理。
比如:业务规则要求 “用户年龄不能为负数”,MySQL 可以通过字段类型(如INT)或约束(CHECK age >= 0)提供技术支持,但 “为什么年龄不能为负” 是业务逻辑决定的,数据库只是工具,最终由用户的业务需求来定义 “什么样的状态是一致的”。 - 技术上通过 AID 保障 C(原子性、隔离性、持久性共同支撑一致性)
原子性(A):避免事务 “做一半” 导致数据残缺,确保状态跃迁的 “完整性”。
隔离性(I):避免多事务并发时互相干扰(如脏读、幻读),确保每个事务看到的是 “合法的中间状态”。
持久性(D):确保事务提交后数据不会丢失,保证 “合法状态的长期有效性”。
三、事务的ACID属性
事务需满足ACID四大属性,确保数据安全与一致性:
| 属性 | 定义与说明 |
|---|---|
| 原子性(A) | 事务中的所有操作要么全完成,要么全不完成;执行出错时会回滚到事务开始前状态,如同未执行过。 |
| 一致性(C) | 事务开始前和结束后,数据库完整性未被破坏(如数据精度、关联性符合预设规则),需结合业务逻辑与原子性保障。 |
| 隔离性(I) | 允许多个并发事务读写数据,防止交叉执行导致数据不一致;分为读未提交、读提交、可重复读、串行化4个隔离级别。 |
| 持久性(D) | 事务处理结束后,数据修改永久有效,即使系统故障也不会丢失(如commit后的数据)。 |
四、事务的版本支持
- 仅InnoDB引擎支持事务,MyISAM、MEMORY、CSV等引擎不支持事务。
- 查看数据库引擎命令:
mysql> show engines; -- 表格显示 mysql> show engines \G -- 行显示(更详细)

- 关键引擎支持情况:
- InnoDB:DEFAULT引擎,支持事务、行级锁、外键、事务保存点(
Savepoints: YES)。 - MyISAM:不支持事务(
Transactions: NO),适用于只读或低频更新场景。
- InnoDB:DEFAULT引擎,支持事务、行级锁、外键、事务保存点(
五、事务的提交方式
1. 两种提交方式
- 自动提交:默认模式,每条SQL(除select特殊情况)自动封装为事务并提交。
- 手动提交:需通过
commit提交事务,rollback回滚事务。
2. 查看与修改提交方式
-
查看自动提交状态:
mysql> show variables like 'autocommit'; -- 结果为ON(自动)/OFF(手动)
-
修改自动提交模式:
mysql> SET AUTOCOMMIT=0; -- 关闭自动提交(手动模式) mysql> SET AUTOCOMMIT=1; -- 开启自动提交(默认模式)
六、事务的常见操作演示
1. 准备工作
- 创建测试表(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 )ENGINE=InnoDB DEFAULT CHARSET=UTF8; - 远程连接与隔离级别设置(示例:设为读未提交用于测试,后续会讲解这一状态):
mysql> set global transaction isolation level READ UNCOMMITTED; -- 全局设置 mysql> quit; -- 重启客户端生效 mysql> select @@transaction_isolation; -- 验证事务隔离级别(结果:READ-UNCOMMITTED)
2. 事务的开始与回滚
-- 1. 查看自动提交状态(设为ON)
mysql> show variables like 'autocommit'; -- 结果:ON-- 2. 开始事务(begin/start transaction均可)
mysql> begin;-- 3. 创建保存点(用于部分回滚)
mysql> savepoint s1;-- 4. 插入数据
mysql> insert into account values(1,'小明',59.99); -- 插入1条mysql> savepoint s2; -- 新建保存点mysql> insert into account values(2,'小张',69.99); -- 再插入1条-- 5. 查看数据(2条记录)
mysql> select * from account; -- 结果:小明和小张-- 6. 回滚到save2(删除第2条记录)
mysql> rollback to s2;
mysql> select * from account; -- 结果:仅小明-- 7. 全量回滚(删除所有记录)
mysql> rollback;
mysql> select * from account; -- 结果:空集

3. 非正常演示:事务的自动回滚与持久性
演示1:未commit时客户端崩溃,MySQL自动回滚
- 终端A(未commit):
mysql> begin; mysql> insert into account values (1, '张三', 100); -- 未commit mysql> Aborted; -- 按ctrl+\异常终止 - 终端B(查看结果):
mysql> select * from account; -- 终端A崩溃前:有张三(100) mysql> select * from account; -- 终端A崩溃后:空集(自动回滚)

演示2:commit后客户端崩溃,数据持久化
- 终端A(已commit):
mysql> begin; mysql> insert into account values(1,'小张',69.99); mysql> commit; -- 提交事务 mysql> Aborted; -- 异常终止 - 终端B(查看结果):
mysql> select * from account; -- 结果:小张(69.99)

4. 关键结论
- 只要用
begin/start transaction开启事务,必须通过commit提交才持久化,与autocommit设置无关。 - 事务可手动
rollback,操作异常时MySQL自动回滚。 - InnoDB中,单条SQL默认封装为事务并自动提交。
- 从上面的例子,我们能看到事务本身的原子性(回滚),持久性(commit)。
七、事务隔离级别
1. 隔离级别的核心作用
解决多事务并发时的问题(如脏读、不可重复读、幻读),不同级别平衡“安全性”与“并发性能”(级别越严,性能越低)。
2. 四种隔离级别详情
(1)读未提交(Read Uncommitted)
- 特点:所有事务可看到其他事务
未提交的结果,无隔离性。 - 问题:存在脏读(读取未提交的临时数据),一个事务在执行中,读到另一个执行中事务的更新(或其他操作)但是未commit的数据就是脏读。
- 实际生产中不可能使用这种隔离级别,相当于没有任何隔离性,也会有很多并发问题!
(2)读提交(Read Committed)
- 特点:事务仅能看到其他事务
已提交的结果(大多数数据库默认级别,非MySQL默认),它满足了隔离的简单定义:一个事务只能看到其他的已经提交的事务所做的改变。 - 问题:这种隔离级别会引起不可重复读,即一个事务执行时,如果多次 select, 可能得到不同的结果。
(3)可重复读(Repeatable Read)
- 特点:这是 MySQL 默认的隔离级别,它确保同一个事务,在执行中,多次读取操作数据时,会看到同样的数据行。(两个事务都进行提交后才可以看到改变的结果)。
- 优势:解决不可重复读,但可能存在幻读:同一事务内多次执行相同的查询语句,得到的结果集行数不一致,就像 “出现了幻觉” 一样。
- 可重复读:其他事务对范围外的行执行了
INSERT或DELETE并提交,不可重复读:其他事务对已有行执行了UPDATE并提交,这样可以简单区别开可重复读与不可重复读。 - MySQL通过Next-Key锁解决幻读(其他数据库可能仍有幻读)。
(4)串行化(Serializable)
- 特点:这是事务的最高隔离级别,它通过强制事务排序,使之不可能相互冲突,在每个读的数据行上面加上共享锁,从而解决了幻读的问题,。
- 问题:效率极低,易超时和锁竞争,生产环境基本不使用。
3. 隔离级别的查看与设置
-
查看隔离级别:
mysql> SELECT @@global.transaction_isolation; -- 全局 mysql> SELECT @@session.transaction_isolation; -- 或简写为 SELECT @@transaction_isolation; -- 会话级别(当前终端) mysql> SELECT @@tx_isolation; -- 默认同会话级别
-
设置隔离级别:
-- 设置会话/全局级别,可选值:READ UNCOMMITTED/READ COMMITTED/REPEATABLE READ/SERIALIZABLE SET [SESSION | GLOBAL] TRANSACTION ISOLATION LEVEL 隔离级别; -
注意:设置全局级别后,需重启客户端生效。
总结
- 隔离级别越严格,安全性越高,但数据库的并发性能也就越低,往往需要在两者之间找一个平衡点。
- 不可重复读的核心是数据的
“修改 / 删除”操作导致同一行数据在事务内多次读取时内容不一致;幻读的核心是数据的“新增”操作导致同一查询在事务内多次执行时,结果集的行数不一致。 - mysql默认的隔离级别是可重复读,一般情况下不要修改。
- 事务也有长短事务的概念。事务间互相影响,指的是事务在并行执行且都未commit的时候,影响会比较大。
隔离级别对比表
| 隔离级别 | 脏读 | 不可重复读 | 幻读 | 加锁读 |
|---|---|---|---|---|
| 读未提交 (read uncommitted) | ✔ | ✔ | ✔ | 不加锁 |
| 读已提交 (read committed) | ✖ | ✔ | ✔ | 不加锁 |
| 可重复读 (repeatable read) | ✖ | ✖ | ✖ | 不加锁 |
| 可串行化 (serializable) | ✖ | ✖ | ✖ | 加锁 |
八、多版本并发控制(MVCC)
数据库并发的场景有三种:
- 读-读:不存在任何问题,也不需要并发控制
- 读-写:有线程安全问题,可能会造成事务隔离性问题,可能遇到脏读,幻读,不可重复读 (重点)
- 写-写:有线程安全问题,可能会存在更新丢失问题,比如第一类更新丢失,第二类更新丢失(由于大多数是读写并发,这类情况不做重点讲解)
下面用读写并发的场景来讲解MVCC的机制:
多版本并发控制( MVCC )是一种用来解决读-写冲突的无锁并发控制,为事务分配单向增长的事务ID,为每个修改保存一个版本,版本与事务ID关联,读操作只读该事务开始前的数据库的快照。
所以MVCC 可以为数据库解决以下问题:在并发读写数据库时,可以做到在读操作时不用阻塞写操作,写操作也不用阻塞读操作,提高了数据库并发读写的性能,同时还可以解决脏读,幻读,不可重复读等事务隔离问题,但不能解决更新丢失问题,下面从三个角度来理解MVCC:
事务ID
事务拥有ID决定先后顺序,并用结构体组织,数据结构管理,其中3个记录隐藏列字段:
DB_TRX_ID:6 byte,最近修改( 修改/插入)事务ID,记录创建这条记录/最后一次修改该记录的事务IDDB_ROLL_PTR: 7 byte,回滚指针,指向这条记录的上一个版本(简单理解成,指向历史版本就行,这些数据一般在undo log 中)DB_ROW_ID: 6 byte,隐含的自增ID(隐藏主键),如果数据表没有主键,InnoDB会自动以DB_ROW_ID 产生一个聚簇索引,这就是隐藏主键。- 补充:实际还有一个删除
flag隐藏字段, 记录是否被删除。
undo log
undo log 是数据库 (如 MySQL InnoDB) 中用于事务回滚和MVCC(多版本并发控制) 的核心日志,记录数据修改前的原始状态。
-
问题 1:undo 储存在哪里?
在 MySQL 的InnoDB存储引擎中,undo 日志存储在 “回滚段(Rollback Segment)” 中。InnoDB 会为每个数据库实例维护若干个回滚段,每个回滚段又包含多个 undo 日志段,用于管理和存储 undo 日志的内容。(初步理解为存储在mysql开辟的内存中) -
问题 2:undo 的作用是什么?
- 事务回滚(保障原子性):当事务执行失败(如触发约束、系统崩溃)时,数据库可通过 undo 日志反向执行操作,将数据恢复到事务开始前的状态。
- 支持多版本并发控制(MVCC):为事务提供数据的历史版本,让不同事务能在并发时读取到 “一致性快照”,避免锁竞争,提升并发性能。
模拟MVCC:
插入数据时版本链的形成:(undo log中的数据可以称为快照)

tips:
- 如果当前只有一个事务修改后提交,undo log会被清空
- 如果有多个事务,某个事务执行插入并且
commit了,那么undo log会被清空,但如果是update或者delete则不一定,上面模拟的是update的版本链,如果是delete,那么修改的是flag仍然有版本链,如果是insert,那么不存在版本链,但是undo log中会插入delete这条数据供事务回滚时删除这条记录! - 来介绍
当前读和快照读:- 当前读:读取数据的 “最新版本”,并且会对读取的记录加锁(共享锁或排他锁),确保其他事务无法同时修改该记录,从而保证读取到的数据是 “当前时刻最新的、未被其他事务干扰的”,增删改都是当前读,select也可以当前读。
- 触发
当前读的场景:
带锁的查询语句:
SELECT … FOR UPDATE(加排他锁,阻止其他事务修改或加排他锁)
SELECT … LOCK IN SHARE MODE(加共享锁,阻止其他事务加排他锁,但允许读)
写操作(本质是 “先读后写”,读取阶段需要当前读):
INSERT、UPDATE、DELETE(执行时会先读取最新数据,再进行修改,读取阶段加锁) - 快照读:读取数据的历史版本,通过数据库的多版本控制(MVCC)机制实现,不加锁,因此不会阻塞其他事务的读写操作,能显著提升并发效率。
- 触发
快照读的场景:
普通的 SELECT 语句(不包含 FOR UPDATE 或 LOCK IN SHARE MODE) - 读写并发就是因为读写根据的是不同版本,隔离级别可以决定具体可以看到哪些版本,看到不同版本就体现出来隔离性。
Read View:
事务进行快照读操作的时候会生产读视图(Read View),在该事务执行的快照读的那一刻,会生成数据库系统当前的一个快照,记录并维护系统当前活跃事务的ID(当每个事务开启时,都会被分配一个ID, 这个ID是递增的,所以最新的事务,ID值越大),Read View 在源码中就是一个类,本质是用来进行可见性判断的。 即当我们某个事务执行快照读的时候,对该记录创建一个Read View 读视图,把它比作条件,用来判断当前事务能够看到哪个版本的数据,既可能是当前最新的数据,也有可能是该行记录的undo log 里面的某个版本的数据。 其中重要数据项包括:
- m_ids:是一个列表,用于维护
Read View生成时刻,系统中正在活跃的事务 ID。换句话说,就是在创建这个Read View时,所有还没提交的事务的 ID 都会被记录在这里。 - min_trx_id或者up_limit_id:记录m_ids列表中事务 ID 最小的值。它用于界定 “活跃事务中最早启动的那个”,是判断数据版本可见性的边界之一。
- max_trx_id或者low_limit_id:表示
Read View生成时刻系统尚未分配的下一个事务 ID,也就是 “目前已出现过的事务 ID 的最大值 + 1”。可以理解为 “未来事务 ID 的起始值”,用于区分 “生成Read View 之后才启动的事务”。 - creator_trx_id:指创建该 Read View 的事务自身的 ID。用于判断当前事务修改的数据对自身的可见性(事务内的修改对自己是可见的)。
以 “时间轴” 为核心,将事务分为三类:
- 已经提交的事务:在 Read View 生成前就已提交的事务,其修改对当前事务可能可见。
- 正在操作的事务(活跃事务):Read View 生成时还在执行、未提交的事务,其修改对当前事务不可见。
- 快照后新来的事务:Read View 生成后才启动的事务,其修改对当前事务不可见。
可见性判断规则:
通过数据版本的事务 ID(DB_TRX_ID) 与 Read View 字段对比,确定是否可见:
- 已经提交的事务:
若 creator_trx_id == DB_TRX_ID(自身修改),或 DB_TRX_ID < up_limit_id(快照前已提交的事务)则数据可见。 - 正在操作的事务:
所有活跃事务的 ID 都在m_ids中,若DB_TRX_ID在m_ids里,说明事务未提交,数据不可见。
(思维误区:m_ids里的事务 ID 不一定连续,比如 11、13、15 号事务活跃,12、14 号已提交,以m_ids是 {11,13,15}。) - 快照后新来的事务:
若 DB_TRX_ID >= low_limit_id,说明是快照后才启动的事务,数据不可见。
RR(可重复读)与 RC(读已提交)隔离级别的本质区别,核心在于快照读时 Read View 的生成时机不同,这直接导致了两者快照读结果的差异,具体如下:
RR 级别下的快照读特性:
事务中首次执行快照读时,会创建一个快照及对应的 Read View,该Read View会记录此时系统中所有活跃(未提交)的其他事务信息。
此后,该事务内的所有快照读都会复用这个 Read View,不会重新生成。
因此:
- 若当前事务在其他事务提交修改前已执行过快照读,后续快照读仍会基于首次生成的 Read View 判断可见性,其他事务的后续修改(即使提交)也无法被当前事务的快照读看到,从而保证了“可重复读”。
- 只有在 Read View 生成前已提交的事务所做的修改,才对当前事务可见。
RC 级别下的快照读特性:
事务中每次执行快照读时,都会重新生成一个新的 Read View,每次都获取当前最新的活跃事务状态。
因此:
- 每次快照读都能看到**“最新已提交事务”**的修改,这会导致同一事务内多次快照读可能返回不同结果(即“不可重复读”)。
