摘要事务与锁是 MySQL 并发控制的两大基石。本文从 ACID 四大特性出发深入讲解 InnoDB 的 MVCC 多版本并发控制机制、四种隔离级别下的并发问题、七种锁类型从表锁到行锁、间隙锁、Next-Key 锁以及死锁的产生原因与排查方法。全文结合原理图和实战案例帮你彻底搞懂 MySQL 并发控制的核心逻辑。一、事务的 ACID 四大特性事务是数据库操作的基本单位ACID 是事务的四个核心特性特性英文含义InnoDB 实现机制原子性Atomicity事务中的所有操作要么全部成功要么全部回滚undo log回滚日志一致性Consistency事务执行前后数据库从一个一致状态变为另一个一致状态约束检查 触发器 存储过程隔离性Isolation并发事务之间相互隔离互不干扰MVCC锁机制持久性Durability事务一旦提交数据永久保存redo log重做日志 刷盘机制核心理解原子性靠 undo log 实现事务回滚时根据 undo log 将数据恢复到事务开始前的状态持久性靠 redo log 实现事务提交时先将修改写入 redo log顺序 IO再异步刷盘到数据文件隔离性是并发控制的核心由 MVCC 和锁共同实现二、事务隔离级别与并发问题2.1 四种隔离级别隔离级别脏读不可重复读幻读MySQL 默认READ UNCOMMITTED可能可能可能否READ COMMITTED不可能可能可能否Oracle 默认REPEATABLE READ不可能不可能可能InnoDB 解决是SERIALIZABLE不可能不可能不可能否并发问题定义脏读Dirty Read事务 A 读取了事务 B 未提交的数据B 回滚后 A 读到的就是脏数据不可重复读Non-repeatable Read事务 A 两次读取同一行数据中间事务 B 修改并提交导致 A 两次结果不一致幻读Phantom Read事务 A 两次执行相同条件的查询中间事务 B 插入或删除了符合条件的行导致 A 两次结果集行数不同2.2 为什么 MySQL 默认用 REPEATABLE READInnoDB 的MVCC Next-Key 锁在 REPEATABLE READ 级别下通过快照读避免了不可重复读通过间隙锁避免了幻读大部分场景。因此 MySQL 的 RR 级别实际上已经解决了幻读问题这是与其他数据库不同的地方。-- 查看当前隔离级别SELECTtransaction_isolation;-- 设置会话隔离级别SETSESSIONTRANSACTIONISOLATIONLEVELREADCOMMITTED;-- 设置全局隔离级别需重启生效SETGLOBALTRANSACTIONISOLATIONLEVELREPEATABLEREAD;三、MVCC多版本并发控制机制3.1 MVCC 的核心思想MVCCMulti-Version Concurrency Control是 InnoDB 实现高并发读写的核心机制。其基本思想是写操作不阻塞读操作读操作不阻塞写操作。实现原理每行数据除了实际数据外还隐藏了三个系统字段DB_TRX_ID最后修改该行的事务 IDDB_ROLL_PTR回滚指针指向 undo log 中的历史版本DB_ROW_ID隐藏主键如果没有显式主键每次修改数据时不直接覆盖原数据而是生成一个新的数据版本旧版本通过 undo log 链保存读操作根据事务的 Read View一致性视图判断应该读取哪个版本的数据3.2 Read View 的生成时机Read View 是事务执行快照读时生成的一致性视图决定了事务能看到哪些版本的数据。隔离级别Read View 生成时机效果READ COMMITTED每次 SELECT 都生成新的 Read View能看到其他事务已提交的最新数据REPEATABLE READ事务第一次 SELECT 时生成之后复用整个事务期间看到的数据保持一致Read View 包含四个关键字段creator_trx_id创建该视图的事务 IDm_ids生成视图时所有活跃未提交事务的 ID 列表min_trx_idm_ids 中的最小值max_trx_id生成视图时系统分配的下一个事务 ID即当前最大事务 ID 1可见性判断规则如果数据行的DB_TRX_IDcreator_trx_id自己修改的可见如果DB_TRX_IDmin_trx_id在视图生成前已提交可见如果DB_TRX_IDmax_trx_id在视图生成后才开始不可见如果min_trx_idDB_TRX_IDmax_trx_id检查是否在 m_ids 中在则不可见未提交不在则可见已提交3.3 当前读 vs 快照读读取方式实现机制SQL 示例是否加锁快照读Snapshot ReadMVCC Read View普通 SELECT不加锁当前读Current Read读取最新版本必须加锁SELECT … FOR UPDATE / LOCK IN SHARE MODE / UPDATE / DELETE / INSERT加锁关键理解普通 SELECT 是快照读利用 MVCC 实现不加锁的并发读SELECT ... FOR UPDATE和 DML 操作是当前读必须读取最新数据并加锁在 RR 级别下快照读解决了不可重复读但当前读仍可能遇到幻读需要间隙锁解决四、InnoDB 锁机制详解4.1 锁的分类体系InnoDB 的锁可以从两个维度分类按粒度分锁类型锁定范围开销并发度使用场景表锁Table Lock整张表小低DDL 操作、MyISAM行锁Row Lock单行记录大高DML 操作、InnoDB 默认页锁Page Lock一页数据中中BDB 存储引擎按功能分锁类型英文互斥关系说明共享锁Shared Lock (S)S-S 兼容S-X 互斥读锁允许多个事务同时读排他锁Exclusive Lock (X)X-X 互斥X-S 互斥写锁独占访问意向共享锁Intention Shared Lock (IS)IS-IS 兼容事务有意向对表中某些行加 S 锁意向排他锁Intention Exclusive Lock (IX)IX-IX 兼容事务有意向对表中某些行加 X 锁意向锁的作用意向锁是表级锁用于协调表锁和行锁的冲突。事务加行锁前必须先加对应的意向锁。这样其他事务想加表锁时只需检查意向锁即可无需遍历所有行锁。4.2 行锁的三种类型InnoDB 的行锁不是简单的锁一行而是根据操作类型分为三种1. 记录锁Record Lock锁定索引中的一条具体记录。-- 对 id5 的记录加排他锁SELECT*FROMuserWHEREid5FORUPDATE;-- 对 id5 的记录加共享锁SELECT*FROMuserWHEREid5LOCKINSHAREMODE;2. 间隙锁Gap Lock锁定索引记录之间的间隙防止其他事务在间隙中插入数据。-- 假设索引列 age 已有值: 10, 20, 30-- 以下查询会对 (20, 30) 之间的间隙加锁SELECT*FROMuserWHEREage25FORUPDATE;-- 即使 age25 不存在也会锁住 (20, 30) 的间隙间隙锁的特点只在REPEATABLE READ级别下生效READ COMMITTED 下禁用间隙锁之间不互斥事务 A 和 B 可以同时持有同一间隙的间隙锁间隙锁与插入意向锁互斥持有间隙锁时其他事务不能在该间隙插入数据3. 临键锁Next-Key Lock记录锁 间隙锁的组合锁定一条记录及其前面的间隙。-- 假设索引列 age 已有值: 10, 20, 30-- 以下查询会锁住 [20, 30) 的范围包含 20不包含 30SELECT*FROMuserWHEREage20ANDage30FORUPDATE;Next-Key Lock 是 InnoDB 默认的行锁算法在 RR 级别下等值查询且记录存在退化为记录锁等值查询且记录不存在退化为间隙锁范围查询使用 Next-Key Lock4.3 插入意向锁Insert Intention Lock一种特殊的间隙锁表示事务有意向在某个间隙中插入数据。-- 事务 A 持有 (20, 30) 的间隙锁-- 事务 B 想插入 age25 的记录-- B 需要获得 (20, 30) 的插入意向锁-- 如果 A 持有的是间隙锁非插入意向锁B 可以获取插入意向锁并插入-- 如果 A 持有的是 Next-Key 锁B 需要等待4.4 自增锁AUTO-INC Lock用于保证自增列的唯一性有三种模式模式参数说明传统模式innodb_autoinc_lock_mode 0每次 INSERT 都加表级锁性能最差连续模式innodb_autoinc_lock_mode 1简单 INSERT 用轻量锁批量 INSERT 用表锁交错模式innodb_autoinc_lock_mode 2所有 INSERT 都用轻量锁性能最好但自增值可能不连续默认五、死锁原因、排查与解决5.1 死锁的产生条件死锁是指两个或多个事务相互等待对方释放锁导致所有事务都无法继续执行。死锁的四个必要条件同时满足才会发生互斥条件资源一次只能被一个事务占用请求与保持事务已持有资源又请求新的资源不可剥夺已获得的资源不能被强制剥夺循环等待事务之间形成循环等待链5.2 经典死锁案例-- 表结构CREATETABLEaccount(idINTPRIMARYKEY,balanceDECIMAL(18,2));INSERTINTOaccountVALUES(1,1000),(2,1000);-- 事务 ASTARTTRANSACTION;UPDATEaccountSETbalancebalance-100WHEREid1;-- 持有 id1 的锁-- ... 此时切换到事务 BUPDATEaccountSETbalancebalance100WHEREid2;-- 等待 id2 的锁被 B 持有-- 死锁-- 事务 BSTARTTRANSACTION;UPDATEaccountSETbalancebalance-100WHEREid2;-- 持有 id2 的锁-- ... 此时切换到事务 AUPDATEaccountSETbalancebalance100WHEREid1;-- 等待 id1 的锁被 A 持有-- 死锁死锁日志MySQL 自动检测并选择一个事务回滚ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction5.3 死锁排查方法-- 1. 查看最近一次死锁信息SHOWENGINEINNODBSTATUS;-- 在输出中搜索 LATEST DETECTED DEADLOCK 部分-- 2. 开启死锁日志记录到错误日志SETGLOBALinnodb_print_all_deadlocksON;-- 3. 查看当前锁等待情况SELECTr.trx_id waiting_trx_id,r.trx_mysql_thread_id waiting_thread,r.trx_query waiting_query,b.trx_id blocking_trx_id,b.trx_mysql_thread_id blocking_thread,b.trx_query blocking_queryFROMinformation_schema.innodb_lock_waits wINNERJOINinformation_schema.innodb_trx bONb.trx_idw.blocking_trx_idINNERJOINinformation_schema.innodb_trx rONr.trx_idw.requesting_trx_id;-- 4. 查看当前所有锁SELECT*FROMinformation_schema.innodb_locks;-- 5. 查看锁等待详情SELECT*FROMinformation_schema.innodb_lock_waits;死锁日志解读------------------------ LATEST DETECTED DEADLOCK ------------------------ *** (1) TRANSACTION: TRANSACTION 4217, ACTIVE 12 sec starting index read mysql tables in use 1, locked 1 LOCK WAIT 3 lock struct(s), heap size 1136, 2 row lock(s) MySQL thread id 10, OS thread handle 123145303453696, query id 108 localhost root updating UPDATE account SET balance balance 100 WHERE id 2 *** (1) WAITING FOR THIS LOCK TO BE GRANTED: RECORD LOCKS space id 24 page no 3 n bits 72 index PRIMARY of table test.account trx id 4217 lock_mode X locks rec but not gap waiting *** (2) TRANSACTION: TRANSACTION 4216, ACTIVE 18 sec starting index read mysql tables in use 1, locked 1 3 lock struct(s), heap size 1136, 2 row lock(s) MySQL thread id 11, OS thread handle 123145304526848, query id 109 localhost root updating UPDATE account SET balance balance 100 WHERE id 1 *** (2) HOLDS THE LOCK(S): RECORD LOCKS space id 24 page no 3 n bits 72 index PRIMARY of table test.account trx id 4216 lock_mode X locks rec but not gap *** (2) WAITING FOR THIS LOCK TO BE GRANTED: RECORD LOCKS space id 24 page no 3 n bits 72 index PRIMARY of table test.account trx id 4216 lock_mode X locks rec but not gap waiting *** WE ROLL BACK TRANSACTION (1)解读要点事务 4217线程 10在等待 id2 的记录锁X 锁事务 4216线程 11持有 id1 的记录锁同时在等待 id2 的记录锁MySQL 选择回滚事务 4217通常选择修改行数少的事务5.4 死锁预防策略策略具体做法适用场景统一加锁顺序所有事务按相同的顺序访问资源转账、库存扣减减少事务范围事务中只包含必要的操作尽快提交所有场景使用乐观锁用版本号或 CAS 代替数据库锁读多写少、冲突概率低降低隔离级别使用 READ COMMITTED 代替 REPEATABLE READ允许不可重复读的场景添加合适的索引避免全表扫描导致的锁升级所有涉及锁的场景统一加锁顺序示例// 转账操作总是先锁 id 小的账户publicvoidtransfer(intfromId,inttoId,BigDecimalamount){intfirstIdMath.min(fromId,toId);intsecondIdMath.max(fromId,toId);jdbcTemplate.update(UPDATE account SET balance balance - ? WHERE id ?,amount,firstId);jdbcTemplate.update(UPDATE account SET balance balance ? WHERE id ?,amount,secondId);}六、实战隔离级别与锁的验证实验实验 1验证 READ COMMITTED 的不可重复读-- 会话 ASETSESSIONTRANSACTIONISOLATIONLEVELREADCOMMITTED;STARTTRANSACTION;SELECTbalanceFROMaccountWHEREid1;-- 结果: 1000-- 切换到会话 BSTARTTRANSACTION;UPDATEaccountSETbalance800WHEREid1;COMMIT;-- 切换回会话 ASELECTbalanceFROMaccountWHEREid1;-- 结果: 800不可重复读COMMIT;实验 2验证 REPEATABLE READ 解决不可重复读-- 会话 ASETSESSIONTRANSACTIONISOLATIONLEVELREPEATABLEREAD;STARTTRANSACTION;SELECTbalanceFROMaccountWHEREid1;-- 结果: 1000-- 切换到会话 BSTARTTRANSACTION;UPDATEaccountSETbalance800WHEREid1;COMMIT;-- 切换回会话 ASELECTbalanceFROMaccountWHEREid1;-- 结果: 1000RR 级别快照读保持一致COMMIT;实验 3验证幻读与间隙锁-- 会话 ASTARTTRANSACTION;SELECT*FROMaccountWHEREidBETWEEN1AND5FORUPDATE;-- 假设结果: id1, id2-- 切换到会话 BSTARTTRANSACTION;INSERTINTOaccountVALUES(3,500);-- 阻塞被间隙锁锁住-- 切换回会话 ASELECT*FROMaccountWHEREidBETWEEN1AND5FORUPDATE;-- 结果仍然是 id1, id2COMMIT;-- 会话 B 的 INSERT 才能继续执行七、锁优化建议7.1 减少锁冲突的 SQL 优化-- ❌ bad: 长事务持有锁时间长STARTTRANSACTION;SELECT*FROMordersWHEREid1FORUPDATE;-- ... 业务逻辑处理 5 秒 ...UPDATEordersSETstatuspaidWHEREid1;COMMIT;-- ✅ good: 缩短事务范围-- 业务逻辑处理完再开启事务STARTTRANSACTION;UPDATEordersSETstatuspaidWHEREid1;COMMIT;7.2 索引与锁的关系-- ❌ bad: 无索引导致全表扫描锁升级UPDATEuserSETstatusinactiveWHEREage25;-- 如果没有 age 索引会对全表加锁-- ✅ good: 添加索引只锁符合条件的行CREATEINDEXidx_ageONuser(age);UPDATEuserSETstatusinactiveWHEREage25;-- 只锁 age25 的行7.3 监控锁等待-- 查看当前锁等待超过 1 秒的事务SELECTTIMESTAMPDIFF(SECOND,trx_started,NOW())AStrx_seconds,trx_id,trx_mysql_thread_id,LEFT(trx_query,100)ASqueryFROMinformation_schema.innodb_trxWHERETIMESTAMPDIFF(SECOND,trx_started,NOW())1ORDERBYtrx_secondsDESC;-- 查看当前锁等待详情MySQL 8.0SELECT*FROMperformance_schema.data_lock_waits;SELECT*FROMperformance_schema.data_locks;结语事务与锁是 MySQL 并发控制的两大支柱MVCC实现了读写的无锁并发是高并发读性能的基础锁机制保证了写操作的互斥和数据一致性隔离级别在一致性和并发性之间提供了权衡选择死锁是并发控制的代价需要通过设计和监控来最小化影响理解这些机制不仅能写出更高效的 SQL还能在出现并发问题时快速定位和解决。如果本文对你有帮助欢迎点赞收藏