
1. 总体分类MySQL 的锁可以按锁定范围和使用思想来理解分类角度类型说明按锁范围全局锁锁住整个 MySQL 实例中的所有库、所有表按锁范围表级锁锁住一张表或者锁住表结构的元数据按锁范围行级锁锁住某些行或某些索引范围主要由 InnoDB 提供按并发控制思想悲观锁先加锁再操作按并发控制思想乐观锁不先加锁更新时检查数据是否被别人改过在实际开发中InnoDB 是 MySQL 最常用的事务型存储引擎它主要依靠MVCC 行锁 间隙锁 / 临键锁来保证并发安全和事务隔离。2. 全局锁2.1 什么是全局锁MySQL 里最常见的全局锁是FLUSH TABLES WITH READ LOCK;它会关闭所有打开的表并对所有数据库中的所有表加一个全局读锁。加锁后其他会话通常可以继续读数据但不能修改数据。释放锁使用UNLOCK TABLES;官方文档中说明FLUSH TABLES WITH READ LOCK获取的是 global read lock而不是普通表锁它会锁住所有数据库中的所有表常用于文件系统快照或备份场景。2.2 全局锁的典型场景全库备份全局锁常用于全库逻辑备份或物理快照。原因是如果备份过程中先导出 A 表再导出 B 表而此时 B 表被修改就可能导致 A、B 两张表的数据不是同一个时间点的数据。例如订单系统中A 表是ordersB 表是order_items备份orders后用户又新增了订单明细再备份order_items这样备份出来的数据就可能出现主表和明细表不一致的问题。所以全局锁的作用是让整个实例在备份期间保持一个相对静止的数据状态。2.3 InnoDB 为什么可以不依赖全局锁完成一致性备份InnoDB 支持 MVCC也就是多版本并发控制。普通SELECT在REPEATABLE READ或READ COMMITTED隔离级别下通常是一致性非锁定读。它不是去等最新数据而是基于某个时间点的 Read View 读取快照数据。因此InnoDB 做逻辑备份时可以使用mysqldump --single-transaction这个参数会在一个事务中创建一致性快照从而尽量避免用全局锁阻塞业务写入。MySQL 官方文档也说明mysqldump --single-transaction可以在不锁住其他客户端的情况下创建一致性快照。仍然可能破坏快照一致性或导致异常。3. 表级锁3.1 表级锁是什么表级锁是锁住整张表。相比全局锁它的范围更小相比行锁它的粒度更大开销较低但并发能力较弱。MySQL 可以显式使用LOCK TABLES table_name READ; LOCK TABLES table_name WRITE; UNLOCK TABLES;其中表锁类型含义READ锁当前会话可以读不能写其他会话也可以读但不能写WRITE锁当前会话可以读写其他会话不能读也不能写官方文档说明持有READ锁的会话可以读表但不能写表多个会话可以同时持有READ锁持有WRITE锁的会话可以读写表并且只有持有该锁的会话可以访问这张表其他会话会被阻塞。3.2 哪些存储引擎常用表级锁MyISAM、MEMORY、MERGE 等存储引擎主要使用表级锁同一时间通常只允许一个会话更新表因此更适合读多写少、只读或单用户场景。官方文档也指出MySQL 对 MyISAM、MEMORY、MERGE 表使用 table-level locking这会降低写并发。InnoDB 主要使用行级锁但并不代表完全没有表级锁。InnoDB 中也存在一些表级相关的锁例如意向锁IS、IX自增锁AUTO-INC Lock元数据锁Metadata Lock显式表锁LOCK TABLESDDL 场景下的表结构锁4. 行级锁4.1 行级锁是什么行级锁是 InnoDB 的核心锁机制。它锁的不是整张表而是某些行准确地说InnoDB 的行锁大多数情况下锁的是索引记录。即使表没有显式索引InnoDB 也会创建隐藏聚簇索引用于记录锁。如果没有合适索引扫描范围变大锁的范围也可能变大。行级锁的优点是并发能力强不同行之间可以并发修改缺点是锁管理开销更大也更容易出现死锁、锁等待等问题。4.2 共享锁 S Lock共享锁又叫读锁英文是 Shared Lock简称S Lock。特点持有共享锁的事务可以读取该行多个事务可以同时对同一行持有共享锁如果某行已经有共享锁其他事务不能对它加排他锁。MySQL 8.0 以后推荐写法SELECT * FROM user WHERE id 1 FOR SHARE;旧写法是SELECT * FROM user WHERE id 1 LOCK IN SHARE MODE;4.3 排他锁 X Lock排他锁又叫写锁英文是 Exclusive Lock简称X Lock。特点持有排他锁的事务可以更新或删除该行如果某行已经有排他锁其他事务不能再对它加共享锁或排他锁UPDATE、DELETE、SELECT ... FOR UPDATE通常会涉及排他锁。示例SELECT * FROM user WHERE id 1 FOR UPDATE; UPDATE user SET name Tom WHERE id 1; DELETE FROM user WHERE id 1;5. 当前读与快照读5.1 快照读快照读就是普通SELECTSELECT * FROM user WHERE id 1;在 InnoDB 中普通SELECT通常不加锁而是通过 MVCC 读取历史版本快照。快照读的特点不读取未提交数据不阻塞其他事务修改其他事务修改数据也通常不阻塞当前快照读在 InnoDB 默认的REPEATABLE READ隔离级别下同一个事务中的一致性读会读取第一次读建立的快照。5.2 当前读当前读是读取数据的最新版本并且通常需要加锁防止读到后马上被别人改掉。常见当前读包括SELECT * FROM user WHERE id 1 FOR UPDATE; SELECT * FROM user WHERE id 1 FOR SHARE; UPDATE user SET name Tom WHERE id 1; DELETE FROM user WHERE id 1; INSERT INTO user(id, name) VALUES(1, Tom);SELECT ... FOR UPDATE、SELECT ... FOR SHARE、UPDATE、DELETE这类 locking read 或写操作会根据是否使用唯一索引、是否是范围条件来加记录锁、间隙锁或临键锁。6. 记录锁、间隙锁、临键锁这三个是理解 InnoDB 行锁的重点。6.1 记录锁 Record Lock记录锁锁住的是某一条索引记录。例如SELECT * FROM user WHERE id 10 FOR UPDATE;如果id是主键或唯一索引那么 InnoDB 通常只锁住id 10这一条索引记录。注意InnoDB 的记录锁本质上是锁索引记录而不是直接锁“表中的物理行”。6.2 间隙锁 Gap Lock间隙锁锁住的是两个索引值之间的空隙不是某条具体记录。例如表中有索引值10, 20, 30如果锁住(10, 20)这个间隙那么其他事务不能在这个区间插入新的索引值比如 15。间隙锁的主要作用是防止其他事务在范围中插入新数据从而解决幻读问题。6.3 临键锁 Next-Key Lock临键锁也叫 Next-Key Lock它可以理解为临键锁 记录锁 该记录前面的间隙锁例如索引中有10, 11, 13, 20那么可能的临键锁范围包括(-∞, 10] (10, 11] (11, 13] (13, 20] (20, ∞)6.4 为什么非唯一索引容易产生临键锁假设有表CREATE TABLE user ( id INT PRIMARY KEY, age INT, KEY idx_age(age) );数据如下age: 10, 20, 20, 30执行SELECT * FROM user WHERE age 20 FOR UPDATE;如果age是非唯一索引InnoDB 不能只锁一条记录因为可能有多条age 20而且还要防止其他事务继续插入新的age 20。所以它可能会锁住相关的索引记录以及附近间隙。如果使用的是唯一索引并且查询条件也是唯一等值查询例如SELECT * FROM user WHERE id 10 FOR UPDATE;这种情况下通常只需要记录锁不需要锁前面的间隙。使用唯一索引查找唯一行时不需要 gap lock如果没有索引或使用非唯一索引就可能锁住前面的间隙。7. 意向锁7.1 什么是意向锁意向锁是 InnoDB 中的一种表级锁用于支持“表锁和行锁共存”。它不是真的要锁整张表的数据而是用来声明我这个事务准备在这张表的某些行上加锁。意向锁分为两类类型含义ISIntention Shared Lock表示事务准备对某些行加共享锁IXIntention Exclusive Lock表示事务准备对某些行加排他锁例如SELECT * FROM user WHERE id 1 FOR SHARE;会先加IS意向共享锁。SELECT * FROM user WHERE id 1 FOR UPDATE;会先加IX意向排他锁。7.2 为什么需要意向锁假设事务 A 已经锁住了表user中id 1的这一行。此时事务 B 想对整张表加写锁LOCK TABLES user WRITE;如果没有意向锁MySQL 可能要一行一行检查表里是否存在行锁这个代价很高。有了意向锁后事务 A 在加行锁之前会先在表上加IX锁。事务 B 想加整表写锁时只需要检查表上是否有冲突的意向锁即可不用扫描所有行。一句话总结意向锁是为了让表锁和行锁能够高效判断冲突。8. 乐观锁与悲观锁8.1 悲观锁悲观锁的思想是我认为数据很可能被别人修改所以我先加锁再操作。常见实现BEGIN; SELECT * FROM product WHERE id 1 FOR UPDATE; UPDATE product SET stock stock - 1 WHERE id 1; COMMIT;适合场景写冲突多库存扣减、余额扣减等强一致性场景不允许多个事务同时修改同一份数据。缺点容易出现锁等待并发性能可能下降事务写得不好容易死锁。8.2 乐观锁乐观锁的思想是我先不加锁提交更新时再检查数据有没有被别人改过。常见实现是增加version字段SELECT id, stock, version FROM product WHERE id 1; UPDATE product SET stock stock - 1, version version 1 WHERE id 1 AND version 3;如果更新影响行数为 0说明版本号已经变化当前事务更新失败需要重试或提示用户。适合场景读多写少冲突概率低希望减少数据库锁等待。9. 死锁与锁等待行锁粒度小但也更容易出现死锁。死锁例子事务 ABEGIN; UPDATE account SET balance balance - 100 WHERE id 1; UPDATE account SET balance balance 100 WHERE id 2; COMMIT;事务 BBEGIN; UPDATE account SET balance balance - 100 WHERE id 2; UPDATE account SET balance balance 100 WHERE id 1; COMMIT;事务 A 先锁id 1事务 B 先锁id 2然后双方都等待对方释放锁就会产生死锁。死锁可能发生在多个事务以相反顺序锁住多张表或多个范围时减少死锁的方法包括缩短事务、多个事务按相同顺序访问表和行、为SELECT ... FOR UPDATE和UPDATE ... WHERE中的条件列建立索引等。排查死锁可以使用SHOW ENGINE INNODB STATUS;10. 总结MySQL 的锁可以分为全局锁、表级锁和行级锁。全局锁常用于全库备份但 InnoDB 可以通过 MVCC 和一致性快照减少对全局锁的依赖。表锁粒度大、开销小、并发差MyISAM 等引擎主要使用表级锁InnoDB 主要使用行锁同时也有意向锁、元数据锁、自增锁等表级锁。InnoDB 行锁本质上是索引记录锁普通SELECT是快照读不加锁SELECT ... FOR UPDATE、UPDATE、DELETE属于当前读会加锁。为了防止幻读InnoDB 在REPEATABLE READ下会使用间隙锁和临键锁其中临键锁可以理解为记录锁加前一个间隙锁。