十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

MySQL事务从原理到实战:ACID、MVCC、锁与Spring事务全解析

MySQL事务从原理到实战:ACID、MVCC、锁与Spring事务全解析 1. 面试官问“事务”时到底在问什么先还原一个真实的面试场景。我刚坐下对面面试官喝了口茶问出了那句经典开场白“聊聊你对MySQL事务的理解。”很多人的第一反应是背ACID原子性、一致性、隔离性、持久性。这四个词背起来容易但面试官真正想听的是你有没有在真实项目里被事务坑过。说白了MySQL事务这题表面考概念实际考的是并发控制、日志机制、异常场景这三条线的交叉理解。我见过太多候选人卡在同一道衍生题上“RR隔离级别下到底有没有幻读”有人斩钉截铁说没有有人支支吾吾说好像有。两种答案都不完全准确因为MySQL的InnoDB引擎在Repeatable Read下通过MVCC解决了快照读的幻读但当前读仍然存在幻读可能。这就是典型的“背了结论但没理解机制”的表现。所以这篇“祖传八股文”我打算把MySQL事务从底层日志一直串到 Spring 事务失效再延伸到分布式事务把它彻底讲透。1.1 事务的本质一组不可分割的操作先回到最朴素的定义。事务是数据库执行的最小逻辑工作单元它包含一组SQL操作要么全部执行成功要么全部回滚。最经典的例子就是转账A账户扣100B账户加100。如果扣款成功但入账失败这笔钱就凭空消失了这是绝对不能接受的。但这里要稍微拔高一点事务不只是“要么全做要么全不做”它本质上是解决并发场景下多个操作交错执行时的正确性问题。单线程执行SQL根本不需要事务是因为有了并发事务机制才有了意义。MySQL中的事务由存储引擎层实现注意只有InnoDB支持事务MyISAM不支持的。建表时如果没有显式指定引擎MySQL 5.5.5以后默认就是InnoDB这点已经不用强调但每次回答时带一句能体现你对存储引擎的了解程度。1.2 一个事务从开始到结束经历了什么标准事务生命周期可以拆成四步START TRANSACTION; -- 或者在客户端里直接 BEGIN; UPDATE account SET balance balance - 100 WHERE id 1; UPDATE account SET balance balance 100 WHERE id 2; COMMIT; -- 出错的话执行 ROLLBACK;一条UPDATE会经历什么先查Buffer Pool里有没有目标页没有就加载然后更新内存中的行数据接着写undo log和redo log最后在合适的时机由后台线程把脏页刷盘。COMMIT代表事务的写入被确认即使数据库在COMMIT后立刻宕机重启后数据也能通过redo log恢复。这个流程里藏着一个关键点COMMIT才是持久性的分水岭。在COMMIT之前宕机事务回滚数据恢复原样COMMIT之后宕机事务虽然没来得及刷脏页但redo log已经落盘重启后会重放事务不会丢。1.3 面试常见的第一个坑ACID里的一致性到底是什么ACID里的C一致性是四个特性里最容易被一句话带过的但它其实是最抽象的一个。原子性靠undo log保证隔离性靠锁和MVCC保证持久性靠redo log保证——这些都是机制层面的实现。唯独一致性它不是一个独立的技术而是一个业务层面的约束事务操作前后数据必须满足所有预定义规则包括约束、级联、触发器等等。也就是说一致性是结果其他三个特性是手段。如果面试官问“一致性靠什么保证”回答“靠A、I、D共同保证”比背“指数据库从一个一致状态变为另一个一致状态”要高级得多。这属于那种“一句话暴露深度”的考点。2. 从redo log和undo log看ACID的底层保障面试进行到这里如果候选人ACID背得还行面试官大概率会追加一个致命问题“这些特性底层是怎么实现的”这时候就得把日志机制搬出来。说实话MySQL事务能成立靠的就是三类日志redo log、undo log、binlog。它们各自分工又互相配合理解这三者才算真正懂事务。2.1 redo log崩溃后凭什么数据不丢redo log是InnoDB自己维护的物理日志记录的是“对哪个数据页做了什么修改”比如“页号100偏移量200将值从A改为B”。为什么需要它因为InnoDB的数据页默认16KB一条UPDATE可能只改了其中几个字节。如果每次修改都立即把整个脏页刷回磁盘磁盘IO开销太大性能扛不住。所以InnoDB采用WALWrite-Ahead Logging机制先写日志再刷脏页。数据页可以留在内存里慢慢刷但redo log必须尽快落盘这样即使宕机也能根据redo log重放修改。这里有个面试官非常爱深挖的点redo log什么时候刷盘这由innodb_flush_log_at_trx_commit参数控制有三个可选值0提交时不写redo log每秒刷一次。性能最好但MySQL崩溃会丢最多1秒的事务。你想想如果业务不能接受数据丢失这个配置就是灾难。1每次提交都刷盘。最安全默认值适合金融类业务。每次COMMIT都要等磁盘fsync性能开销最大。2提交时写到操作系统缓存每秒再刷盘。MySQL进程崩溃不丢但操作系统崩溃可能丢1秒。性能与安全的折中选择。我实际调优时见过一个案例某个报表系统的写入压力巨大DBA把innodb_flush_log_at_trx_commit改成了0写入性能翻了两倍但换来的是崩溃时最多丢1秒数据。这种取舍必须由业务方确认不能DBA自己拍板。2.2 undo log回滚和MVCC的双重身份和redo log相对undo log记录的是逻辑日志方向相反用来“撤销”操作。比如执行INSERTundo log记录删除该行的信息执行DELETEundo log记录该行的完整旧数据执行UPDATEundo log记录旧值。它的第一个作用自然是回滚。事务执行到一半出错或者手动ROLLBACKInnoDB就根据undo log把数据恢复原状。但第二个作用更关键——它是MVCC快照读的底层支撑。每一行记录被修改时会通过undo log形成一个版本链事务读取时沿着版本链找到自己“看得见”的版本。这一块在讲MVCC时会展开。2.3 binlog和redo log的两阶段提交binlog是MySQL Server层维护的逻辑日志记录SQL语句或行数据变更主要用于主从复制和数据恢复。为什么需要两阶段提交因为redo log属于InnoDB引擎层binlog属于Server层两个日志分属不同模块如果不做协调同步过程中一旦宕机就可能出现“引擎层已提交、binlog没写”或相反的情况。那样从库就会和主库数据不一致。两阶段提交的流程大致是事务执行过程中InnoDB写redo log状态标记为prepare准备阶段。Server层写binlog并落盘。InnoDB收到确认后把redo log状态改为commit提交阶段。如果步骤2之后、步骤3之前宕机重启后会检查binlog里有没有这个事务有就补完提交没有就回滚。这套机制保证了两个日志的一致性。面试时只要能把这条线讲清楚基本就让面试官知道你不是背的。3. 四种隔离级别与并发异常面试的标准战场隔离级别是整个MySQL事务里最常考的章节没有之一。原因很简单它直接关联并发场景下的数据正确性也是实际开发中问题最多的地方。3.1 并发事务会带来哪三个问题先明确三个并发异常的定义这是讨论隔离级别的地基。脏读事务A读到了事务B未提交的数据。如果事务B回滚事务A就拿到了数据库中根本不存在的“脏数据”。比如事务B把金额从100改成50还没提交事务A读到了50但事务B随后回滚金额仍是100事务A后面所有基于50的运算全错了。不可重复读同一个事务内两次读同一行结果不一样。原因是事务B在A的两次读之间修改并提交了数据。举个例子事务A先读到金额100事务B改成50并提交事务A再读一遍变成50。幻读同一个事务内两次执行同一个查询结果集的行数不一样。比如事务A按条件WHERE balance 100查到了5条记录事务B插入一条balance为200的新记录并提交事务A再查一次发现了6条。新增的记录对A来说就像“幻觉”一样。这里有个高频辨析题不可重复读和幻读的区别是什么核心差异在于操作粒度——不可重复读针对同一行数据的值变化幻读针对结果集的记录数量变化。前者是“值变了”后者是“多了行”。3.2 隔离级别如何一一对应SQL标准定义了四种隔离级别MySQL的InnoDB都支持从宽松到严格排列隔离级别脏读不可重复读幻读实现机制Read Uncommitted可能可能可能直接读最新版本Read Committed不可能可能可能每次读生成新ReadViewRepeatable Read不可能不可能可能InnoDB快照读下避免事务内复用同个ReadViewSerializable不可能不可能不可能所有读都加锁完全串行注意表格里的措辞。SQL标准说RR级别可能幻读但InnoDB在RR级别下通过MVCC快照读避免了幻读这就是MySQL和标准不一致的地方也是面试官最爱挖坑的点。3.3 InnoDB为什么把默认隔离级别设为RRMySQL的InnoDB默认隔离级别是Repeatable Read这一点和很多数据库不同。PostgreSQL、Oracle的默认级别是Read Committed。为什么MySQL选了RR历史原因是MySQL的早期主从复制在binlog为statement格式时如果使用RC级别某些事务的执行顺序在主从库上会产生不同结果复制容易出错。虽然现在binlog有row格式解决了这个问题但默认值一直保留下来。到了今天如果你在新建项目里没有特殊需求我认为完全可以用RC级别因为RC能减少间隙锁导致的死锁概率并发处理能力更好。MySQL 8.0以后statement格式的statement被标记为弃用RC的使用环境也成熟了。不过这只是我的实际经验面试时先聊机制再给观点会显得更有底气。3.4 一个SQL实验演示RR下快照读的表现用一个最小复现实验来看清RR级别下快照读的特性。-- 会话A START TRANSACTION; SELECT * FROM account WHERE id 1; -- 读到balance 100 -- 会话B START TRANSACTION; UPDATE account SET balance 50 WHERE id 1; COMMIT; -- 会话A再次查询 SELECT * FROM account WHERE id 1; -- 仍然读到balance 100快照读在RR级别下会话A事务开始后第一次SELECT就生成ReadView后续所有普通SELECT都复用这个视图所以即使B提交了A看到的还是100。但如果A执行的是当前读比如SELECT ... FOR UPDATE情况就变了SELECT * FROM account WHERE id 1 FOR UPDATE; -- 读到 balance 50读的是最新已提交版本这就是很多人搞晕的快照读与当前读的区别。当前读永远读最新已提交版本并且会加锁快照读读的是事务开始时的快照。隔离级别对读行为的影响必须区分这两种读再来谈否则一定绕晕。4. MVCC快照读隐藏列、版本链与ReadView的完整推演MVCCMulti-Version Concurrency Control多版本并发控制是MySQL事务最核心的机制之一也是面试深度最容易拉开差距的地方。我把它的三个关键构件一次性讲清楚。4.1 MVCC解决什么问题不能解决什么问题MVCC的核心思路是同一行数据保存多个历史版本读操作不需要加锁就能读到一致的数据从而大幅度提升并发度。它主要解决的是读写冲突——读不阻塞写写不阻塞读。我们通常把普通SELECT称为快照读把SELECT ... FOR UPDATE、DELETE、UPDATE称为当前读两种读的规则不同这个区分在前面已经提过这里是整个MVCC理解的分水岭。需要特别指出的是MVCC没有解决写写冲突。两个事务同时修改同一行还是需要加锁排队。所以MVCC和行锁是并存的它们各自处理不同类型的问题。4.2 隐藏列和undo log版本链InnoDB的聚簇索引记录里有三个隐藏列DB_TRX_ID最近一次修改该行记录的事务ID。DB_ROLL_PTR回滚指针指向该记录上一个版本的undo log。DB_ROW_ID行ID当表没有主键时用它作为聚簇索引这个不常考。每次更新一行记录时不是直接覆盖旧值而是把当前版本的值写入undo log。把新版本的DB_TRX_ID改成当前事务ID。把新版本记录的DB_ROLL_PTR指向undo log中旧版本的位置。这样多次更新同一行后这一行就形成了一条以物理记录为头、以undo log往前追溯的版本链。MVCC判断一条记录是否可见就是沿着这条链找。4.3 ReadView判断可见性的规则ReadView就是事务做快照读时生成的“可见性判定快照”记录以下几项关键信息m_ids生成ReadView时系统中活跃未提交事务的ID列表。min_trx_idm_ids中最小的活跃事务ID。max_trx_idm_ids中最大的事务ID加1其实表示“下一个将被分配的事务ID”。creator_trx_id创建这个ReadView的事务自己的ID。判断逻辑是条件判断结果trx_id小于min_trx_id该版本已提交可见trx_id等于creator_trx_id自己修改的可见trx_id大于等于max_trx_id该事务在生成ReadView之后才开启不可见min_trx_id trx_id max_trx_id且trx_id不在m_ids中该事务已提交可见min_trx_id trx_id max_trx_id且trx_id在m_ids中该事务未提交不可见如果某个版本对当前事务不可见就沿着版本链往上找下一个版本再判断。这套规则理解之后再回看RC和RR的区别就很清晰了RC级别每次SELECT都生成新的ReadView所以能读到其他事务新提交的数据出现不可重复读。RR级别事务第一次SELECT时生成ReadView后续一直复用所以事务内读到的数据保持一致。面试时能把RC和RR的差异归结到ReadView生成时机这一个点上就是真的理解MVCC了。4.4 一个完整的可见性推演案例来推演一个具体案例。假设当前事务ID是10我执行第一次SELECT时生成ReadView此时系统里有事务12和事务15还在跑活跃max_trx_id是16。版本链头部是事务20写入的版本那对于我来说这个版本不可见因为trx_id20大于16。接着往前找如果找到一个trx_id8的版本8小于min_trx_id12说明事务8已经提交了这个版本可见。如果是事务10自己修改的行呢版本链头部的trx_id等于自己的ID直接判定可见。这也是为什么同一事务内更新后能立刻看到新值不用管别的。5. 当前读加锁行锁、间隙锁与死锁的实战推演快照读解决的是普通SELECT的场景但更新数据、抢锁、防超卖这些场景需要的是当前读。当前读必须加锁锁机制才是并发写控制的真正主力。5.1 InnoDB的锁类型以及它们之间的关系按锁粒度分InnoDB有表级锁和行级锁。表级锁主要是MDL锁元数据锁和AUTO-INC锁前者保护表结构后者用于自增主键。行级锁有三种Record Lock记录锁锁的是索引记录本身。Gap Lock间隙锁锁的是索引记录之间的间隙。Next-Key Lock临键锁记录锁和间隙锁的组合同时锁住记录和前面的间隙。锁的兼容性矩阵需要记住兼容性共享锁S排他锁X共享锁S兼容不兼容排他锁X不兼容不兼容5.2 为什么RR级别下会出现间隙锁这就要回到幻读问题本身。在RR级别下快照读已经被MVCC解决了幻读但当前读没有。两个事务同时执行SELECT ... WHERE id 5 FOR UPDATE如果只锁住已有记录另一个事务插入一条id6的新记录前一个事务再查就会多出一行这样事务内部两次当前读结果不一致幻读就发生了。InnoDB的解法是给条件涉及的索引区间加间隙锁锁住“5到正无穷”这个范围其他事务插入id6的记录时会被间隙锁阻塞。这是锁机制对MVCC盲区的补位。RC级别则不加间隙锁只锁记录本身因此冲突更少、死锁概率更低这也是我说新项目可以考虑RC的原因之一。5.3 一条UPDATE语句实际加锁的范围加锁范围不取决于表结构本身而是取决于WHERE条件走的索引类型。这是所有Java开发必须知道的事因为一旦没走索引锁就会升级成全表扫描而每条扫描记录都会加锁最终可能导致整个表都被锁住线上直接炸。-- 假设 account 表在 balance 字段上没有索引 START TRANSACTION; UPDATE account SET balance balance - 100 WHERE balance 200;这条SQL在执行时会全表扫描InnoDB对扫描到的每一条记录都加锁实际效果和锁全表没有本质区别。如果并发量稍大所有对该表的写操作都会阻塞性能瞬间雪崩。加索引后WHERE balance 200能走索引锁定的范围就只限定在满足条件的记录上。开发规范里常说的“更新操作必须用索引字段做条件”就是这个原因不是DBA在刁难你。5.4 死锁的经典案例和排查手段死锁的本质是两个事务各自持有锁又互相等待对方持有的锁。经典案例事务AUPDATE account SET balance balance - 100 WHERE id 1; -- 持有id1的行锁 事务BUPDATE account SET balance balance - 100 WHERE id 2; -- 持有id2的行锁 事务AUPDATE account SET balance balance - 100 WHERE id 2; -- 等待B释放id2 事务BUPDATE account SET balance balance - 100 WHERE id 1; -- 等待A释放id1InnoDB检测到死锁后会选择一个代价较小的事务回滚让另一个继续执行所以应用层一般会收到Deadlock found when trying to get lock的异常。排查死锁的方法执行SHOW ENGINE INNODB STATUS查看最近一次死锁的详细日志里面会打印两个事务各自持有哪些锁、等待哪个锁。检查业务代码中多个事务操作同一组资源的顺序是否一致。看看是不是某个更新的WHERE条件没走索引导致锁范围意外扩大。我踩过最深的坑就是两个微服务更新同一张表的一批数据一个按id升序更新一个按id降序更新结果每天固定时间报死锁。改成统一按id升序后问题彻底消失。这就是典型的“按固定顺序访问资源”原则。6. Spring事务失效面试官最爱追问的一层MySQL事务的机制讲完之后面试官通常会顺理成章把话题引到Spring事务上。毕竟真实项目中没人会用原生JDBC手写事务基本都是靠Spring的Transactional管理。但Transactional不是万能的它的失效场景几乎是Java后端面试的必考压轴题。6.1 自调用导致事务失效是最容易踩的坑最常见的失效场景是同一个类内部方法自调用Service public class OrderService { public void createOrder() { // 做一些业务逻辑 updateStock(); // 这是一个自调用 } Transactional public void updateStock() { // 更新库存 } }updateStock()上的Transactional完全不会生效。原因是Spring事务默认基于动态代理实现Transactional只有在外部通过代理对象调用时才会被拦截。这里的createOrder()调用updateStock()走的是this对象也就是原始对象不是代理对象拦截器根本没有机会介入。解决方法有两个一是把事务方法拆到另一个Bean里通过注入的Bean调用二是自注入OrderService的代理对象再调用。个人更推荐第一种职责分离得更好。6.2 异常被吞掉是事务静默失效的元凶Transactional public void createOrder() { try { updateStock(); insertOrder(); } catch (Exception e) { log.error(订单创建失败, e); } }这个代码里异常被catch住没有抛出。Spring事务拦截器默认只在方法抛出RuntimeException或Error时回滚方法正常返回了它当然认为事务成功直接提交。结果是库存扣了但订单没插入数据不一致而且线上日志看起来一切正常。我看过不少生产事故就是这种“try-catch吞异常”导致的。解决方案是不要在事务方法内部catch后不抛出如果确实要catch需要手动TransactionAspectSupport.currentTransactionStatus().setRollbackOnly()标记回滚。6.3 非public方法、同类多数据源、传播行为配置错误等失效场景还有一些高频失效原因场景一Transactional加在private方法上。Spring动态代理只能拦截public方法private方法直接不参与代理。这个问题在代码评审时经常能抓到。场景二异常类型不是RuntimeException。默认情况下只有运行时异常和Error触发回滚检查性异常比如FileNotFoundException不会回滚需要显式声明rollbackFor Exception.class。场景三传播行为配置不当导致外层事务提前提交。比如内层方法标注REQUIRES_NEW它的提交独立于外层事务。如果外层事务后续回滚内层已经提交的数据就无法回滚了。整理成一张排查表方便面试和日常排查失效场景原因解决方式同类自调用走的是原始对象不是代理对象拆Bean或自注入代理异常被catch事务拦截器收不到异常信号重新抛出或手动标记回滚非public方法动态代理只拦public改为public检查型异常默认不触发回滚加rollbackFor Exception.class传播行为REQUIRES_NEW内层事务独立提交评估是否需要嵌套事务方法内多线程子线程拿不到事务上下文用编程式事务或避免跨线程事务6.4 大事务比事务失效更隐蔽的性能杀手面试时如果能主动讲出大事务的问题往往能加分这是很多候选人没准备的盲区。一个事务里执行了大量SQL或长时间占用连接会带来三个问题锁持有时间过长。事务没提交前它加的行锁或间隙锁一直不释放其他事务只能阻塞等待。undo log膨胀。长事务意味着MVCC版本链很长清理线程无法及时回收undo可能导致版本链过长、查询性能下降。连接资源被长时间占用。连接池里的连接是有限的一个事务跑几秒连接就被占用几秒高并发下连接池很快被耗尽。解决大事务的思路很直接事务范围能小就小事务里只放必要的写操作查询、远程调用、耗时的IO操作统统挪到事务外面。这也是我在代码评审时反复强调的一条线。7. 从分布式事务看事务边界的扩展聊完本地事务面试官往往会把话题进一步放大“如果订单服务和库存服务是两个独立部署的服务事务怎么保证”这直接跳到分布式事务。注意面试官不是要你完整实现一个分布式事务框架而是想看你对数据一致性的思考深度。这部分答案分层次从最简单的方案讲到重方案。7.1 两阶段提交2PC的假设和局限2PC是最经典的分布式一致性协议。引入一个协调者角色分为准备阶段和提交阶段准备阶段协调者问所有参与者能否提交参与者执行事务但先不提交各自返回Yes或No。提交阶段如果全部返回Yes协调者通知所有人提交有任何一个返回No协调者通知所有人回滚。这个方案的致命弱点是协调者单点故障。如果协调者在第二阶段宕机参与者们会一直卡在“已准备未提交”的状态无法决策。而且整个过程是同步阻塞的性能不高。这也是后来为什么出现TCC、消息最终一致性等方案的原因。7.2 TCC补偿式事务的优与痛TCC是Try-Confirm-Cancel的缩写。它把每个操作拆成三个阶段Try完成资源预留。比如扣库存时先把库存从100冻结10冻结库存10可用库存90。Confirm确认执行。Try阶段所有参与者都成功就执行Confirm把冻结库存真正扣掉。Cancel取消执行。有参与者失败就执行Cancel把预留的资源释放。TCC的好处是粒度可控业务上保留了高灵活性性能比2PC好。代价是侵入性强每个业务接口都要写三套逻辑开发和维护成本很高。实际项目里我见过很多团队把TCC写成事务脚本Confirm和Cancel复用了同一段逻辑这在资金类业务里很容易踩大坑复杂度远远超出预期。7.3 本地消息表和最大努力通知最终一致性的务实方案在大多数互联网业务中订单和库存的分布式事务需求其实是“最终一致性”就能满足的没必要上强一致方案。本地消息表的核心思路在订单服务所在的数据库建一张message表业务操作和写消息表在同一个本地事务里完成然后通过异步任务把消息发到消息队列。下游库存服务消费消息执行扣减如果失败就重试直到成功。最大努力通知则是一种更轻量的设计发起方把结果通过消息队列或回调接口反复通知接收方接收方收到后做幂等处理实在通知不到就记录日志人工处理。它不保证消息一定送达只保证“尽力而为”适合对一致性要求不那么极端的场景。订单与库存这个经典案例我实际推演过下单时先锁定优惠券和扣减本地乐观锁库存库存服务失败时通过MQ重试同时有定时任务扫描超时订单做补偿。这套方案不需要引入Seata成本低基本能满足绝大多数电商场景。7.4 Seata的AT模式在实际项目里的定位Seata是目前Java生态里用得最多的分布式事务框架。它的AT模式有一个关键思路对业务无侵入。AT模式的执行过程大致是事务发起方注册全局事务拿到全局事务ID。分支事务执行业务SQL时Seata自动记录数据快照到undo_log表。分支事务执行成功后先不提交等待全局事务决策。全局事务提交时分支事务才真正提交回滚时Seata根据快照反向补偿。AT模式的优点是无侵入业务代码几乎不用改。但缺点是依赖数据库本地事务性能有一定损耗而且对SQL有要求比如必须带主键条件。在并发量特别高的场景可以考虑TCC。选型永远没有银弹关键看业务的一致性等级和性能预算。8. 面试官问完这些之后我的回答思路聊了这么多最后回到面试本身。MySQL事务这个话题能聊的深度几乎没有上限但一场面试的时间有限会问这个问题的人不一定是要把所有细节都背下来。8.1 回答的主线从机制到实践再到底层我自己的回答通常分三层递进第一层讲清楚事务的四条特性和隔离级别这部分是基本盘。面试官如果只想知道你会不会用到这里就够了。此时用一两句话总结并发环境下事务的隔离级别决定了一个事务能“看”到其他事务的哪些修改。第二层往下挖机制。主动说MVCC、ReadView、redo log、undo log。面试官听你主动讲这些会意识到你是真的读过源码或深入分析过而不是背了面试题。这里有技巧不用把所有细节全倒出来先讲结论留出空间让面试官追问细节引导面试朝你熟悉的方向走。第三层讲实践中的坑。自调用事务失效、异常被吞、大事务锁表、死锁排查——这些都是我自己在线上项目和代码评审里真实踩过的讲出来的时候不用背稿子直接讲当时的现象和排错过程就行。8.2 高频追问的应对建议我总结几个最常被追问的问题以及建议的回答方向RR下到底有没有幻读标准回答是快照读没有、当前读有间隙锁只是尽量减小当前读幻读的概率。别一刀切说没有。SELECT COUNT(*) 是快照读吗在RR级别下第一次执行就确定了快照事务内多次执行结果一致。但如果是SELECT COUNT(*) FOR UPDATE那就是当前读结果会随其他事务提交而变化。隔离级别能不能动态修改可以一个连接级别用SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED全局用SET GLOBAL。但要注意分布式场景下每个连接都可能走不同配置压测时要特别留心。为什么有人推荐RC因为RC下不加间隙锁死锁概率更低并发更高。主从同步用row格式后RC在复制上也没问题。适合不需要在事务内做复杂一致性读的业务。8.3 我的真实体会最后说一点个人的经验。我整理这套笔记的时候本意是给面试做准备但后来发现真正受益最大的是日常开发排查死锁时因为知道锁的兼容矩阵一眼就能看出是Gap Lock冲突写事务方法时因为知道自调用失效的原理代码评审时能精准指出问题定位线上数据不一致时因为知道两阶段提交的时序能快速判断是binlog同步问题还是业务逻辑问题。事务知识不是背下来给面试官听的它是排查问题时间接帮你的那根拐杖。把原理吃透面试自然会过代码也会更稳。希望这篇拆解能帮到你如果有项目里的实践问题随时可以再聊。
返回列表