个人主页Cx330❄️个人专栏《C语言》《LeetCode刷题集》《数据结构-初阶》《C知识分享》《优选算法指南-必刷经典100题》《Linux操作系统》:从入门到入魔《Git深度解析》:版本管理实战全解 《Qt 极境架构》MySQL 核心技术与实战心向往之行必能Cx330的简介目录前言一、 从售票系统说起CURD 不加控制会有什么问题并发冲突场景如何解决上述问题二、 什么是事务Transaction1. 事务的定义2. 事务的核心属性ACID3. 为什么会出现事务4. 事务的版本支持三、 事务提交方式与基本操作1. 事务的提交方式2. 事务常用操作实战实验一正常演示开始、保存点与回滚实验二未 Commit 客户端崩溃异常自动回滚实验三Commit 之后客户端崩溃持久化成功实验四begin 会自动忽略 autocommit 设置实验五单条 SQL 与事务的关系四、 深入探究事务隔离级别与并发问题1. 为什么要有隔离性2. 四大隔离级别与 anomalies并发缺陷3. 查看与设置隔离级别4. 并发事务带来的 3 个核心问题五、四 种隔离级别实操演示5.1 读未提交read uncommitted5.2 读已提交read committed5.3 可重复读repeatable read5.4 串行化serializable六. 一致性事务的最终归宿七、 硬核底层MVCC多版本并发控制原理1. 理解 MVCC 的三个核心基石核心基石一3 个记录隐藏字段核心基石二Undo Log 与历史版本链【模拟演练】版本链的形成过程核心基石三Read View 的可见性算法可见性判断算法图解2. 探究RC 与 RR 的本质区别是什么实例对比测试用例测试用例 1 (在 RR 隔离级别下)测试用例 2 (在 RR 隔离级别下)结语前言在多线程高并发的后端开发中数据安全与并发控制永远是重中之重。无论是面试中必问的“高频考点”还是实际业务如电商秒杀、票务系统中的核心逻辑MySQL 事务Transaction及其隔离机制都是每一位优秀程序员必须掌握的看家本领。今天博主将带大家完全拆解 MySQL 事务的底层世界从最直观的售票系统并发缺陷切入横跨 ACID 属性、事务实战操作、四大隔离级别最终直击最硬核的MVCC多版本并发控制与 Read View 机制。文章干货极多建议收藏后反复研读一、 从售票系统说起CURD 不加控制会有什么问题想象一个经典的火车票售票系统数据库中有一张tickets表idnamenums10西安 - 兰州1此时有客户端 A 和客户端 B 同时尝试买票两者的执行逻辑如下if (nums 0) { // 卖票逻辑... update tickets set nums nums - 1 where id 10; }并发冲突场景客户端 A检查数据库发现nums 1 0决定卖票步骤 1。在 A 还没有来得及更新数据库时客户端 B也进来检查发现nums依然是 1也决定卖票步骤 2、3。A 执行更新将nums改为 0步骤 4。B 也执行更新将nums改为 0步骤 5。结果同一张票被卖了两次超卖问题如何解决上述问题要彻底解决这种并发混乱购票操作必须满足以下属性原子性买票的过程必须是一个整体不能停在中间。隔离性客户端之间的买票操作互相不应该有干扰。持久性买票成功后数据的修改必须是永久有效的。一致性买票前和买票后数据库的状态都必须是确定的、符合业务规则的。这正是事务Transaction诞生的根本原因。二、 什么是事务Transaction1. 事务的定义事务是指由一组 DML数据操作语言语句组成的逻辑处理单元。这些语句在逻辑上存在强相关性要么全部成功要么全部失败是一个不可分割的整体。生活场景比如你毕业了学校的教务系统要删除你在学校的所有信息基本信息、成绩、论坛文章等。这就需要多条DELETE语句这些语句必须构成一个事务。如果基本信息删了成绩却因为网络断开没删掉就会产生脏数据。2. 事务的核心属性ACID原子性Atomicity事务中的所有操作要么全部完成要么全部不完成。事务在执行过程中出错会被回滚Rollback到事务开始前的状态如同从未执行过一样。一致性Consistency在事务开始之前和结束以后数据库的完整性没有被破坏。这属于业务层的最终目的通过 AID 三个技术特性共同保证。隔离性Isolation数据库允许多个并发事务同时对数据进行读写。隔离性可以防止并发交叉执行时导致的数据不一致。隔离性分为不同级别RU、RC、RR、Serializable。持久性Durability事务一旦提交Commit其对数据的修改就是永久性的即使发生系统崩溃、断电数据也不会丢失。3. 为什么会出现事务事务本质上是 MySQL 开发者为了简化应用层编程模型而设计的。它让业务层不需要考虑复杂的网络异常、服务器宕机、并发冲突等底层细节。在写业务代码时我们只需要简单地决定是“提交Commit”还是“回滚Rollback”即可。4. 事务的版本支持在 MySQL 中只有使用了 InnoDB 存储引擎的表才支持事务传统的 MyISAM 引擎是不支持事务的。我们可以通过以下命令查看show engines;输出中可以看到InnoDB-Transactions: YES(支持事务、行级锁、外键)MyISAM-Transactions: NO(不支持事务)三、 事务提交方式与基本操作1. 事务的提交方式MySQL 事务提交方式分为两种自动提交Autocommit手动提交查看自动提交配置show variables like autocommit;Value ON表示默认自动提交即执行单条 DML 语句后MySQL 会自动将其提交。修改方式SET AUTOCOMMIT0; -- 禁止自动提交 SET AUTOCOMMIT1; -- 开启自动提交2. 事务常用操作实战为了便于直观观察我们先在会话中把隔离级别设置为读未提交set global transaction isolation level READ UNCOMMITTED; -- 注意需要重启客户端生效创建测试表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;实验一正常演示开始、保存点与回滚mysql start transaction; -- 开启事务或使用 begin Query OK, 0 rows affected (0.00 sec) mysql savepoint savel; -- 创建保存点 savel Query OK, 0 rows affected (0.00 sec) mysql insert into account values (1, 张三, 100); Query OK, 1 row affected (0.05 sec) mysql savepoint save2; -- 创建保存点 save2 Query OK, 0 rows affected (0.01 sec) mysql insert into account values (2, 李四, 10000); Query OK, 1 row affected (0.00 sec) mysql select * from account; ---------------------- | id | name | blance | ---------------------- | 1 | 张三 | 100.00 | | 2 | 李四 | 10000.00 | ---------------------- mysql rollback to save2; -- 回滚到保存点 save2 Query OK, 0 rows affected (0.03 sec) mysql select * from account; -- 李四这条记录回滚消失了 ---------------------- | id | name | blance | ---------------------- | 1 | 张三 | 100.00 | ---------------------- mysql rollback; -- 直接 rollback 回滚到最开始位置 Query OK, 0 rows affected (0.00 sec) mysql select * from account; Empty set (0.00 sec) -- 数据全部清空回滚成功实验二未 Commit 客户端崩溃异常自动回滚终端 A开启事务begin插入“张三”但不提交未执行commit。终端 B因为隔离级别是 RU此时能查到“张三”这条记录。终端 A突然遭遇异常比如使用ctrl \或kill强行终止客户端。终端 B再次查询发现“张三”已经不见了。结论如果事务未 Commit一旦客户端崩溃断开MySQL 会自动对该事务执行回滚操作保证原子性。实验三Commit 之后客户端崩溃持久化成功终端 A开启事务begin插入“张三”并执行commit。终端 A异常强杀客户端。终端 B再次查询“张三”数据依然存在。结论一旦事务被commit提交数据就会被持久化到磁盘上无法再通过rollback撤销。实验四begin 会自动忽略 autocommit 设置实验五单条 SQL 与事务的关系很多人误以为不显式写begin就没有事务。其实如果设置了autocommit 1自动提交模式开启执行单条 SQL 时InnoDB 会自动将这条 SQL 包装成一个事务并立即提交。如果客户端在执行过程中崩溃只要执行成功数据就会持久化。如果设置了autocommit 0关闭自动提交即便是一条简单的INSERT在终端异常挂掉后由于没有执行commitMySQL 也会直接在后台将其回滚。终极结论只要输入了begin或start transaction事务就必须通过commit提交才会持久化不受全局autocommit变量的影响。MySQL 的单条 DML 语句在 InnoDB 下默认都是当成独立事务自动提交的。四、 深入探究事务隔离级别与并发问题1. 为什么要有隔离性MySQL 作为一个高性能的网络服务随时可能有成百上千个客户端发起并发事务。如果大家同时修改同一张表、同一行数据在不加保护的前提下必然会乱套。为了在高并发性能与数据安全性之间取得平衡MySQL 引入了隔离性并提供了四种不同的隔离级别。2. 四大隔离级别与 anomalies并发缺陷读未提交Read Uncommitted, RU特点没有任何真正的隔离保护效率最高但基本不加锁。致命缺陷存在脏读Dirty Read。即一个事务在运行中能读取到另一个事务更新了、但尚未提交Uncommitted的数据。如果另一个事务最后回滚了那读取到的就是虚假数据。读提交Read Committed, RC特点一个事务只能看到其他事务已经提交Committed的修改。这也是绝大多数数据库如 Oracle默认的隔离级别。缺陷存在不可重复读Non-repeatable Read。在同一个事务内多次执行相同的SELECT查询可能会因为其他事务在此期间提交了修改导致前后读取到的数据内容不一致。可重复读Repeatable Read, RR特点MySQL 的默认隔离级别。它确保在同一个事务内多次读取同一行数据看到的结果始终是一致的。理论缺陷存在幻读Phantom Read问题。即当事务 A 进行范围查询时事务 B 在该范围内插入Insert了新记录并提交事务 A 再次读取该范围时会莫名其妙多出一些记录就像产生了幻觉。注意MySQL 的 InnoDB 引擎在 RR 级别下通过Next-Key LocksGAP 间隙锁 行锁已经很大程度上解决了幻读问题串行化Serializable特点最高隔离级别。所有并发事务会被强制排队串行化执行。机制在读的数据行上全部加共享锁读取也会被阻塞。代价并发性能极低实际生产中几乎从不使用。3. 查看与设置隔离级别查看当前隔离级别SELECT global.tx_isolation; -- 查看全局级别 SELECT session.tx_isolation; -- 查看当前会话级别 SELECT tx_isolation; -- 默认同上设置隔离级别语法SET [SESSION|GLOBAL] TRANSACTION ISOLATION LEVEL {READ UNCOMMITTED | READ COMMITTED | REPEATABLE READ | S4. 并发事务带来的 3 个核心问题在讲解隔离级别之前我们先搞懂并发事务会带来的 3 个经典问题这也是面试必考点问题名称定义说明脏读一个事务读到了另一个事务未提交的修改数据。如果另一个事务回滚读到的数据就是无效的脏数据。不可重复读同一个事务内多次执行相同的 select 语句读到的结果不一样。核心原因是其他事务对数据做了修改 / 删除并提交。幻读同一个事务内多次执行相同的 select 语句第二次读到了第一次没有的新增记录就像出现了幻觉。核心原因是其他事务做了新增并提交。五、四 种隔离级别实操演示所有演示均基于account表初始数据truncate table account; insert into account values (1, 张三, 100.00), (2, 李四, 10000.00);5.1 读未提交read uncommitted这是最低的隔离级别几乎没有隔离性会出现脏读生产环境严禁使用核心问题脏读读到了其他事务未提交的数据一旦其他事务回滚数据就失效了。5.2 读已提交read committed这是大多数数据库Oracle 、SQL Server的默认隔离级别解决了脏读但会出现不可重复读。核心问题不可重复读同一个事务内相同的查询语句两次读到的结果不一样。5.3 可重复读repeatable readMySQL 默认隔离级别解决了脏读和不可重复读同时通过 Next-Key 锁间隙锁 行锁解决了幻读问题。5.4串行化serializable最高的隔离级别强制所有事务串行执行完全解决了脏读、不可重复读、幻读但并发性能极差生产环境几乎不使用。核心特点所有读写操作都会加锁事务只能串行执行安全性最高但并发性能最低。六. 一致性事务的最终归宿我们前面讲了原子性、隔离性、持久性最终都是为了保证一致性。一致性分为两个层面数据库层面的一致性事务执行前后数据库的完整性约束主键、外键、唯一索引、非空约束等不会被破坏比如主键不会重复、库存不会出现负数。业务层面的一致性这是由我们的业务代码决定的比如转账前后两个账户的总金额不变、订单创建后库存必须扣减、用户下单后优惠券必须标记为已使用。MySQL 的事务机制为我们提供了保证一致性的技术基础原子性、隔离性、持久性但最终的业务一致性还是需要我们的业务代码来保证。一句话总结通过 AID原子性、隔离性、持久性来保证 C一致性。七、 硬核底层MVCC多版本并发控制原理在并发访问中最常见的场景有三种读-读、读-写、写-写。读-读不需要任何并发控制。写-写通过加锁解决防止更新丢失。读-写如果读写互相阻塞性能会极差类似 Serializable。为了实现读写不冲突、无锁并发InnoDB 引入了MVCCMulti-Version Concurrency Control多版本并发控制。1. 理解 MVCC 的三个核心基石要搞懂 MVCC 的秘密必须先掌握以下三个前提3个隐式字段Undo Log回滚日志Read View一致性读视图核心基石一3 个记录隐藏字段InnoDB 在存储表记录时除了我们定义的显式列之外每一行数据都会自动添加 3 个隐藏列DB_TRX_ID6 字节最近修改或插入当前记录的事务 ID。DB_ROLL_PTR7 字节回滚指针指向这条记录在undo log中的上一个历史版本地址。DB_ROW_ID6 字节隐式自增主键。如果表没有定义主键InnoDB 会自动使用该字段构建聚簇索引。(另外还有一个删除标记 flag 列表示记录是否被逻辑删除。)假设我们在student表插入一条初始记录(张三, 28)它的结构大概如下nameageDB_TRX_IDDB_ROW_IDDB_ROLL_PTR张三28null (假设初始为0)1null核心基石二Undo Log 与历史版本链Undo Log是 MySQL 存放在内存缓冲区并在适当时机刷盘的一段日志。 每当一个事务修改数据时InnoDB 会采用写时拷贝Copy-on-Write的机制修改前先给该行加锁。将当前记录复制一份到undo log中。修改当前记录的值并将DB_TRX_ID改为当前事务的 ID。将当前记录的DB_ROLL_PTR指向刚刚复制到undo log中的旧版本地址。释放锁。【模拟演练】版本链的形成过程第一步事务 10将name从“张三”改为“李四” 此时最新物理记录变为“李四”而它的回滚指针指向了undo log中的“张三”。第二步事务 11将age从 28 改为 38 物理记录变为(李四, 38)回滚指针指向undo log中的(李四, 28)而(李四, 28)再次指向(张三, 28)。这样一个以物理最新记录为头节点、通过DB_ROLL_PTR指针不断向前溯源的历史版本链链表就形成了[最新物理数据: 李四, 38] (DB_TRX_ID 11) | v (DB_ROLL_PTR) [Undo Log 历史版本 1: 李四, 28] (DB_TRX_ID 10) | v (DB_ROLL_PTR) [Undo Log 历史版本 2: 张三, 28] (DB_TRX_ID 0/null)读的类型区分当前读读取最新版本的记录需要加锁。如insert、update、delete以及select... lock in share mode、select ... for update。快照读读取历史版本不加锁避免与写操作冲突。这就是 MVCC 的精髓。核心基石三Read View 的可见性算法那么问题来了面对这一条长长的历史版本链当某个事务执行快照读SELECT时它到底应该读取链表中的哪一个版本 答案是由Read View决定。当事务执行快照读时MySQL 会自动生成一个Read View读视图。在源码中它是一个用来进行可见性判断的类主要包含以下核心属性m_ids在生成 Read View 的那一刻系统内当前活跃且未提交的事务 ID 列表。up_limit_idm_ids列表中事务 ID最小的那个值低水位比这个 ID 小的事务说明在创建 Read View 之前已经全部提交肯定可见。low_limit_id目前已出现过的最大事务 ID 1即尚未分配的下一个事务 ID高水位比这个 ID 大的事务说明是在创建 Read View 之后才开启的绝对不可见。creator_trx_id创建该 Read View 的当前事务 ID自己修改的数据永远可见。可见性判断算法图解当事务尝试读取版本链上的某条记录其事务 ID 为 DB_TRX_ID时判断逻辑如下low_limit_id up_limit_id | | v ------------------------------------------------ 时间轴 (事务ID递增) 历史已提交事务 | 活跃未提交事务 (m_ids) | 快照后新开启的事务 (可见) | (不可见/判断可见) | (不可见)对应源码策略2. 探究RC 与 RR 的本质区别是什么很多人会问既然 RC读已提交和 RR可重复读都使用了 MVCC那为什么它们读取的结果会不同核心本质只有一个Read View 的生成时机不同RC读提交级别 在事务中每次执行SELECT快照读时都会重新生成最新的 Read View。 因为每次重新生成那些在两次SELECT之间刚刚提交的事务其 ID 就会从新Read View的m_ids中被移除因此后面那次读取就能看到新的提交。这就是不可重复读的来源。RR可重复读级别 在一个事务中只有在第一次执行SELECT快照读时才会创建 Read View。 此后在整个事务的生命周期内不管你执行多少次SELECT读取使用的都是同一个 Read View因此不管别的并发事务怎么提交它读取到的数据永远跟第一次读取时一模一样。实例对比测试用例测试用例 1 (在 RR 隔离级别下)事务 A与事务 B同时begin。事务 B首次执行select * from user;此时生成 Read View。事务 A执行更新update user set age 18 where id 1;并commit。事务 B再次执行快照读select * from user;。结果依然看到老数据看不到age 18。事务 B执行当前读select * from user lock in share mode;。结果因为当前读强制读取最新物理版本并加锁所以看到了age 18。测试用例 2 (在 RR 隔离级别下)事务 A与事务 B同时begin。事务 B在事务 A 更新前没有进行任何快照读。事务 A执行更新update user set age 28 where id 1;并commit。事务 B此时才执行首次快照读select * from user;此时才生成 Read View。结果读到了最新的age 28分析因为事务 B 首次快照读是在 A 提交之后所以新生成的 Read View 判定 A 已经提交因此可见。这充分证明RR 级别下的快照读其数据版本极度依赖于该事务中首次执行快照读的时机结语MySQL 的事务机制是整个关系型数据库高并发设计的精髓所在通过Undo LogMySQL 实现了低成本的“版本保留”与“事务回滚”保证原子性与持久性。通过Read View配合可见性算法MySQL 实现了读写不冲突极大地释放了并发潜能。RC 与 RR 的本质不同仅仅是 Read View 产生频次的不同。深刻理解这些底层机制不仅能帮助你在面试中对答如流、尽显深度更能在实际高并发系统设计中让你对数据的行为有着绝对的掌控力如果你觉得这篇文章写得足够硬核、对你有帮助欢迎点赞、收藏并关注博主我们下期再见