
1. 直击本质先搞清楚事务到底在解决什么问题1.1 转账场景里的“钱必须守恒”聊MySQL事务之前先把场景摆出来。假设你在写一个转账接口从A账户扣100给B账户加100。如果这两条SQL中间任何一条执行失败、数据库崩溃、网络抖动最尴尬的结果就是A扣了钱B没到账或者A没扣钱B多了100。这个场景说出来谁都懂但落到工程实现上“保证一组操作要么全成功要么全不成功”这件事远比想象中复杂得多。什么是事务一句话事务是数据库执行的最小逻辑工作单元事务内部的一组SQL要么全部提交成功要么全部回滚不允许存在中间态。MySQL里默认每条SQL都是自动提交的也就是说你执行一条UPDATE它本身就是独立事务执行完立刻落盘。只有显式地BEGIN、START TRANSACTION或者在编程框架里开启事务这一组SQL才会被当成一个整体来对待。这里有一个必须澄清的误区事务不是MySQL发明的概念而是数据库领域的通用规范但MySQL在实现上有自己的独特机制。面试时问“MySQL事务”真正考察的其实是四层东西ACID理论、隔离级别、MVCC与锁、以及redo/undo日志这四层承上启下缺一不可。后面我会一层层拆开讲。1.2 面试官为什么揪着事务不放讲个有意思的事。我做了几年后端参加过不少面试也当过面试官事务这个知识点几乎每场必问。为什么因为它能完美区分“背过八股”和“真干过活”的人。基础题问ACID四个特性背过书的人都能答上来追问一句“MySQL默认隔离级别是什么为什么选这个”不少人就开始支支吾吾再往深一步问“RR级别下会不会出现幻读MVCC怎么生成的ReadView”能说清楚的人就真的不多了。而实际工作中事务隔离级别选错导致的数据错乱、长事务拖垮主库、Spring事务自调用失效这些坑几乎每个人都踩过。我曾经维护过一个订单系统线上出现过一次“超发”事故并发下单时库存扣成了负数。排查到最后发现问题出在事务隔离级别被人改成了READ UNCOMMITTED读到了别的事务未提交的数据。这种问题不深入理解事务光靠眼睛盯代码是盯不出来的。所以这篇内容我不会只罗列概念而是会把事务从理论到实现、从单机到分布式、从原理到排障完整走一遍保证你面试能聊、干活能用。2. 事务的四大特性与隔离级别网上都在背但真正理解的不多2.1 原子性、一致性、隔离性、持久性到底谁保证的ACID这四点的官方定义大家都见过但我要说点干货这四点在MySQL里并不是平级的它们是由不同组件分别负责的。原子性指的是事务里的所有操作不可分割要么全做要么全不做。在MySQL里原子性靠undo log实现。事务执行过程中每修改一条数据就会生成一条相反操作的undo记录。比如你执行了UPDATEundo里就存着修改前的旧值如果事务回滚InnoDB就根据undo log把数据恢复原样。一致性是最抽象的它不是一个独立机制而是原子性、隔离性、持久性三者共同作用的结果。换句话说一致性是目标其他三个是手段。从应用层看一致性就是业务规则不被破坏比如转账前后总金额不变、库存不为负、订单状态流转合法。数据库能保证的“一致性”是有限的真正复杂的一致性约束还得应用层配合。隔离性解决的是并发事务互相干扰的问题。MySQL的隔离性靠锁和MVCC多版本并发控制实现。多个事务同时操作同一批数据要么通过加锁串行执行要么通过快照读让每个事务看到自己该看的版本后面我会详细展开。持久性指的是事务提交后数据不丢失。MySQL靠redo log保证。提交事务时不只是改内存里的数据页还会先把redo log刷到磁盘。就算数据库瞬间宕机重启后也能通过redo log把已提交的事务重放出来一条不丢。这四个特性面试时最好用一句话概括原子性是操作层面的保证持久性是存储层面的保证隔离性是并发层面的保证一致性是业务层面的最终目标。这样回答立刻和只会背ABCD定义的人拉开差距。2.2 四种隔离级别分别解决什么问题SQL标准定义了四种隔离级别从低到高依次是READ UNCOMMITTED读未提交事务可以读到其他事务未提交的数据。这个级别几乎不用因为它会产生脏读。举个例子事务A改了一行数据还没提交事务B就看到了修改后的结果结果A回滚了B读到的就是一笔“假数据”。READ COMMITTED读已提交事务只能读到其他事务已提交的数据解决了脏读但会出现不可重复读。什么叫不可重复读同一个事务里执行两次相同的SELECT结果不一样。原因就是两次查询之间有其他事务提交了修改。RC级别下每次SELECT都会生成新的快照所以看到的数据版本可能不同。REPEATABLE READ可重复读同一个事务里多次读取同一批数据结果保持一致。这就是MySQL InnoDB的默认隔离级别。RR级别解决了不可重复读但理论上有幻读问题。幻读指事务内两次查询返回的记录数不同比如第一次查出来5条第二次查出来6条多出来的记录是其他事务刚插入并提交的。注意关键词是“插入”不是“修改”所以和不可重复读严格说是两种问题。InnoDB在RR级别下通过间隙锁Gap Lock和MVCC的组合在绝大多数场景下已经规避了幻读。这也是我后文要重点讲的“MySQL在RR下并不完全符合SQL标准定义”。SERIALIZABLE串行化最高级别所有事务串行执行读写都加锁没有并发问题但性能最差基本没人拿它当默认配置。这里有一个非常关键的面试陷阱SQL标准对隔离级别的定义和MySQL的实际实现是有出入的。SQL标准认为RR不能解决幻读但MySQL的RR使用了间隙锁和MVCC在大部分场景下已经把幻读挡掉了。面试官如果问“MySQL RR级别下还会幻读吗”答案是在特定条件下仍然可能发生比如当前读中范围条件未覆盖到的间隙、或者并发插入边界值但常规的并发查询不会。这个问题答得漂亮就是加分项。2.3 一个表格看清隔离级别与并发问题的对应关系隔离级别脏读不可重复读幻读典型场景READ UNCOMMITTED可能可能可能几乎不用READ COMMITTED不会可能可能大多数业务系统的合理选择REPEATABLE READMySQL默认不会不会基本不会间隙锁MVCC电商订单、金融交易SERIALIZABLE不会不会不会极少数强一致场景很多团队为了让MySQL支持“读已提交”、降低间隙锁带来的并发冲突会把隔离级别改成RC。阿里很多内部规范就推荐RC因为RR的间隙锁在并发插入时很容易造成死锁。但改之前要想清楚你的业务能不能接受同一事务内两次查询结果不一致如果不能保持RR。这个取舍没有绝对标准完全看场景。3. 深入InnoDB事务到底是怎么实现的3.1 一条UPDATE在事务里经历了什么讲原理不说过程就是耍流氓。拿一条最普通的UPDATE语句来拆解看看InnoDB在事务内部到底做了什么。假设表里有一行数据主键ID100金额字段从500改成400第一步先把这条记录所在的页从磁盘加载到内存缓冲池Buffer Pool。第二步给这条记录加上排他锁X锁防止别的事务同时修改。第三步把修改前的旧值500写入undo log。第四步在内存中把值改成400同时标记数据页为脏页。第五步把“我改了ID100这行旧值500新值400”这个操作追加到redo log buffer。第六步事务提交时把redo log刷到磁盘具体刷盘策略由innodb_flush_log_at_trx_commit控制。之后后台线程会在适当时机把脏页刷回磁盘完成最终落盘。注意一个关键点数据页不是提交时就刷盘的而是后台异步刷盘。所以持久性靠的不是数据页而是redo log。redo log是顺序写的速度远快于随机写这也是InnoDB能承受高并发写入的根本原因。为了这个机制我曾经吃过一个亏。生产环境配置的innodb_flush_log_at_trx_commit0想着性能能提升不少结果一次数据库宕机恢复后发现最近一两秒提交的事务丢了。后来才明白这个参数设成0redo log根本不刷盘靠后台线程每秒刷一次性能最好但可能丢数据设成1每次提交都刷盘最安全但慢设成2提交时只写入操作系统缓存由系统决定何时落盘比0安全比1快。对于金融类业务必须设成1非核心业务可以接受丢最后一秒数据时用2是相对平衡的。3.2 日志三板斧redo log、undo log、binlog的分工MySQL里一共有三份关键日志功能和用途完全不同经常有人搞混。redo log和undo log是InnoDB存储引擎层的binlog是MySQL Server层的。redo log是物理日志记录的是“数据页做了什么修改”比如“把页5的偏移量100的值改成400”它只关心物理变化服务于崩溃恢复保证持久性。undo log是逻辑日志记录的是“修改前的数据长什么样”服务于事务回滚和MVCC快照读。binlog是归档日志记录的是“执行过的SQL逻辑”比如“执行了UPDATE t SET money400 WHERE id100”它服务于主从复制、数据备份和误删恢复。事务提交时redo log会先刷盘binlog随后刷盘两者之间的协调靠内部事务IDXID关联。两阶段提交这套机制就是为了保证redo log和binlog的一致性如果先写redo后写binlog中途崩溃可能导致主从数据不一致如果先写binlog后写redo同理。所以MySQL把提交过程拆成prepare和commit两个阶段这也是分布式事务思想的单机版体现。日常开发和面试时最容易混淆的就是redo log和binlog。你记住一句话redo log是“保命”的数据库崩溃后靠它恢复binlog是“续命”的主从同步、数据恢复靠它。两份日志缺一不可但职责完全不同。3.3 MVCC同一行数据多个事务各看各的版本MVCC全称Multi-Version Concurrency Control中文叫多版本并发控制。它是InnoDB实现高并发读的核心机制也是面试题里出场率最高的一块。理解MVCC之前先要理解两个概念当前读和快照读。当前读读的是最新已提交版本并且要加锁。比如SELECT ... FOR UPDATE、UPDATE、DELETE都属于当前读。快照读读的是一条历史版本不加锁普通的SELECT就是快照读。MVCC主要服务的就是快照读让读操作不阻塞写操作写操作也不阻塞读操作。这是InnoDB并发性能强大的根本原因。每一行数据实际上有多个隐藏列其中包括隐藏主键、事务IDDB_TRX_ID最近一次修改该行的事务ID和回滚指针DB_ROLL_PTR指向undo log中该行的上一个版本。当一个事务修改某行数据时不会把旧版本直接覆盖掉而是把旧版本写入undo log新版本在原位置生成回滚指针指向旧版本。这样一行数据就在undo log里形成了版本链。快照读时事务根据自己生成的ReadView读视图沿着版本链找到自己“可见”的那个版本。ReadView核心内容是当前系统活跃事务ID列表、最小活跃事务ID、最大事务ID。判断规则说白了就一条如果版本的事务ID小于当前事务ID说明是已提交的旧版本可见如果版本的事务ID在当前事务之后开始不可见如果版本的事务ID是活跃的未提交事务不可见。通过这条规则RR级别下事务第一次执行SELECT时生成ReadView之后一直复用所以多次读取结果一致RC级别下每次SELECT都重新生成ReadView所以可能读到新提交的数据。我自己的理解是MVCC相当于给每个事务发了一副“带时间戳的眼镜”RR的眼镜是一次性定制的整场电影看起来都一个样RC的眼镜每次眨眼前都会重启所以看到的世界会变。这个比喻面试时说出来面试官通常会点头。3.4 锁机制行锁、间隙锁、临键锁的配合MVCC解决了快照读的并发问题但当前读还得靠锁。InnoDB的锁按粒度分有表锁和行锁行锁又分共享锁S锁读锁和排他锁X锁写锁。读读不互斥读写互斥写写互斥。InnoDB的行锁是基于索引实现的如果查询没有走索引行锁会退化成表锁这是很多并发性能问题的根源。在RR隔离级别下为了处理幻读InnoDB引入了间隙锁Gap Lock。间隙锁锁的是一个范围而不是具体的行它的作用是防止其他事务在这个范围内插入新记录。临键锁Next-Key Lock则是行锁和间隙锁的组合锁住的是“记录本身记录前面的间隙”。因为有了这一套组合事务A在某个范围做当前读时事务B试图往这个范围的间隙里插数据就会被阻塞从而在锁层面堵住了幻读。但是临键锁也带来一个负面问题死锁。举个例子事务A先锁了ID1的行想再锁ID2的行事务B先锁了ID2的行想再锁ID1的行。两个事务互相等待对方释放锁死锁就产生了。InnoDB内部有死锁检测机制会主动回滚代价较小的事务来打破死锁。但频繁发生死锁本身就说明SQL设计有问题比如事务里操作表的顺序不一致、索引区分度低导致锁范围过大。排查死锁最直接的办法是执行SHOW ENGINE INNODB STATUS看LATEST DETECTED DEADLOCK部分里面会明确告诉你两个事务各持有什么锁、在等什么锁。4. 事务传播机制与事务失效Java开发者的必修课4.1 Spring事务传播行为七种行为怎么选后端开发用Spring管理事务是日常操作Spring的Transactional注解背后是AOP代理通过拦截方法调用在方法执行前开启事务、执行后提交或回滚。这里有个前置知识必须先讲Spring事务本身是基于数据库事务的封装它不改变数据库事务的底层语义只是在应用层增加了管理能力。传播行为就是当多个事务方法互相调用时事务如何在这些方法之间扩散。Spring定义了七种传播行为面试常考且日常常用的主要是以下三种REQUIRED是默认值如果当前没有事务就新建一个如果有就加入当前事务。大多数业务方法都该用它。REQUIRES_NEW表示无论如何都新建一个事务如果当前有事务先把当前事务挂起。适合记录日志、发送消息这类“子任务失败不能影响主任务”的场景。NESTED表示如果当前有事务则创建一个嵌套事务嵌套事务回滚不影响外层事务如果当前没有事务行为等同于REQUIRED。适合部分业务需要局部回滚的场景。我实际项目里就遇到过用REQUIRES_NEW的经典案例。一个订单创建入口主流程保存订单和扣减库存同时要写一条操作日志。日志服务相对独立如果日志写入失败导致整个订单创建回滚那用户体验就很差了。把日志方法标记为REQUIRES_NEW日志失败只回滚日志自己的事务订单照常提交。但是这里有个隐蔽陷阱REQUIRES_NEW会开启独立物理连接在高并发下会额外占用数据库连接池的连接如果事务内还有远程调用连接持有时间会变长池子小的系统可能连接耗尽。用之前要掂量好。4.2 事务失效的六个经典场景事务失效是线上问题的高发区也是Spring生态下最值得排查的一类问题。我盘点六个最常见的失效场景你对照自己的代码检查一次大概率能踩中至少一条。第一方法自调用。同一个类里方法A调用方法BB上标了Transactional但事务不生效。为什么Spring事务靠代理实现自调用走的是this.method()绕过了代理对象注解根本没被解析。解决办法是注入自身代理或者把B方法拆到另一个Bean里。第二非public方法上标注Transactional。Spring的AOP默认只拦截public方法私有方法、保护方法的注解不会生效。虽然Spring 4之后对protected也有了支持但概率非常有限最好统一只用public。第三方法内部捕获异常并吞掉。事务回滚依赖异常向上抛出触发AOP拦截如果catch了异常不重新抛出Spring以为方法正常执行完就提交了。想把某些异常吞掉不触发回滚可以用rollbackFor和noRollbackFor精细化控制而不是盲目catch。第四异常类型不匹配。Spring默认只对RuntimeException和Error回滚受检异常比如IOException默认不回滚。这就是为什么通常建议在Transactional上写rollbackFor Exception.class。第五数据库引擎不支持事务。MyISAM引擎根本不支持事务方法上注解加得再多也没用。现在基本都是InnoDB但如果你接手了老项目先查一下表引擎。第六多线程导致事务各自独立。在事务方法里开子线程去执行DB操作子线程的事务和主线程的事务不是同一个子线程的异常不会影响主线程回滚主线程的提交也不会带着子线程一起提交。这在大批量异步处理时特别容易踩而且不好排查因为日志看起来一切正常。4.3 加Transactional之前先想清楚这三个问题每次在方法上加Transactional之前我的习惯是先问自己三个问题。第一这个方法真的需要事务吗如果只是单条SQLMySQL默认就是自动提交的不需要事务包裹。第二事务里有没有网络调用如果有RPC、HTTP、消息发送一定把事务控制到最小范围。举个例子事务里调用外部接口超时5秒事务就悬着5秒不提交数据库连接一直占着锁也一直持着。并发一上来连接池耗尽、锁等待超时就是必然的。第三事务的粒度够不够小一个方法里既有核心的订单保存又有非核心的积分赠送把积分赠送也包在同一个大事务里积分服务偶尔抖动就会拖垮核心下单。正确的做法是把积分赠送拆出去用REQUIRES_NEW或者改成异步消息。这里的本质原则叫“事务中不做无关的事无关的事不进事务”。很多开发把事务当救火工具遇到多个写操作就一把梭加上注解结果数据库连接不是被耗死就是被锁等待卡死。SQL执行本身是毫秒级的真正的性能杀手是事务里嵌套的I/O等待和远程调用。5. 分布式事务从单机事务到跨服务数据一致性5.1 为什么单机事务解决不了微服务问题我把单机和分布式事务分开讲是因为很多人从单体转微服务后第一反应还是用Transactional包住所有操作结果会发现“明明所有步骤都执行成功了数据还是对不上”。原因很简单跨服务调用时每个服务有自己独立的数据库、自己的事务A服务的事务提交了B服务的事务回滚了单机事务管不到别的机器上的事情。用一个最典型的电商下单场景来拆解用户下单要同时做三件事创建订单订单服务、扣减库存库存服务、扣减余额账户服务。三个服务各自有独立数据库。如果在扣库存这一步失败了订单已经创建成功此时必须想办法把订单撤销。这个“想办法”的过程就是分布式事务要解决的。分布式事务的核心挑战叫“一致性协议”。单机事务靠undo log回滚分布式环境下没有一个全局协调者的话就无从谈回滚。业界发展出了多种方案没有银弹只有针对不同场景的权衡。选择分布式事务方案的条件我总结就三个业务对一致性的要求有多高、并发量有多大、团队能接受多大的改造和运维成本。5.2 四大分布式事务方案横向对比先给出方案清单再逐个讲适用场景。方案一致性性能复杂度适用场景2PC两阶段提交强一致低中单数据库或跨库事务且能接受XA性能TCC补偿事务最终一致中高互联网高并发业务可控性强本地消息表最终一致高低对实时性要求不高的异步场景最大努力通知最终一致高低支付结果通知、短信发送等非核心链路2PC是最经典的方案分为准备阶段和提交阶段。协调者先发Prepare给所有参与者全部准备好后发Commit任何一方Prepare失败全局回滚。实现上有基于XA协议的数据库原生支持也有Atomikos等中间件封装。但2PC的致命缺点是同步阻塞Prepare后的资源全部被锁住协调者单点故障时整个事务卡住性能上限低。生产环境直接用XA的不多金融类对强一致有硬性要求的会考虑。TCC是Try-Confirm-Cancel的简称本质是把一个业务拆成三个操作。Try阶段预留资源比如扣库存时先冻结库存数量Confirm阶段确认执行真正扣减Cancel阶段取消释放把冻结的库存解冻。TCC的优点是最终一致、性能较高、不锁数据库资源缺点是开发量爆炸每个参与的服务都要实现Try、Confirm、Cancel三套方法且要处理好幂等和空回滚。本地消息表是很容易理解和落地的方案在核心业务数据库里建一张消息表业务操作和消息写入放在同一个本地事务里。然后起一个定时任务把消息表中的消息发送到消息队列消费者幂等处理。如果消费成功更新消息状态如果失败定时任务重试。这套方案其实没什么高深技术胜在简单可靠但消息表会随着数据量增长而膨胀需要定期清理。最大努力通知方案是柔性事务里最常见的一种适用于那些“通知到了就行不用强一致”的场景。核心思路是发起方尽最大努力把结果通知给接收方接收方收到后做幂等处理。如果通知失败发起方按照一定策略间隔递增、最大次数限制持续重试。最典型的例子是支付结果回调支付平台把支付结果通知给商户系统可能失败那就隔几秒、几十秒、几分钟反复通知。这个方案基本不要求接收方在线语义最弱但实现最简单。我个人的选择经验如果是公司内部服务间高频调用又无法接受数据不一致优先考察TCC如果业务能接受秒级甚至分钟级延迟本地消息表是最稳的方案如果链路上有外部系统比如支付网关、短信平台最大努力通知是不二之选。至于Seata它是目前Java生态里把AT模式本质上是对2PC的改进借助undo log实现自动补偿真正做到开箱即用的框架后面单独说。5.3 Seata分布式事务的原理与落地Seata这个名字现在几乎成了分布式事务的代名词。它把分布式事务拆成三个核心角色TCTransaction Coordinator事务协调器、TMTransaction Manager事务管理器、RMResource Manager资源管理器。流程大致是TM向TC申请开启一个全局事务拿到全局事务ID各个服务在执行本地事务时RM把本地事务的提交/回滚结果注册到TCTM最终根据所有RM的结果向TC发起全局提交或回滚请求。这些概念听起来抽象其实对应到代码就是三个注解GlobalTransactional、GlobalLock等。Seata主推AT模式思路很巧妙它利用数据库本身的本地事务通过拦截SQL解析出修改前后的数据快照写入undo_log表。全局提交时直接提交本地事务全局回滚时根据undo_log里的前后快照反向生成补偿SQL把数据恢复到修改前。注意AT模式对SQL有约束不能支持某些复杂DDL、SELECT FOR UPDATE等且底层耗时较高。业务选择AT模式前要压测验证。还有一种TCC模式需要按前面说的方法自己实现三套接口Seata只负责协调调用。我实际用过AT模式做订单库存分布式事务改造整体体验是“接入简单、性能能接受、排障要花点心思”。问题是全局锁Global Lock会让低并发下没问题、高并发下写冲突的概率上升。如果你的系统单表TPS超过2000分布式事务瓶颈很容易出现在TC的全局锁协调上。对于超高并发场景我更推荐去掉分布式事务框架改成业务层最终一致设计用一个可靠消息闭环替代跨服务强事务。6. 高频问题排查事务场景下的真实事故与解法6.1 线上案例一库存扣成负数问题出在哪先说一个我亲自处理过的线上事故。某电商平台做秒杀活动活动刚开始订单量和库存扣减都在暴增监控突然报警商品库存出现负数。当时第一反应是并发问题以为代码里没加锁但查完代码发现扣库存的SQL确实有库存大于0的条件理论上不该扣成负数。最后定位到问题根源是事务隔离级别被从RR改成了RC。在RC级别下两个事务同时执行UPDATE stock SET count count - 1 WHERE id 1 AND count 0A事务先拿到行锁执行扣减并提交B事务此时才能进入但它进入时读取的count是A提交后的新值所以条件下一次判断应该还是成立。等等这样理论上也不会扣成负数那问题出在哪实际上事故的原因比这个更隐蔽。检查binlog后发现扣库存的逻辑大概是先SELECT库存是否大于0再执行UPDATE扣减。在RC级别下SELECT是一个独立的快照读拿到一个旧值然后事务再去执行UPDATE这个间隔里另一个事务已经把库存扣成了0但第一个事务的SELECT结果还是0之前的旧值它认为库存充足继续执行UPDATE于是把库存扣成了负数。RR级别下SELECT生成的ReadView在整个事务内复用虽然也同样存在读到旧值可能但配合锁定读SELECT ... FOR UPDATE和间隙锁可以有效规避。而代码里SELECT没有任何锁所以问题在RC下被放大了。这个案例的教训有两层。第一业务上凡是“先查后改”的地方查询必须是当前读——要么用SELECT ... FOR UPDATE要么干脆把判断条件写进UPDATE的WHERE里让数据库在加锁状态下做原子判断。第二不要轻易把生产环境隔离级别从RR改成RC除非你全面评估过业务里所有“先查后改”的场景否则就是在埋雷。从这里引出一个高频面试题如何设计一个安全的库存扣减方案标准答案是SQL原子操作UPDATE stock SET count count - #{num} WHERE id ? AND count #{num}通过受影响行数判断是否扣减成功不需要显式加锁。如果还需要先查询展示库存再用更新条件校验必须在同一个事务里配合使用SELECT ... FOR UPDATE。6.2 线上案例二长事务拖垮数据库第二个案例来自一次数据库性能告警。某后台管理系统的导出功能每次点击导出都要把全量订单数据查一遍耗时几十秒。后来发现这个导出操作被包在一个事务里事务里先查询大量数据、再做一些数据组装和文件写入导致事务长时间持有快照和锁资源。更麻烦的是随着事务时间拉长undo log不断膨胀数据库的磁盘空间和内存压力都大幅上升。排查时执行了SELECT * FROM information_schema.innodb_trx发现一条事务已经运行了60多秒。顺着应用日志找代码看到导出方法上的Transactional赫然在目。查数据为什么要开事务没有任何写操作用一个不支持事务的只读连接就能解决问题。而且事务里还带着文件I/O操作是典型的长事务反模式。这里说一个实用的排查技巧MySQL的sys.schema_unused_indexes和performance_schema.events_statements_history_long可以帮你定位长事务期间执行过的SQL。information_schema.innodb_trx可以查到当前所有未提交事务的持续时间、操作的行数、锁等待情况。出现长事务的时候第一时间列出所有事务结合应用日志找到源头先终止异常事务止血再改代码。长事务的危害我总结成三条第一锁持有时间长容易造成大量锁等待和死锁第二MVCC的undo log无法及时清理导致回滚段膨胀甚至引发磁盘满和性能雪崩第三主从复制延迟加大因为从库要串行回放该事务产生的binlog。凡是超过几秒的事务都值得警惕。6.3 事务相关面试题速查表我梳理了一份面试常见问题和要点背下来不算本事能用自己的话讲明白才算真懂。问题回答要点ACID里最难实现的是哪个隔离性需要锁和MVCC配合MySQL默认隔离级别是什么RR靠MVCC间隙锁规避幻读RR和RC怎么选RC并发高、间隙锁少但需接受不可重复读RR一致性更好一条UPDATE的执行流程加锁→写undo→改内存→写redo→提交时刷redo脏读、不可重复读、幻读区别脏读是读未提交不可重复读是两次读值不同幻读是两次读行数不同MVCC中的ReadView什么时候生成RR下第一次快照读时生成RC下每次快照读重新生成什么情况会锁表UPDATE没走索引、全表扫描、DDL操作Spring事务什么时候失效自调用、非public、吞异常、异常类型不匹配、多线程分布式事务怎么选2PC强一致但性能差TCC开发量高本地消息表简单可靠最大努力通知最轻量6.4 聊聊我对事务的长期理解和建议说几个踩坑踩出来的体会。第一事务是数据库的“能力”但不是业务的“银弹”。别一遇到多个写操作就加事务先分析到底需不需要原子性、一致性。很多场景用补偿、重试、幂等设计就能解决非要用分布式事务反而把系统搞复杂了。第二事务的粒度一定要小。事务里千万不能有网络调用、文件操作、消息发送这些不可控因素。我见过一个项目事务里调了一个第三方接口对方响应特别慢事务长时间不提交最终把数据库线程池打满整个系统挂了。事后整改就是一句话把远程调用挪出事务。第三遇到事务相关的问题先看日志、再看监控、最后看代码。MySQL的错误日志、慢查询日志、performance_schema、information_schema是排查事务问题的四大抓手。光盯着代码猜效率太低。第四MySQL官方文档在隔离级别、行锁、MVCC这几章写得很清楚但读起来枯燥。我的建议是配合线上问题来读遇到一个案例去查一章印象会特别深。事务这个主题面试考的是广度工作考的是深度。能把这篇文章里提到的每个点都亲手复现一遍你在这个知识点上的理解就超过大部分人了。