尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

MySQL 中的 MVCC 是什么?如果没有 MVCC,会有什么影响?

MySQL 中的 MVCC 是什么?如果没有 MVCC,会有什么影响? 面试官想通过这道题考察什么概念理解能否准确说出 MVCC 的全称、作用对象和“多版本”到底多在哪里。底层机制能否讲清 Undo Log 版本链、Read View 和隐藏字段三者的协作关系。隔离级别关联能否区分 MVCC 在「读已提交」和「可重复读」下的不同行为。并发影响能否回答“没有 MVCC 会怎样”体现对锁机制与并发性能的理解。当前读与快照读能否说清 MVCC 不能覆盖所有读场景以及它和行锁如何配合。一、标准回答MVCCMulti-Version Concurrency Control多版本并发控制是 InnoDB 存储引擎用来实现事务隔离、支持高并发读写的一种机制。它的核心思想是不为“读”操作加锁而是让读操作访问数据的“历史快照版本”从而实现读不阻塞写、写也不阻塞读。如果让我用一句话总结MVCC 通过记录并保留一行数据的多个历史版本让每个事务都能在自己的“某时刻快照”上完成一致性读取而不是直接去争抢同一份最新数据。1. MVCC 解决了什么问题解决读-写冲突在没有 MVCC 之前读和写很多时候只能通过加锁互斥。一个事务在读某行时另一个事务想更新同一行就得等待反过来写也会阻塞读整体并发上不去。实现非阻塞一致性读MVCC 让普通 SELECT 直接读快照版本不需要加 S 锁也不会被正在执行的事务阻塞。支撑事务隔离级别在「读已提交Read CommittedRC」和「可重复读Repeatable ReadRR」下通过 Read View 的生成时机差异分别解决脏读、不可重复读等问题。2. MVCC 的特点读不加锁快照读基于历史版本不阻塞写操作。写不阻塞读更新操作只修改最新版本并生成新版本旧版本仍可被其他事务读取。依赖 Undo Log历史版本保存在 Undo Log 中通过回滚指针串联成版本链。有代价历史版本会占用存储空间需要后台 purge 线程定期清理不再被引用的旧版本。3. 如果没有 MVCC会有什么影响并发性能大幅下降读写只能靠锁互斥大量读操作会阻塞写写也会阻塞读在线事务吞吐显著降低。一致性和并发难以兼得要么读加锁保证一致性但牺牲性能要么不加锁但可能读到未提交或半更新的数据。隔离级别实现受限RC、RR 的语义更难低成本实现快照式报表、历史查询等场景会很难做。可以这样回答面试官没有 MVCC 时读要么阻塞写、要么需要读加锁业务系统的高并发读写会成为瓶颈而 MVCC 通过“读历史版本、写新版本”的方式让读写解耦在一致性可接受的前提下大幅提升并发能力。二、核心原理MVCC 的实现主要依赖三块隐藏字段、Undo Log 版本链、Read View 可见性判断。下面一点点拆开讲。1. 聚簇索引中的隐藏字段InnoDB 在每行聚簇索引记录中除业务字段外还隐藏维护了几个系统字段常见的三个是隐藏字段大小作用DB_TRX_ID6 字节最近一次修改本行记录的事务 IDDB_ROLL_PTR7 字节回滚指针指向 Undo Log 中该行上一个版本DB_ROW_ID6 字节隐藏自增行 ID当表没有主键时用于生成聚簇索引每次对一行数据执行 INSERT、UPDATE、DELETEInnoDB 都会在 Undo Log 中写入一个对应的旧版本并用DB_ROLL_PTR把新版本和旧版本串起来形成一条从最新版指向历史版的单向链表这就是版本链。2. Undo Log 与版本链以一条account表数据为例假设一行余额字段经历了“初始 100、改成 200、再改成 300”三个版本最新版本balance300DB_TRX_ID指向最后一次修改事务。通过DB_ROLL_PTR可回溯到 balance200 的旧版本。再往前回溯到 balance100 的最早版本。版本链不但用于事务回滚也用于快照读当前事务能不能看到某个版本取决于这个版本对当前事务是否“可见”。3. Read View 的可见性判断执行一次普通 SELECT快照读时InnoDB 会生成一个 Read View核心字段如下字段含义creator_trx_id创建 Read View 的事务 IDm_ids创建 Read View 时系统中活跃未提交事务的 ID 列表min_trx_idm_ids 中的最小值max_trx_id系统下一个将要分配的事务 ID注意不是当前已出现的最大事务 ID遍历版本链时对某个版本的DB_TRX_ID记为 trx_id按如下规则判断若trx_id creator_trx_id本事务自己改出来的版本可见。若trx_id min_trx_id该版本由已提交事务生成可见。若trx_id max_trx_id该版本由“创建 Read View 之后才开始”的事务生成不可见。若min_trx_id trx_id max_trx_id再判断 trx_id 是否在m_ids活跃列表中在则不可见不在则说明已提交可见。如果当前版本不可见就顺着DB_ROLL_PTR往前找旧版本直到找到第一个可见版本如果都不可见该行对当前事务就相当于不存在。4. RC 与 RR 的差异隔离级别Read View 生成时机效果读已提交RC每次快照读都生成新的 Read View能读到其他事务已提交的最新版本存在不可重复读可重复读RR事务内第一次快照读生成 Read View之后复用同一事务多次快照读结果一致避免不可重复读这也是为什么同样是 MVCCRR 级别能保证重复读取时数据不变因为 Read View 固定下来了后续读都按同一个可见性标准筛选版本。5. 当前读与快照读快照读普通SELECT走 MVCC读历史一致性版本。当前读SELECT ... FOR UPDATE、SELECT ... LOCK IN SHARE MODE、UPDATE、DELETE、INSERT读取最新已提交版本并加锁防止其他事务并发修改。所以 MVCC 并非“万能无锁”写入操作以及显式加锁读仍然依赖行锁MVCC 主要优化的是普通读场景。三、应用场景1. 日常开发场景订单列表分页查询用户反复刷新页面时列表基于快照读避免被后台正在写入的事务阻塞页面响应更稳定。统计报表查询在 RR 级别下大批量统计语句开始后使用同一个 Read View能拿到一个时间点的一致性数据不会因为中途有新提交而出现前后对不上的情况。详情页展示浏览商品详情、文章内容等“读多写少”的场景MVCC 让读操作不受写入影响。2. 企业真实场景交易系统对账夜间批处理任务在 RR 下跑对账事务开始时生成 Read View整批数据保持在同一快照上读取保证账目前后一致。电商大促库存扣减使用当前读加锁保证不超卖而商品详情、推荐位等读接口使用快照读提高并发读写分离到不同 SQL 形态。数据导出与备份一致性快照导出脚本让导出的数据是某个统一时刻的状态而不是导出过程中不断变化的数据。审计与日志查询历史审计报表需要可重复读效果避免同一事务内两次查询结果不一致。一句话概括读业务尽量用快照读写业务和强一致校验要认清当前读的锁语义不能把 MVCC 当作分布式锁或强一致性的替代品。四、使用方式下面通过一个 Java MySQL 示例演示 RR 级别下 MVCC 的“快照读复现旧值、当前读读取新值”的典型现象。import java.sql.Connection; import java.sql.DriverManager; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; public class MvccDemo { private static final String URL jdbc:mysql://localhost:3306/demo?useSSLfalseserverTimezoneAsia/Shanghai; public static void main(String[] args) throws Exception { Connection connA DriverManager.getConnection(URL, root, 123456); connA.setAutoCommit(false); connA.setTransactionIsolation(Connection.TRANSACTION_REPEATABLE_READ); // 1. 事务A第一次快照读生成 Read View long first queryBalance(connA); System.out.println(事务A第1次读取余额 first); // 2. 事务B修改余额为 200 并提交 Connection connB DriverManager.getConnection(URL, root, 123456); connB.setAutoCommit(false); updateBalance(connB, 200L); connB.commit(); connB.close(); System.out.println(事务B已提交余额改为 200); // 3. 事务A第二次快照读RR 复用 Read View读到旧值 long second queryBalance(connA); System.out.println(事务A第2次读取余额 second); // 4. 事务A当前读加锁读取最新已提交版本 long current queryBalanceForUpdate(connA); System.out.println(事务A当前读余额 current); connA.commit(); connA.close(); } private static long queryBalance(Connection conn) throws SQLException { String sql SELECT balance FROM account WHERE id 1; try (PreparedStatement ps conn.prepareStatement(sql); ResultSet rs ps.executeQuery()) { rs.next(); return rs.getLong(balance); } } private static long queryBalanceForUpdate(Connection conn) throws SQLException { String sql SELECT balance FROM account WHERE id 1 FOR UPDATE; try (PreparedStatement ps conn.prepareStatement(sql); ResultSet rs ps.executeQuery()) { rs.next(); return rs.getLong(balance); } } private static void updateBalance(Connection conn, long newBalance) throws SQLException { String sql UPDATE account SET balance ? WHERE id 1; try (PreparedStatement ps conn.prepareStatement(sql)) { ps.setLong(1, newBalance); ps.executeUpdate(); } } }执行流程说明事务 A 以 RR 级别开启但此时还没有生成 Read View。第 1 次SELECT触发快照读生成 Read View并固定下来。事务 B 更新余额并提交产生新版本旧版本仍保留在 Undo Log 版本链中。事务 A 第 2 次SELECT仍是快照读由于复用同一个 Read View新版本的DB_TRX_ID对 Read View 不可见于是根据版本链向前找到旧版本读到 100。事务 A 执行SELECT ... FOR UPDATE这是当前读直接读最新已提交版本因此读到 200并对该行加锁。注意事项隔离级别要匹配示例中两个事务都应有明确隔离级别。生产环境连接池的默认隔离级别需要确认避免“以为 RR 其实 RC”的坑。长事务会导致版本链变长事务长时间不提交Read View 一直持有过期版本无法被 purge 清理Undo Log 会膨胀可能导致大事务回滚变慢或磁盘占用升高。当前读会加锁FOR UPDATE会阻塞其他写操作别把普通读都改成加锁读否则并发收益就没了。MVCC 不能替代业务分布式锁超卖、抢券等强一致场景必须结合唯一索引、乐观锁、悲观锁或 Redis 等方案综合处理。五、扩展延伸1. MVCC 与行锁的关系两者不是替代关系而是配合关系。MVCC 主要服务于快照读让读不用锁行锁则服务于当前读和写保证并发修改时数据不被写坏。实际执行链路通常是这样事务执行UPDATE是当前读先按索引找到最新版本并加行锁。修改后写入新版本同时在 Undo Log 记录旧版本维护版本链。其他事务的快照读继续通过 Read View 读取合适的旧版本不受写入影响。2. MySQL 与 Oracle 的 MVCC 对比维度MySQL InnoDBOracle版本存储位置聚簇索引隐藏字段 Undo Log回滚段Undo Segments普通读语义快照读基于 Read View一致性读基于 SCNSystem Change Number是否阻塞快照读不加锁、不阻塞一致性读不加锁写不阻塞读版本清理后台 purge 线程SMON 等后台进程Oracle 的一致性读思想与 MySQL InnoDB 的 MVCC 很接近都是保留旧版本来支持非阻塞读但在具体存储结构和时间点判断上有所不同。3. 优缺点总结优点读多写少场景吞吐高普通读无锁响应稳定RC、RR 隔离语义实现得更自然。缺点需要额外存储 Undo Log长事务下版本链会变长对开发者要求更高需要理解快照读与当前读否则容易误判数据一致性。4. 实际开发注意事项缩短事务时间RR 下 Read View 一旦生成整个事务都持有事务越短版本清理越及时。避免在事务中混用快照读和当前读导致口径不一致同一事务内如果既有普通 SELECT 又有 FOR UPDATE返回结果可能不同需要在业务逻辑中明确取舍。写后读用当前读事务内自己 UPDATE 后再用普通 SELECT 读到的是旧快照这是很多人踩过的坑要读自己刚改的数据应用SELECT ... FOR UPDATE或同一事务内基于当前读语义。关联查询的一致性多表关联的快照读在 RR 下统一按一个 Read View 判断理论上保持一致性需要保证所有表都在同一事务内访问。六、面试追问追问 1MVCC 能彻底解决幻读吗回答思路先区分“快照读的幻读”和“当前读的幻读”再说明 InnoDB 在 RR 下如何补上 Gap Lock。参考答案MVCC 只能让快照读在 RR 下复用 Read View两次普通 SELECT 结果一致避免快照读幻读但UPDATE、FOR UPDATE这类当前读会读最新版本仅靠 MVCC 无法避免幻读。InnoDB 在 RR 级别为当前读引入间隙锁Gap Lock和临键锁Next-Key Lock锁住索引区间阻止其他事务插入符合条件的新行从而真正解决幻读。追问 2Read View 判断版本可见性的完整规则是什么回答思路按字段含义说清楚边界避免漏掉“右闭区间”和“不在活跃列表”两个容易答错的点。参考答案对一个版本的事务 ID trx_id 做判断如果等于creator_trx_id可见如果小于min_trx_id说明生成该版本的事务已提交可见如果大于等于max_trx_id说明该事务在 Read View 之后才开始不可见如果落在中间区间再看是否在m_ids活跃列表中。在列表中表示尚未提交不可见不在列表中表示已提交可见。追问 3RR 为什么能避免不可重复读回答思路联系 Read View 生命周期强调“只生成一次、复用”这个机制。参考答案因为 RR 级别下事务在第一次快照读时生成 Read View并在整个事务内复用同一个 Read View。后续其他事务提交的新版本其DB_TRX_ID相对这个固定 Read View 不可见所以每次快照读都拿到相同结果。而 RC 级别每次快照读都生成新 Read View因此会读到别的事务最新提交的版本产生不可重复读。追问 4如果一个事务 UPDATE 后还没提交另一个事务读这行会怎样回答思路区分“普通读”和“加锁读”。参考答案普通 SELECT 是快照读不会等锁而是通过 Read View 判断版本可见性。因为修改该行的事务还活跃其新版本对当前事务不可见于是读取旧版本。如果是SELECT ... FOR UPDATE当前读要读最新已提交版本并加锁而该行已被未提交事务锁住就会阻塞等待直到对方提交或回滚。追问 5长事务对 MVCC 有什么影响回答思路从 Undo Log 膨胀、purge 延迟、回滚成本三个角度作答。参考答案长事务会长期持有 Read View导致大量旧版本因为“仍有事务可能读取”而不能被 purge 清理Undo Log 会持续膨胀同时如果该长事务最终回滚需要沿版本链逆向恢复回滚时间会变长此外过多的旧版本也可能拖慢某些查询扫描效率。生产环境建议缩短事务时间尤其是批量任务要分批提交。
返回列表