当前位置: 首页 > news >正文

面试必问!MySQL 事务到底是怎么实现的?这篇文章讲透了

说实话,这个问题我被问过不止一次。每次有人来问我 MySQL 事务是怎么回事,我都发现大家普遍停留在「ACID 四个特性」这个层面,背得挺溜,但真要问你 MySQL 底层是怎么实现原子性的,怎么保证崩了数据不丢,怎么做到多个事务并发跑还互不干扰——很多人就开始含糊了。

这篇文章我就把这块彻底说清楚。不搞那些花里胡哨的,直接从底层机制讲起,生产上遇到过的坑也会顺带提一嘴。


先说说事务是什么

事务这个概念说白了就是:把一组操作捆绑成一个整体,要么全部成功,要么全部失败,不允许中间状态存在。

举个最经典的例子,转账。A 给 B 转 500 块,数据库层面是两步操作:A 的账户减 500,B 的账户加 500。这两步必须同时成功或者同时失败,不然 A 扣了钱 B 没收到,或者 B 收到了 A 没扣,这都是灾难性的数据错误。

这就是事务要解决的核心问题。

MySQL 里事务主要是 InnoDB 引擎实现的,MyISAM 不支持事务,这个要先知道。

ACID 大家都背过:原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)、持久性(Durability)。但背概念没用,我们要知道 MySQL 是用什么技术手段实现这四个特性的。

  • 原子性→ 靠undo log
  • 持久性→ 靠redo log
  • 隔离性→ 靠锁 + MVCC
  • 一致性→ 是上面三个共同作用的结果

下面一个一个展开说。


Undo Log:原子性的保障

undo log 翻译过来叫回滚日志。它的核心思想很简单:在你修改数据之前,先把原来的数据记下来,万一事务失败了,就拿这个日志把数据恢复回去。

你执行了一条INSERT,undo log 里就记一条DELETE;你执行了UPDATE,undo log 里就记一条把数据改回去的UPDATE;你执行了DELETE,undo log 里就记一条INSERT

这样,当事务需要回滚的时候,MySQL 就把 undo log 里的操作反向执行一遍,数据就回到了事务开始之前的状态。

有一点要注意:undo log 一定是优先于数据修改落盘的,这个顺序不能乱。如果数据先改了,undo log 还没写,这时候崩了,你连回滚的依据都没有了。

实际上 undo log 不只是用来回滚,它还承担着 MVCC 的职责,后面会说到。


Redo Log:持久性的保障

redo log 这块是我觉得 MySQL 设计里最精妙的地方之一。

先说问题背景。MySQL 的数据最终是存在磁盘上的,但读写操作都是在内存里的 Buffer Pool 里进行的,不是每次改完数据都立刻写磁盘。这样做是为了性能,磁盘随机 IO 太慢了。但这就带来了一个风险:数据在内存里改了,还没来得及刷到磁盘,MySQL 突然崩了,数据就丢了。

怎么解决?redo log 就是答案。

redo log 记录的是数据页的物理修改,每次事务提交的时候,不需要立刻把数据页刷到磁盘,但必须先把 redo log 写到磁盘。redo log 是顺序写的,顺序写磁盘的速度比随机写快很多,这个性能差距在机械硬盘时代尤其明显。

这个机制有个专业名字叫WAL(Write-Ahead Logging),意思就是先写日志再写数据。

MySQL 崩溃重启之后,会扫描 redo log,把已经提交但还没来得及刷盘的数据重新应用一遍,数据就恢复了。这就是为什么事务一旦提交,就算服务器崩了,数据也不会丢。

redo log 有个重要的参数:innodb_flush_log_at_trx_commit,这个参数控制 redo log 的刷盘策略:

  • 设置为1:每次事务提交都强制刷盘,最安全,但性能最差
  • 设置为2:提交时写到操作系统的缓存,每秒刷一次盘,折中方案
  • 设置为0:每秒刷一次盘,性能最好,但崩溃可能丢 1 秒数据

生产环境一般金融类业务设置 1,对数据安全性要求没那么高的业务可以设置 2。设置 0 风险比较大,不建议。

-- 查看当前配置SHOWVARIABLESLIKE'innodb_flush_log_at_trx_commit';-- 查看 redo log 相关配置SHOWVARIABLESLIKE'innodb_log%';

MVCC:隔离性的核心机制

这块是整个事务机制里最复杂的,也是面试最爱考的。

MVCC 全称 Multi-Version Concurrency Control,多版本并发控制。它解决的核心问题是:怎么让读操作不加锁,同时还能保证数据的隔离性?

传统的做法是读写都加锁,读的时候其他人不能写,写的时候其他人不能读,这样数据是安全了,但并发性能很差。MVCC 的思路是给数据维护多个版本,不同的事务看到不同版本的数据,读操作基本不需要加锁。

InnoDB 是怎么实现多版本的?

InnoDB 在每行数据上隐式地加了几个字段:

  • DB_TRX_ID:最后一次修改这行数据的事务 ID
  • DB_ROLL_PTR:指向 undo log 的指针,通过这个可以找到这行数据的历史版本
  • DB_ROW_ID:隐式的行 ID

每次事务修改一行数据,不会直接覆盖原来的数据,而是创建一个新版本,旧版本通过DB_ROLL_PTR串起来,形成一个版本链。

举个例子,一行数据初始值age = 20,事务 A(ID=100)把它改成了 25,事务 B(ID=101)又把它改成了 30,这行数据就有了三个版本,通过版本链串联在一起。

Read View 是什么?

光有版本链还不够,还需要一个机制来决定:当前事务应该看哪个版本的数据?这就是 Read View(读视图)的作用。

Read View 里记录了几个关键信息:

  • 当前活跃的事务 ID 列表(m_ids
  • 最小活跃事务 ID(min_trx_id
  • 下一个待分配的事务 ID(max_trx_id
  • 创建这个 Read View 的事务 ID

判断一个数据版本是否对当前事务可见,规则大概是这样:

  1. 如果这个版本的DB_TRX_ID小于min_trx_id,说明这个版本是在 Read View 创建之前就已经提交的,可见
  2. 如果DB_TRX_ID大于等于max_trx_id,说明这个版本是在 Read View 创建之后才开始的事务改的,不可见
  3. 如果在这两者之间,就看DB_TRX_ID是不是在活跃事务列表里,在的话说明这个事务还没提交,不可见;不在的话说明已经提交了,可见

如果当前版本不可见,就顺着版本链往前找,直到找到一个可见的版本。


隔离级别和 MVCC 的关系

MySQL 有四个隔离级别:读未提交、读已提交、可重复读、串行化。

读未提交基本不用 MVCC,直接读最新版本,啥都不管,脏读问题很严重,生产上几乎不用。

读已提交(Read Committed):每次执行 SELECT 都重新创建一个 Read View,所以每次都能读到其他事务最新提交的数据。这个级别会有不可重复读的问题——同一个事务里,两次查询同一行数据,结果可能不一样,因为中间有其他事务提交了修改。

可重复读(Repeatable Read):这是 MySQL 的默认隔离级别。它在事务第一次执行 SELECT 的时候创建 Read View,整个事务期间都复用这个 Read View,所以不管其他事务怎么改,你每次查到的都是一样的数据。这就是"可重复读"的含义。

来看个具体的场景:

-- 事务 A 开始STARTTRANSACTION;SELECTageFROMuserWHEREid=1;-- 读到 age = 20-- 此时事务 B 把 age 改成了 25 并提交-- UPDATE user SET age = 25 WHERE id = 1; COMMIT;-- 事务 A 再次查询SELECTageFROMuserWHEREid=1;-- 在 RR 级别下,仍然读到 age = 20COMMIT;

在可重复读级别下,事务 A 两次查询结果是一样的,事务 B 的修改对 A 不可见,因为 A 的 Read View 是在 B 提交之前创建的。

串行化(Serializable):所有事务串行执行,完全不存在并发问题,但性能最差,基本只在极端场景下用。


幻读问题和间隙锁

说到这里要提一个经典问题:MVCC 能解决幻读吗?

答案是:不能完全解决

幻读是指:同一个事务内,两次范围查询,第二次查到了第一次没有的记录(通常是其他事务插入了新数据)。

MVCC 通过 Read View 可以解决快照读(普通 SELECT)的幻读问题,但如果你用的是当前读(SELECT ... FOR UPDATE或者UPDATEDELETE),那就不走 MVCC 了,是直接读最新数据,这时候幻读就可能出现。

InnoDB 解决这个问题用的是间隙锁(Gap Lock)Next-Key Lock

Next-Key Lock = 行锁 + 间隙锁,它不只锁住符合条件的行,还会锁住这些行之间的"间隙",防止其他事务在这个范围内插入新数据。

-- 这条语句会加 Next-Key LockSELECT*FROMuserWHEREageBETWEEN20AND30FORUPDATE;-- 锁住了 age 在 20-30 范围内的所有行,以及这个范围内的间隙-- 其他事务无法在这个范围内插入新记录

这就是为什么 MySQL 在可重复读级别下能基本解决幻读问题,但要注意,这是在当前读的场景下,靠锁来保证的,不是靠 MVCC。


事务提交和崩溃恢复的完整流程

把前面说的串起来,看看一个事务从开始到提交,底层到底发生了什么:

  1. 事务开始,分配事务 ID
  2. 执行 SQL 操作,修改 Buffer Pool 里的内存数据
  3. 每次修改,先写 undo log(记录原始数据,用于回滚和 MVCC)
  4. 再把修改记录到 redo log buffer
  5. 事务提交时,把 redo log buffer 里的内容刷到磁盘(这步完成,事务就算持久化了)
  6. 释放事务持有的锁
  7. 后台线程异步把 Buffer Pool 里的脏页刷到磁盘数据文件

崩溃恢复时:

  1. 扫描 redo log,找到已提交但没有刷盘的事务,重新应用
  2. 找到未提交的事务,用 undo log 回滚

这个设计保证了数据既不会因为崩溃而丢失已提交的数据,也不会因为崩溃而保留未提交的数据。


生产上几个要注意的坑

长事务是大忌。事务越长,持有锁的时间就越长,其他事务等待的时间就越长,系统并发能力就越差。而且长事务会导致 undo log 不能被清理(因为可能还有其他事务需要读历史版本),undo log 会一直膨胀,磁盘空间会被大量占用。

我之前处理过一个案例,一个业务同学写了个数据迁移脚本,在一个事务里处理了几十万条数据,跑了将近一个小时,这期间 undo log 撑到了几十 GB,把磁盘快打满了,差点影响整个数据库服务。

-- 查看当前活跃事务,找出长事务SELECT*FROMinformation_schema.INNODB_TRXORDERBYtrx_started;-- 看看有没有跑了很久的事务SELECTtrx_id,trx_started,trx_state,trx_queryFROMinformation_schema.INNODB_TRXWHERETIMESTAMPDIFF(SECOND,trx_started,NOW())>60;

不要在事务里做外部调用。比如在事务里调用第三方接口、发消息队列,这些操作耗时不可控,会导致事务时间变长,锁持有时间变长。正确的做法是先提交事务,再做外部调用,或者用异步的方式处理。

隔离级别不是越高越好。串行化虽然安全,但并发性能极差。大多数业务用可重复读就够了,某些读多写少且对一致性要求不那么严格的场景,用读已提交性能更好。

autocommit 要注意。MySQL 默认开启自动提交,每条 SQL 都是一个独立的事务。如果你用BEGIN或者START TRANSACTION开启了事务,记得要COMMIT或者ROLLBACK,不然事务会一直挂着。


总结

MySQL 事务的实现,核心就是三个东西:undo log、redo log、MVCC

undo log 保证了原子性,事务失败可以回滚,同时也是 MVCC 多版本数据的存储基础。redo log 保证了持久性,通过 WAL 机制,事务提交后数据不会因为崩溃而丢失。MVCC 加上锁机制保证了隔离性,让并发事务之间互不干扰,同时尽量减少锁的使用,提升并发性能。

这几个机制不是独立运作的,它们相互配合,共同构成了 InnoDB 事务体系。理解了这些,再去看各种隔离级别的行为、幻读的成因、长事务的危害,就都能说清楚了。

下次面试官再问你 MySQL 事务是怎么实现的,希望你不只是背 ACID,而是能说出 undo log 怎么保证原子性,redo log 为什么能保证持久性,MVCC 的 Read View 是怎么判断版本可见性的。这才是真正理解了事务机制。


如果觉得这篇文章对你有帮助,欢迎点赞转发,让更多人看到。我会持续分享生产环境中的运维实战经验,MySQL 优化、故障排查、架构设计这些内容后续都会出,关注不迷路。

个人博客:躬行笔记

http://www.jsqmd.com/news/738808/

相关文章:

  • 为什么你的YOLOv5在树莓派跑不动?Python轻量化不是“简单剪枝”——资深边缘架构师拆解4层冗余消除机制(含热力图可视化诊断)
  • 如何高效解放双手:绝区零一条龙智能自动化助手实战指南
  • 2026年公共管理论文降AI工具推荐:行政管理政策研究答辩前知网达标方案 - 还在做实验的师兄
  • C语言OTA固件差分升级调试实录(基于bsdiff+ed25519签名验证的端到端调试日志还原)
  • 别再死记硬背Nash均衡了!用Python模拟‘囚徒困境’和‘性别战’,5分钟搞懂博弈论核心
  • 学术研究中事实陈述提取的技术实现与应用
  • 【Python低代码平台插件化开发实战指南】:20年架构师亲授5大核心设计模式与3个工业级落地案例
  • AKShare金融数据接口库:Python量化分析的完整高效解决方案
  • 刷蛋机哪家好:企业选购核心标准标准与策略深度解析
  • 告别Outlook!Foxmail 7.2.25保姆级配置教程,手把手教你同步Gmail和企业微信
  • 解锁Switch游戏新境界:3步掌握大气层整合包安装与优化
  • 智能作业车辆路径规划【附ROS仿真】
  • 如何在普通PC上安装macOS:OpenCore完整配置方案指南
  • 2026年农业科学论文降AI工具推荐:农学园艺畜牧研究亲测99.26%达标指南 - 还在做实验的师兄
  • 从传感器数据到颜色判断:用FPGA处理ZC-CLS381RGB的RGB原始值(含阈值设定技巧)
  • 在Node.js后端服务中集成Taotoken实现稳定的大模型能力调用
  • WaveTools鸣潮工具箱:终极免费工具箱解锁游戏新体验 [特殊字符]
  • 如何安全备份微信聊天记录:5步完成数据保护的完整指南
  • B站缓存视频解锁指南:3分钟无损转换m4s为MP4的完整方案
  • Taotoken 用量看板如何帮助开发者清晰掌控 AI 调用成本
  • 中石化加油卡不用浪费!高折扣回收平台帮你省更多 - 团团收购物卡回收
  • 突破Windows窗口限制:3步掌握WindowResizer强制调整技巧
  • 1个侦探工具:3分钟解决Windows快捷键修复难题
  • 快速变现:中石化加油卡高折扣回收线上平台怎么选? - 团团收购物卡回收
  • 如何用VinXiangQi打造你的智能象棋AI助手:3个步骤快速上手
  • 为 OpenClaw Agent 框架配置 Taotoken 作为统一的模型提供商
  • LVGL Table实战:手把手教你打造一个带合并单元格和自定义样式的嵌入式UI数据表格
  • 如何让订单系统和营销系统解耦
  • 京东e卡怎么提现到微信?实用变现攻略大公开 - 京顺回收
  • Photon-GAMS光影引擎完全指南:如何打造电影级Minecraft视觉体验