ARTICLE DETAIL

资讯详情

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

MySQL事务与锁机制:从隔离级别到MVCC与死锁排查

MySQL事务与锁机制:从隔离级别到MVCC与死锁排查 1. 从一条卡死的SQL说起为什么你需要真正理解事务与锁先讲个我实际碰到的事。前几年在维护一个订单系统某天线上突然出现大量超时后台监控显示某个UPDATE语句的等待时间飙到了几十秒。当时第一反应是慢查询结果一查执行计划索引齐全、扫描行数也就几百条。后来才发现根本不是查询慢而是这条UPDATE一直在等待一个行锁——前面有个事务开了没提交把那一行的锁攥在手里不放。当时DBA给的解释就是一句锁等待可这句锁等待背后牵扯出来的一整套东西——事务隔离级别、MVCC快照读、当前读与锁的配合、死锁检测——才是真正让我把MySQL底层机制重新啃了一遍的契机。很多人写了几年代码SELECT、INSERT、UPDATE用得贼溜但遇到并发更新同一行数据、统计报表和线上写入互相干扰、死锁报错Deadlock found when trying to get lock这类问题还是只能靠猜。说白了就是没把事务和锁这两条线串起来理解。这篇文章我想做的事就是把这套东西从头到尾捋一遍隔离级别到底隔离了什么InnoDB的行锁是怎么加在索引上的MVCC是怎么做到读不阻塞写、写不阻塞读的以及真正线上出问题时你该怎么一步步排查、怎么设计索引和事务来避免锁冲突。适合谁看如果你是刚接触MySQL不久、想搞懂InnoDB事务原理的初级开发或者你写了好几年业务代码但一遇到并发场景就发怵再或者你即将面试需要把事务四大特性、隔离级别、MVCC、锁分类这些点讲出深度——这篇文章都能给你一条清晰的主线。我不打算写成一本文档手册更像是我自己反复踩坑、反复看源码和官方文档之后沉淀下来的一套理解路径。注意下文所有内容默认讨论InnoDB存储引擎下、MySQL 5.7及8.0版本的行为。MyISAM那种表级锁、不支持事务的老古董不在讨论范围内。2. 一图看懂隔离级别四种级别到底隔离了什么事务的四大特性ACID——原子性、一致性、隔离性、持久性——大部分人都能背出来但真正难的是隔离性落到数据库实现上它演化出了四个隔离级别对应解决三个并发问题。很多人面试时能说出读未提交、读已提交、可重复读、串行化也能说出脏读、不可重复读、幻读但一旦问MySQL默认的RR级别下到底还有没有幻读就哑火了。2.1 三个并发问题脏读、不可重复读、幻读脏读事务A修改了一条数据但还没提交事务B就读到了这个未提交的修改。如果事务A回滚了B读到的那条数据就成了不存在过的数据。这个最好理解也最不能容忍。不可重复读事务A先查了一条记录然后事务B修改了这条记录并提交事务A再次查询时发现值变了。问题在于同一条记录、两次读、结果不一致。幻读事务A按某个条件查出了一批记录事务B插入了一条满足条件的新记录并提交事务A再次用同样条件查询时多出了一条凭空出现的记录。和不可重复读的区别在于幻读针对的是记录集合的变化不可重复读针对的是单条记录的值变化。这三个问题的严重程度是递减的所以隔离级别也顺着这个梯度设计。2.2 四种隔离级别解决了什么、没解决什么隔离级别脏读不可重复读幻读实现机制读未提交READ UNCOMMITTED可能可能可能不加锁直接读最新版本读已提交READ COMMITTED不可能可能可能每条SELECT都生成一个快照 当前读加行锁可重复读REPEATABLE READ不可能不可能可能InnoDB通过间隙锁解决事务开始后第一条SELECT生成快照快照读一直复用串行化SERIALIZABLE不可能不可能不可能所有SELECT自动加共享锁读写互相排斥读未提交基本没人用因为它拿正确性换并发代价太大。串行化则反过来拿并发换正确性也没几个人敢在生产环境用。真正有讨论价值的是读已提交和可重复读。这里有个很多人误解的点读已提交下不可重复读依然是可能的因为每次SELECT都重新生成一个快照。这个快照概念先放在这后面讲MVCC时会展开。可重复读下理论上的幻读问题依然存在但是InnoDB在RR级别引入了间隙锁Gap Lock和临键锁Next-Key Lock让它把大部分幻读场景也堵死了。这也是为什么MySQL敢把RR作为默认隔离级别——别的数据库默认用RC因为RR加锁成本高、死锁概率大但InnoDB用间隙锁把这个问题基本压住了。2.3 MySQL的默认级别与一条重要提醒MySQL 8.0之前默认隔离级别是REPEATABLE-READ8.0仍然是。你可以在会话里查一下SELECT transaction_isolation; -- 或者老版本用 SELECT tx_isolation;很多从Oracle、PostgreSQL转过来的人会习惯性把它改成READ-COMMITTED因为RR级别下如果SQL没走到正确的索引间隙锁可能把一大段范围锁住并发写入直接被堵死。这种改动本身没问题但要意识到改成RC之后可重复读的保护就没了你的事务内多次查询可能读到不同的数据业务逻辑必须接受这一点。我自己的一条经验是先在逻辑上确定你的业务到底需要哪个级别再动配置而不是为了模仿别的数据库顺手改掉。大部分互联网业务的高并发写入场景RC配合乐观锁或应用层补偿机制已经够用了但有些报表查询需要多次读同一批数据保持一致RR反而更合适。3. 锁机制全景拆解InnoDB到底在锁什么东西InnoDB的锁体系是这套知识里最绕的部分。很多人一上来就背共享锁、排他锁、意向锁、间隙锁、临键锁、自增锁背完就忘。我建议换个思路先明确锁的粒度和锁的模式再看锁是怎么落在索引上的。3.1 按粒度划分行锁、表锁、意向锁行锁InnoDB默认使用行级锁锁加在索引记录上而非物理行上。开销大、并发性好这也是InnoDB比MyISAM更适合高并发写的原因。表锁锁住整张表开销小但并发能力极差。InnoDB在两种情况下会用到表锁LOCK TABLES语句显式加锁或者ALTER TABLE等DDL操作。意向锁Intention Lock它本身不锁任何实际资源是一种**“预告机制”**——事务想在行上加锁之前必须先给表打上意向锁标记。这样后来者想加表锁时能快速判断“有没有事务正在这个表的行上持有锁”不用一行一行扫描检查。这就是为什么加表锁的开销能控制住。意向锁也分两种意向共享锁IS和意向排他锁IX。注意意向锁之间互相兼容事务A持有行级排他锁时事务B照样可以给表加意向排他锁——反正都是“预告”不是真的锁表。只有真正的LOCK TABLES ... WRITE这种表级锁才会和意向锁冲突。3.2 按模式划分共享锁与排他锁共享锁S锁SELECT ... LOCK IN SHARE MODE多个事务可以同时持有同一行的S锁只读不写互相不干扰。排他锁X锁SELECT ... FOR UPDATE、UPDATE、DELETE、INSERT一旦某事务持有X锁其他事务的S锁和X锁都进不来直到锁释放。S锁和X锁的兼容矩阵非常简单当前锁类型请求S锁请求X锁S锁兼容冲突X锁冲突冲突这个矩阵值得刻在脑子里。死锁的很多“事故现场”就是两个事务都在先S后X的升级路径上互卡——各自持有S锁都在等对方的X锁谁也得不到只能靠死锁检测打破。3.3 从记录锁到临键锁锁绝不只锁一行这是InnoDB锁体系里最有技术含量的一块。先说三个概念再解释关联记录锁Record Lock锁住索引上的一条记录。间隙锁Gap Lock锁住索引记录之间的间隙不允许其他事务在这个间隙里插入新记录。注意它是一个范围概念不是锁在某条记录上。临键锁Next-Key Lock记录锁和间隙锁的组合锁住“左开右闭”的区间——即某条索引记录及其前面的间隙。RR级别下InnoDB进行范围查询或当前读时默认用的就是临键锁。比如SELECT * FROM orders WHERE id BETWEEN 10 AND 20 FOR UPDATE;如果表里id分别是5、10、15、20、25那么临键锁会锁住(5,10]、(10,15]、(15,20]以及20之后的间隙(20,25)如果25不是边界记录锁范围还会往后延伸。这意味着即使id18这条记录根本不存在你也插不进去——这就是间隙锁把幻觉的入口堵死的原理。但间隙锁有一个致命弱点它只在RR及以上级别生效。如果你把隔离级别切到RC间隙锁直接失效幻读就真的可能发生了。另一个容易踩的坑是无索引条件的当前读——如果WHERE条件没有索引可走InnoDB会扫描聚簇索引的整棵树相当于把整个表的间隙都锁住线上立刻大面积堵塞。这个问题我在第4章会细讲。3.4 自增锁一个容易被忽略的隐性锁AUTO_INCREMENT在插入时依赖自增锁AUTO-INC Lock。它是一种特殊的表级锁专门为了分配递增ID而存在。早期MySQL版本里自增锁是要在插入语句执行完才释放的并发插入性能很差后来变成了轻量级互斥锁只锁分配ID的瞬间拿完ID立即释放。8.0里innodb_autoinc_lock_mode默认值是2也就是“交错分配”并发能力最强但有个代价——批量插入时生成的ID可能不连续。如果你的业务里ID必须严格连续就得降级模式或者别用自增列。4. 当前读与快照读MVCC是怎么和大家印象中的锁配合的很多初学者有个困惑既然InnoDB有这么多锁那为什么普通SELECT从来没被阻塞过这正是MVCC多版本并发控制的功劳。锁和MVCC是InnoDB并发控制的两条腿MVCC管的是无锁的“快照读”锁管的是“当前读”。4.1 两种读取方式行为完全不同快照读Snapshot Read普通SELECT就是快照读它不加任何锁读取的是事务开始时的“版本快照”。因为不带锁所以永远不会阻塞别人也永远不会被别人阻塞。当前读Current ReadSELECT ... FOR UPDATE、SELECT ... LOCK IN SHARE MODE、UPDATE、DELETE、INSERT都算当前读。它们读到的必须是最新已提交版本所以必须加锁防止并发修改。这个区分是理解InnoDB并发模型的核心。我见过不少人把“MVCC”和“锁”当成两条平行的技术分别学结果遇到“为什么UPDATE会等锁、SELECT不会等”就解释不清。实际上Update语句内部是先做一次当前读把行锁住再修改数据——所以UPDATE等待锁本质是等当前读拿到行锁。4.2 undo log与隐藏列快照是怎么来的MVCC的实现靠三样东西数据行上的隐藏列、undo log、read view。数据行上有两个关键隐藏列trx_id最近一次修改/插入这行数据的事务ID。roll_pointer指向undo log中的前一个版本。当你更新一行数据时InnoDB并不会覆盖旧值而是先把旧值写入undo log生成一个新版本的行并把新行的roll_pointer指向旧版本。于是同一行数据在逻辑上形成了一条版本链每个版本记录了它所属的事务ID。读操作发起时InnoDB会生成一个read view里面装着“我这个事务开始瞬间仍在活跃未提交的所有事务ID列表”然后沿着版本链从新到旧找只要找到一个版本的trx_id不在活跃列表里、且比自己的事务ID小就说明这个版本是已提交且可见的——返回它。这个机制非常优雅写入时只维护版本链不阻塞读读取时只判断可见性不阻塞写。这就是“读写不互斥”的底层原理。4.3 可重复读和读已提交在MVCC上的区别两种隔离级别最大的差别在于read view的生成时机读已提交RC事务内每条SQL语句执行时都生成一个新的read view。所以同一个事务里两次SELECT可能看到不同的版本——这就是不可重复读的来源。可重复读RR事务内第一条SELECT执行时生成read view之后整个事务内都复用这个视图。所以之后无论查多少次看到的都是一致的快照——这就是“可重复读”的实现原理。记住这个区别面试被问到“RR怎么实现可重复读”时答案就不是背概念而是讲清楚“read view只生成一次”这个关键点。4.4 快照读也救不了先读后写的并发错乱MVCC虽然好用但它有个典型盲区。我举个例子一个库存扣减逻辑第一行查当前库存是10第二行准备UPDATE把它改成9。如果两个并发事务都先做了快照读各自都读到10然后各自执行UPDATE ... WHERE stock 10。其中一个会成功另一个会等锁或更新0行——因为你UPDATE时用的是当前读实际读取的是最新版本所以第二个事务并不会拿旧快照的10去做减法而是会基于当前最新值操作。但如果你写的是“先SELECT出来程序里算好新值再UPDATE set stock新值”没有把“库存等于旧值”作为UPDATE的WHERE条件那么两个事务都可能读到10、计算出9然后都执行UPDATE set stock9结果库存从10变成了9而不是8——丢了一次扣减。这就是典型的“并发丢失更新”问题。解决办法有三种用UPDATE ... SET stock stock - 1这种原子操作让数据库自己处理加减。用SELECT ... FOR UPDATE做当前读锁定行后再在应用层计算并更新。用乐观锁UPDATE ... SET stock 9 WHERE stock 10如果影响行数为0就重试。这三种方案各有利弊但核心都指向一件事不要让“读到的旧值”和“更新的新值”之间出现空隙要么用当前读把空隙锁死要么用条件更新让空隙暴露出来。5. 死锁与线上事故从现象到根因的完整排查链路死锁问题是最能拉开“靠谱DBA/后端”和“CRUD程序员”差距的场景。很多人遇到死锁的第一反应是“报错重试就完了”但如果你不理解死锁怎么产生、为什么偏偏出现在某个业务里那么重试十次可能还是死锁。5.1 一次真实的死锁现场还原我曾处理过一个后台任务和前台下单并发跑的场景后台任务做批量状态更新前台用户在下单。死锁报错信息大概是ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction当时业务逻辑是这样的后台任务先UPDATE订单表批量把状态改为“处理中”然后更新关联的库存表前台下单则先UPDATE库存表扣减再UPDATE订单表更新状态。两个事务各拿了一把锁然后都在等对方的那把锁——锁顺序相反就这么互相卡死了。这就是死锁产生的根本条件多个事务以不同顺序获取同一组资源。排查链路我建议按下面几步走第一步查看死锁日志。执行SHOW ENGINE INNODB STATUS重点看LATEST DETECTED DEADLOCK段落。里面会清清楚楚打出两个事务各自持有和等待的锁以及对应的SQL语句。第二步定位两条SQL的加锁顺序。事务A持有“订单表某行”的锁、等待“库存表某行”事务B持有“库存表某行”、等待“订单表某行”——锁定顺序反了。第三步找到业务代码里两条SQL的调用顺序统一成全局相同的加锁顺序。比如都先更新订单表再更新库存表死锁概率直接归零。第四步如果顺序没法统一那就引入超时重试机制。MySQL默认innodb_lock_wait_timeout是50秒死锁检测到之后会立刻回滚其中一个事务并释放锁应用层捕获到1213错误后做重试即可。5.2 为什么死锁检测不是银弹InnoDB默认开启死锁检测它会维护一个等待图每次加锁时检查是否存在闭环发现死锁就回滚代价最小的那个事务。这套机制在并发量不高的场景下很可靠但并发量极高、加锁频繁时等待图检测本身会成为性能瓶颈——每个加锁操作都要做一次图检测开销不小。如果你确认业务里死锁几乎不可能发生或者为了极限性能可以关掉死锁检测innodb_deadlock_detectOFF让锁等待超时机制兜底。但这是一把双刃剑关了检测后死锁不会被主动打破只能靠50秒超时释放体验极差。所以除非有充分的性能压测数据支撑不要轻易关死锁检测。5.3 最容易引锁竞争的SQL写法根据我的经验下面几类SQL在并发环境下特别容易引发锁问题逐个排雷SELECT ... FOR UPDATE 范围过大。如果条件无法命中有效索引临键锁会锁住整个扫描范围。线上表现就是一个FOR UPDATE下去半个表都写不了。解法是确保条件走索引缩小锁范围。UPDATE 条件无索引。同理更新时没有索引可走InnoDB只能扫全表行锁退化成“全表范围的锁”。这是高并发写入场景的头号杀手。外键约束的隐式锁。子表插入或更新时如果父表有外键约束InnoDB会对父表对应索引记录加S锁。两个子表事务反向操作父表不同行时容易形成循环等待。解法是认真评估外键的必要性——很多互联网公司干脆禁用外键把一致性交给应用层保证。大事务长时间持锁。锁本身没错错的是“持有时间过长”。批量更新几万行数据的事务会在整个执行期间持有大量行锁。并发高的时候后面所有相关操作都在排队。解法是分批更新把大事务拆成小事务。RR级别的间隙锁范围被放大。如果查询条件用了非唯一索引RR级别间隙锁可能锁住非唯一索引的整个范围。线上常见的坑是按非唯一字段做FOR UPDATE结果该字段重复值多锁住的记录数远超预期。6. 索引与锁的恩怨为什么让锁更小本质上是个索引问题很多人理解锁还停留在“锁住一行记录”的层面但InnoDB真正的细节是锁加在索引上不加在“行”的抽象概念上。这意味着一个SQL能不能高效、小范围地加锁完全取决于它能不能通过索引快速定位到目标记录。6.1 锁与索引的关系两条影响深远的推论如果UPDATE走的是主键索引锁会精确加在这一条聚簇索引记录上范围极小。如果UPDATE走的是二级索引InnoDB锁会加在对应的二级索引记录上同时还会在回表查到的聚簇索引记录上加锁。如果UPDATE压根没走索引InnoDB只能全表扫描所有聚簇索引记录对扫描到的每一条记录都加锁——表面是行锁实际效果等于表锁。这个特性直接解释了为什么加索引能提升并发写入性能。因为加索引的本质是缩小了锁的粒度让数据库能用更小的代价完成锁定位。很多人以为加索引只是加速查询其实对写入并发的影响同样巨大。6.2 结合实操用EXPLAIN预判锁范围排查锁问题时我最推荐的习惯是写完一条UPDATE或DELETE先跑一遍EXPLAIN再上线。EXPLAIN SELECT * FROM orders WHERE status paid AND create_time 2024-01-01 FOR UPDATE;关键看type和key两列typeconst或eq_ref或者ref说明走了索引锁范围可控。typeALL说明全表扫描锁范围不可控这条语句上线就是事故。注意EXPLAIN默认分析的是SELECT如果你要分析的是UPDATE/DELETE用EXPLAIN直接跟UPDATE语句也是可以的MySQL会转换为等价SELECT分析。生产环境最好在测试库先跑一遍确认走索引、确认扫描行数在预期内再放线上。6.3 唯一索引vs普通索引的加锁差异同样一条SELECT ... WHERE no A1001 FOR UPDATE如果no是唯一索引InnoDB知道最多只有一条记录直接加记录锁只锁这一行。如果no是普通索引InnoDB不知道是否还有重复值必须用临键锁把noA1001所在的区间都锁住包括这个值前面的间隙和后面的间隙——为的是防止其他事务插入新的A1001记录从而保证RR级别下不会出现幻读。这个差异解释了为什么唯一索引在并发写场景下更友好。能在业务上保证唯一的字段如订单号、流水号设计索引时尽量用唯一索引不仅查询语义更严谨锁的粒度也更小。6.4 一个我踩过的索引坑联合索引的“头字段”决定锁范围有一个线上库存表核心查询条件是shop_id product_id我建了联合索引(shop_id, product_id)。某天有一批SQL只传了product_id没传shop_id结果联合索引的“最左前缀”原则生效不了SQL直接全表扫描更新时的锁范围瞬间铺满整张表。那一次我花了整整一个下午才定位到根因——业务代码为了兼容某些特殊查询把条件顺序写乱了。之后我总结了一条铁律凡是UPDATE、DELETE、FOR UPDATE这类当前读必须确认查询条件能命中索引且命中的是“预期的那个索引”。这不是一个DBA的洁癖而是并发安全的底线。7. 事务实践从ACID是背的到事务边界是设计出来的锁讲完了回到事务本身。很多人在代码里用事务就是方法上加一个Transactional注解从没想过“这个事务到底把哪些操作包在了一起、持锁多久、锁了多少行”。这是很多线上锁问题的根源之一。7.1 事务边界设计能短则短事务越短锁的持有时间越短并发能力越强。这是一个朴素但极容易被忽视的原则。我来说说实际开发中怎么落实不要在事务里做远程调用。假设你在事务里调用了外部HTTP接口这个接口可能要等3秒才返回那么事务的锁就白白持有了3秒。这3秒里任何想更新同一行数据的请求都在排队。正确的做法是把远程调用放到事务提交之后或者放到事务外的消息队列里异步处理。不要在事务里做耗时计算。有人喜欢在事务内生成复杂报表、做大量内存计算或者把大JSON往Redis里写。这些操作跟数据库一致性无关放事务里只会白白延长锁时间。把计算挪到事务外面事务提交后再处理纯计算逻辑。不要在事务里批量处理过多行。一次性UPDATE一万行和一千行持锁数量差一个量级。尤其当你的批量操作逻辑复杂时每行还可能触发其他SQL锁的雪球会越滚越大。务实的做法是拆批比如每500行提交一次。虽然整体耗时可能变长但锁冲突的概率会大幅下降。7.2 事务传播行为与锁的关联Transactional的传播行为在代码层面直接决定哪些SQL被包进同一个事务——也就决定哪些锁被“共享”到同一批。REQUIRED默认意味着“有事务就加入没有就新建”如果你在一个大方法里调用另一个带Transactional(propagation Propagation.REQUIRED)的服务方法这两个方法会用同一个事务所有锁在最早的事务边界内统一管理。而REQUIRES_NEW则会让被调方法开启一个全新事务这个新事务的锁和外部事务的锁互不相干但如果两个事务操作了同一行数据就有可能在提交时互相等待甚至死锁。我在实际开发里见过这样的案例外层事务更新订单内层REQUIRES_NEW事务更新同一订单的日志表结果内层提交时恰逢外层还在持锁日志更新一直等不到锁直接超时。所以设计事务边界时必须想清楚哪些操作必须同生共死哪些操作可以独立提交。不是所有代码都适合包进一个大事务里。7.3 事务里“先查后改”最容易翻车先SELECT再UPDATE的经典写法在并发下非常容易出问题。前面4.4已经提过库存扣减的例子这里再补充一个订单状态的场景两个请求同时查同一订单的状态都是“待支付”都准备改成“已支付”。如果第二个请求的UPDATE没有带“当前状态待支付”的条件那么两个请求都会成功更新——尽管业务上第二个请求根本不该支付成功。加锁的写法是SELECT * FROM orders WHERE id 123 AND status pending FOR UPDATE;这个FOR UPDATE会把订单行锁住第二个请求会等第一个请求提交后才读到最新状态从而判断这笔订单已经支付成功、不再更新。“当前读重新判断”是事务内先查后改的可靠姿势比“无脑UPDATE带条件”更稳妥。8. 高频面试题拆解怎样把锁机制讲出深度把原理和实战都铺完了最后聊聊面试。MySQL锁机制和事务几乎是Java后端面试的必考点但大多数候选人只能背概念讲不出“为什么”。这里我挑几个高频问题给出我认为最能体现深度的答法思路。8.1 MySQL默认隔离级别是什么为什么选RR答InnoDB默认是REPEATABLE-READ。这其实是一个历史选择因为在MySQL主从复制架构诞生的年代binlog日志格式是STATEMENT记录SQL语句如果使用READ-COMMITTED事务内的两次查询可能结果不一致从库执行同样的SQL可能会和主库产生数据不一致。RR级别配合间隙锁、临键锁在很大程度上规避了这个问题。后来binlog有了ROW格式从库按实际数据变更同步RC的隐患变小了很多团队才开始切换到RC。这个回答能同时体现出你对“事务原理”和“主从复制机制”的理解。8.2 MVCC和普通锁的区别是什么答MVCC是“无锁读”机制核心是版本链和read view它把读操作从锁竞争里解放了出来而普通锁是“当前读”机制核心是保证读写冲突时的正确性。两者不是替代关系是分工关系——快照读读历史版本当前读写最新版本组合起来才构成InnoDB完整的并发控制模型。这个回答如果只背到MVCC的三个组件就落了下乘要讲到和锁的分工才算有深度。8.3 可重复读下没有幻读吗答理论上有但InnoDB用临键锁解决了大部分情况。RR 临键锁能阻止其他事务在锁定的间隙插入新记录所以SELECT ... FOR UPDATE这类当前读不会出现幻读。但如果是普通快照读且新插入的数据在快照生成之后才提交理论上还是可能“看到”新数据——当然由于复用read view一般也不会看到。所以严格说InnoDB在RR下“基本没有幻读”但不能说绝对没有尤其是锁条件和插入条件错位时。能说出“基本”和“绝对”的差异说明你真的理解边界。8.4 死锁的四个必要条件是什么答互斥、持有并等待、不可剥夺、循环等待。InnoDB死锁一定是循环等待解决思路就是打破循环等待——最实用的手段是统一加锁顺序、缩小事务规模、减少持有锁的时间。还有一个加分点是提到SHOW ENGINE INNODB STATUS怎么看死锁日志LATEST DETECTED DEADLOCK里能看到两个事务各自的持锁和等待情况这是定位死锁的第一手资料。9. 写在最后一条实践路径从看懂到用对MySQL的锁机制和事务体系是我觉得数据库知识里最值得花时间啃的一块。原因很简单它几乎决定了你在高并发场景下能不能写出安全的代码。你不需要记住每一种锁的所有细节但你必须建立几个核心判断——这条SQL是快照读还是当前读、这个更新能不能走索引、这个事务要持锁多久、这个批量操作会不会把锁范围放大到不可控。我个人实际操作中的一条建议建立一个“并发安全检查清单”。每次写完一条涉及更新、删除、FOR UPDATE的SQL都问自己四件事这条SQL能命中索引吗用EXPLAIN确认它加的是行锁还是范围锁走唯一索引还是普通索引这个事务会持有锁多久里面有没有远程调用、耗时计算两个并发事务会不会以相反顺序获取同一组锁检查事务内SQL顺序这四条都理清了线上锁问题的概率会低一大截。至于死锁日志怎么看、SHOW ENGINE INNODB STATUS怎么定位、锁超时参数怎么调这些是在遇到问题时的“救命技能”但更值钱的是在代码上线前就把锁的边界想清楚——毕竟最好的事故处理是让事故根本不要发生。
返回列表