MySQL事务与MVCC机制深度解析:从ACID到幻读彻底解决

关键词:MySQL事务, ACID, 隔离级别, MVCC, 脏读, 不可重复读, 幻读, ReadView

面试中你是否被问过"什么是ACID特性?"、“脏读、不可重复读、幻读有什么区别?”、"MVCC是怎么实现的?"这些问题看似简单,但要想回答得完整且深入,需要对MySQL的事务机制有系统性的理解。本文将从事务的基本概念出发,深入剖析四种隔离级别,详解MVCC多版本并发控制的实现原理,帮你彻底搞懂MySQL事务的核心机制。


目录

  1. 为什么需要事务
  2. 事务的ACID特性
  3. 事务并发引发的问题
  4. SQL标准的四种隔离级别
  5. MySQL中的隔离级别设置
  6. MySQL事务基本语法
  7. MVCC多版本并发控制

1. 为什么需要事务

事务是数据库管理系统(DBMS)执行过程中的一个逻辑单位(不可再进行分割),由一个有限的数据库操作序列构成(多个DML语句,select语句不包含事务),要不全部成功,要不全部不成功

经典场景:银行转账

假设A给B转账1000元:

  1. A的账户-1000元
  2. B的账户+1000元

这两个UPDATE语句必须作为一个整体来执行,否则会出现A扣钱了但B没收到钱的错误情况。事务可以保证A、B账户的变动要么全部一起发生,要么全部一起不发生


2. 事务的ACID特性

事务应该具有4个属性:原子性、一致性、隔离性、持久性。这四个属性通常称为ACID特性

2.1 原子性(Atomicity)

一个事务必须被视为一个不可分割的最小单元,整个事务中的所有操作要么全部提交成功,要么全部失败,对于一个事务来说,不能只执行其中的一部分操作。

示例:连老师借给李老师1000元

  • 连老师工资卡扣除1000元
  • 李老师工资卡增加1000元

整个事务的操作要么全部成功,要么全部失败,不能出现连老师工资卡扣除但李老师工资卡不增加的情况。

2.2 一致性(Consistency)

一致性是指事务将数据库从一种一致性状态转换到另外一种一致性状态,在事务开始之前和事务结束之后,数据库中数据的完整性没有被破坏

继续上面的例子:扣除的钱(-1000)与增加的钱(+1000)相加应该为0,或者说连老师和李老师的账户的钱加起来,前后应该不变。

2.3 持久性(Durability)

一旦事务提交,则其所做的修改就会永久保存到数据库中。此时即使系统崩溃,已经提交的修改数据也不会丢失。

2.4 隔离性(Isolation)

一个事务的执行不能被其他事务干扰。即一个事务内部的操作及使用的数据对并发的其他事务是隔离的,并发执行的各个事务之间不能互相干扰。

如果隔离性不能保证,会导致什么问题?

假设连老师借给李老师生活费,借了两次,每次都是1000。连老师的卡里开始有10000,李老师的卡里开始有500。从理论上,借完后,连老师的卡里有8000,李老师的卡里应该有2500。

但在真实的数据库中,两次转账操作T1和T2可能交替执行,如果执行顺序不当,可能导致连老师只扣了1000元,但李老师却多了10000元,银行岂不是要亏死?

所以,隔离性的核心目标是:保证一个事务的执行不会被其他事务干扰


3. 事务并发引发的问题

MySQL是一个客户端/服务器架构的软件,对于同一个服务器,可以有若干个客户端与之连接。服务器可能同时处理多个事务。当我们舍弃隔离性来提升性能时,可能会带来以下数据问题:

3.1 脏读(Dirty Read)

定义:当一个事务读取到了另外一个事务修改但未提交的数据,被称为脏读。

场景

  1. 事务A执行过程中,对数据资源进行了修改
  2. 事务B读取了事务A修改后的数据
  3. 由于某些原因,事务A并没有完成提交,发生了RollBack操作
  4. 事务B读取的数据就是脏数据

影响:读取到另一个事务未提交的数据,可能导致数据不一致。

3.2 不可重复读(Non-Repeatable Read)

定义:当事务内相同的记录被检索两次,且两次得到的结果不同时,此现象称为不可重复读。

场景

  • 事务B读取了两次数据资源
  • 在这两次读取的过程中,事务A修改了数据
  • 导致事务B在这两次读取出来的数据不一致

与脏读的区别:不可重复读读取的是已提交的数据,而脏读读取的是未提交的数据。

3.3 幻读(Phantom Read)

定义:在事务执行过程中,另一个事务将新记录添加到正在读取的事务中时,会发生幻读。

场景

  • 事务B前后两次读取同一个范围的数据
  • 在事务B两次读取的过程中,事务A新增了数据
  • 导致事务B后一次读取到前一次查询没有看到的行

与不可重复读的区别

  • 不可重复读重点在于同一行数据被修改
  • 幻读重点在于读取到了之前读取没有获取到的新记录(新增或删除)

问题严重程度排序脏读 > 不可重复读 > 幻读


4. SQL标准的四种隔离级别

SQL标准设立了4个隔离级别,隔离级别越低,越严重的问题就越可能发生:

隔离级别脏读不可重复读幻读说明
READ UNCOMMITTED可能可能可能未提交读,性能最高但问题最多
READ COMMITTED不可能可能可能已提交读,Oracle默认级别
REPEATABLE READ不可能不可能可能可重复读,MySQL默认级别
SERIALIZABLE不可能不可能不可能可串行化,性能最低但最安全

注意:MySQL在REPEATABLE READ隔离级别下,是可以很大程度避免幻读问题发生的(但并不能完全解决)。


5. MySQL中的隔离级别设置

MySQL的默认隔离级别为REPEATABLE READ,我们可以手动修改事务的隔离级别。

5.1 修改隔离级别的语法

SET [GLOBAL|SESSION] TRANSACTION ISOLATION LEVEL level;

其中level可选值有4个:

  • REPEATABLE READ
  • READ COMMITTED
  • READ UNCOMMITTED
  • SERIALIZABLE

5.2 三种设置范围

GLOBAL关键字(全局范围)
SET GLOBAL TRANSACTION ISOLATION LEVEL SERIALIZABLE;

作用:只对执行完该语句之后产生的会话起作用。当前已经存在的会话无效。

SESSION关键字(会话范围)
SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE;

作用:对当前会话的所有后续的事务有效。该语句可以在已经开启的事务中间执行,但不会影响当前正在执行的事务。

不加关键字(仅下一个事务)
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;

作用:只对当前会话中下一个即将开启的事务有效。下一个事务执行完后,后续事务将恢复到之前的隔离级别。该语句不能在已经开启的事务中间执行,会报错。

5.3 查看当前隔离级别

-- 方法1
SHOW VARIABLES LIKE 'transaction_isolation';

-- 方法2(更简便)
SELECT @@transaction_isolation;

6. MySQL事务基本语法

6.1 事务开始

有三种方式可以开始一个事务:

BEGIN;
-- 或
START TRANSACTION;  -- 推荐
-- 或
BEGIN WORK;

6.2 事务回滚与提交

-- 事务回滚(撤销所有操作)
ROLLBACK;

-- 事务提交(保存所有操作)
COMMIT;

6.3 保存点(Savepoint)

如果你开启了一个事务,执行了很多语句,忽然发现某条语句有点问题,可以使用保存点来回滚到指定位置,而不是回到最初的原点。

-- 定义保存点
SAVEPOINT 保存点名称;

-- 回滚到指定保存点
ROLLBACK TO [SAVEPOINT] 保存点名称;

-- 删除保存点
RELEASE SAVEPOINT 保存点名称;

6.4 隐式提交

某些特殊的语句会导致事务自动提交,这种因为某些特殊的语句而导致事务提交的情况称为隐式提交

会导致隐式提交的语句

  1. DDL语句:CREATE、ALTER、DROP等定义或修改数据库对象的语句
  2. 用户管理语句:ALTER USER、CREATE USER、DROP USER、GRANT等
  3. 事务控制语句:在一个事务未提交时开启新事务(再次执行BEGIN)
  4. 锁定语句:LOCK TABLES、UNLOCK TABLES
  5. 数据加载语句:LOAD DATA
  6. 复制相关语句:START SLAVE、STOP SLAVE等
  7. 其他语句:ANALYZE TABLE、FLUSH、OPTIMIZE TABLE等

重要提示:在事务中执行DDL语句(如CREATE TABLE)会导致当前事务自动提交!


7. MVCC多版本并发控制

MVCC(Multi-Version Concurrency Control)即多版本并发控制,主要是为了提高数据库的并发性能

同一行数据平时发生读写请求时,会上锁阻塞住。但MVCC用更好的方式去处理读-写请求,做到在发生读-写请求冲突时不用加锁

7.1 版本链

对于使用InnoDB存储引擎的表,它的聚簇索引记录中包含两个必要的隐藏列

  • trx_id:每次一个事务对某条聚簇索引记录进行改动时,都会把该事务的事务id赋值给trx_id隐藏列
  • roll_pointer:每次对某条聚簇索引记录进行改动时,都会把旧的版本写入到undo日志中,然后这个隐藏列就相当于一个指针,可以通过它来找到该记录修改前的信息

版本链的形成

每次对记录进行改动,都会记录一条undo日志。每条undo日志也都有一个roll_pointer属性(INSERT操作对应的undo日志没有该属性),可以将这些undo日志都连起来,串成一个链表。对该记录每次更新后,都会将旧值放到一条undo日志中,就算是该记录的一个旧版本。随着更新次数的增多,所有的版本都会被roll_pointer属性连接成一个链表,我们把这个链表称之为版本链

版本链的作用:可以利用这个记录的版本链来控制并发事务访问相同记录的行为,这种机制就被称之为多版本并发控制(MVCC)

7.2 ReadView

ReadView是InnoDB为了实现MVCC而提出的一个概念,主要包含4个重要内容:

属性说明
m_ids生成ReadView时当前系统中活跃的读写事务的事务id列表
min_trx_idm_ids中的最小值
max_trx_id生成ReadView时系统中应该分配给下一个事务的id值
creator_trx_id生成该ReadView的事务的事务id

可见性判断规则

  1. 如果被访问版本的trx_id等于creator_trx_id,说明是当前事务自己修改的记录,可见
  2. 如果被访问版本的trx_id小于min_trx_id,说明生成该版本的事务在当前事务生成ReadView前已经提交,可见
  3. 如果被访问版本的trx_id大于等于max_trx_id,说明生成该版本的事务在当前事务生成ReadView后才开启,不可见
  4. 如果被访问版本的trx_id在min_trx_id和max_trx_id之间,需要判断是否在m_ids列表中:
    • 如果在m_ids中,说明事务还是活跃的,不可见
    • 如果不在m_ids中,说明事务已经被提交,可见

7.3 READ COMMITTED实现

READ COMMITTED隔离级别的事务在每次查询开始时都会生成一个独立的ReadView

脏读的解决

在READ COMMITTED隔离级别下,每次查询都会生成新的ReadView。如果另一个事务修改了数据但未提交,该版本的trx_id会在m_ids列表中,因此不可见。这样就避免了脏读问题。

不可重复读问题

由于每次查询都生成新的ReadView,如果在两次查询之间有其他事务提交了修改,第二次查询会看到新的数据版本,导致不可重复读

7.4 REPEATABLE READ实现

REPEATABLE READ隔离级别的事务只在第一次执行查询语句时生成一个ReadView,之后的查询都复用这个ReadView。

解决不可重复读

由于始终复用第一次生成的ReadView,即使其他事务提交了修改,当前事务看到的仍然是第一次查询时的数据版本,因此实现了可重复读

7.5 MVCC与幻读

MVCC能否完全解决幻读?

在REPEATABLE READ隔离级别下,MVCC可以很大程度避免幻读,但不是完全禁止。存在一种特殊情况:

特殊场景

  1. 事务T1执行SELECT * FROM teacher WHERE number = 30,查询不到记录
  2. 事务T2插入number = 30的记录并提交
  3. 事务T1执行UPDATE teacher SET domain='xxx' WHERE number=30,可以更新成功(因为这条记录实际存在)
  4. 事务T1再次执行SELECT * FROM teacher WHERE number = 30可以查询到记录了

原因分析

T1第一次执行SELECT时生成了ReadView,版本链中没有number=30的记录。之后T2插入记录并提交,T1执行UPDATE时,修改了这条记录,导致这条记录的trx_id变成了T1的事务id。之后T1再次SELECT时,根据可见性规则,trx_id等于creator_trx_id,因此可见

解决方案

  • 使用SERIALIZABLE隔离级别
  • 使用SELECT … FOR UPDATE(锁定读)代替普通SELECT

7.6 MVCC小结

特性READ COMMITTEDREPEATABLE READ
ReadView生成时机每次查询前生成新的第一次查询前生成,之后复用
脏读解决解决
不可重复读未解决解决
幻读未解决很大程度解决(特殊场景除外)

核心结论

  • MVCC只在执行普通SELECT(快照读)时生效
  • READ COMMITTED和REPEATABLE READ的根本区别在于生成ReadView的时机不同
  • REPEATABLE READ可以很大程度避免幻读,但在特定场景下仍可能出现

总结

本文从事务的基本概念出发,深入讲解了MySQL事务机制的方方面面:

核心知识点

  1. ACID特性:原子性、一致性、隔离性、持久性是事务的四大基石
  2. 并发问题:脏读、不可重复读、幻读是事务隔离级别需要解决的三大问题
  3. 隔离级别:从READ UNCOMMITTED到SERIALIZABLE,隔离级别越高,安全性越好但性能越低
  4. MVCC机制:通过版本链和ReadView实现不加锁的并发控制,是MySQL高性能的关键

实际应用建议

  • 读多写少的场景:使用REPEATABLE READ(MySQL默认),兼顾一致性和性能
  • 写多读少的场景:考虑使用READ COMMITTED,减少锁竞争
  • 强一致性要求:使用SERIALIZABLE,但会牺牲性能
  • 幻读问题:在REPEATABLE READ下,如果业务对幻读敏感,使用SELECT ... FOR UPDATE

面试高频问题

  • ACID分别代表什么?MySQL如何保证?
  • 脏读、不可重复读、幻读有什么区别?
  • MVCC的实现原理是什么?版本链和ReadView如何工作?
  • READ COMMITTED和REPEATABLE READ的区别是什么?
  • MySQL如何解决幻读问题?MVCC能否完全解决?

希望这篇文章能帮助你在面试中对答如流!如果觉得有帮助,欢迎点赞、收藏、关注


推荐标签

  • MySQL
  • 事务
  • ACID
  • MVCC
  • 隔离级别
  • 幻读
  • 数据库
  • 面试
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

加倍巴巴

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值