mysql行锁
MySQL 的行锁(Row-Level Locking)是 InnoDB 存储引擎实现的一种细粒度锁机制,它允许事务只锁定需要修改或访问的特定行,而不是锁定整个表或页。这大大提高了数据库的并发性能,允许多个事务同时读写不同的行(只要它们不冲突)。
锁的类型:
记录锁(Record Locks): 最基本的行锁,直接锁定索引中的一条记录。当事务通过索引访问行(例如
SELECT ... FOR UPDATE
,UPDATE
,DELETE
)且条件精确匹配到唯一索引时,通常加的就是记录锁。间隙锁(Gap Locks): 锁定索引记录之间的“间隙”,防止其他事务在这个间隙中插入新记录。这是 InnoDB 解决幻读问题(Phantom Read)的关键机制之一。间隙锁主要作用于可重复读(REPEATABLE READ)隔离级别。
例如:如果表中有 id 为 5 和 10 的记录,事务 A 执行
SELECT * FROM t WHERE id > 5 AND id < 10 FOR UPDATE
,间隙锁会锁定 (5, 10) 这个开区间,阻止其他事务插入 id=6,7,8,9 的记录。临键锁(Next-Key Locks): InnoDB 默认的行锁模式。它是 记录锁 + 该记录前面的间隙锁 的组合。临键锁锁定一个索引记录以及该记录之前的间隙。
例如:如果表中有 id 为 5 和 10 的记录,事务 A 执行
SELECT * FROM t WHERE id = 10 FOR UPDATE
,临键锁会锁定 (5, 10] 这个区间(即间隙 (5,10) + 记录 10),阻止其他事务插入 id=6,7,8,9 的记录或修改/删除 id=10 的记录。插入意向锁(Insert Intention Locks): 一种特殊的间隙锁,表示一个事务打算在某个间隙中插入一条新记录。多个事务可以在同一个间隙上持有不冲突的插入意向锁(只要它们插入的具体位置不同)。插入意向锁会等待已有的间隙锁(Gap Locks)或临键锁(Next-Key Locks)释放
锁的兼容性:
共享锁(S Lock / Read Lock): 允许其他事务读取被锁定的行,但不允许获取独占锁或修改这些行。通常由
SELECT ... LOCK IN SHARE MODE
显式获取,或在某些情况下由隔离级别隐式添加。独占锁(X Lock / Write Lock): 阻止其他事务获取任何类型的锁(共享或独占)或修改被锁定的行。由
INSERT
,UPDATE
,DELETE
语句以及SELECT ... FOR UPDATE
隐式或显式获取。兼容性矩阵:
当前持有 / 请求 X (独占锁) S (共享锁) X (独占锁) 冲突 冲突 S (共享锁) 冲突 兼容 一个行上只能有一个独占锁。
一个行上可以有多个共享锁(只要没有独占锁)。
共享锁与独占锁互斥。
间隙锁之间通常是兼容的(因为它们锁的是“空”的区域),除非它们意图锁定的范围完全冲突(非常罕见)。间隙锁与插入意向锁是冲突的。
何时加锁:
行锁通常在语句执行过程中,当需要访问或修改某一行时,按需加锁(Locking as needed)。
事务提交(COMMIT)或回滚(ROLLBACK)时,会释放该事务持有的所有锁。
索引对锁的影响:
InnoDB 的行锁是加在索引记录上的。即使表没有显式定义索引,InnoDB 也会为每行生成一个隐藏的聚簇索引(Clustered Index)。
锁的范围取决于使用的索引:
如果语句使用了主键索引(Primary Key) 或 唯一索引(Unique Index) 进行精确匹配(
WHERE id = 10
),InnoDB 只会锁定找到的那条具体的行(记录锁)。如果语句使用了非唯一索引(Non-Unique Index) 或 范围条件(
WHERE id > 10
),InnoDB 可能不仅锁定满足条件的行,还会锁定这些行周围的间隙(间隙锁或临键锁),以防止幻读。如果语句没有使用索引(Full Table Scan),InnoDB 无法使用行锁!它会退而求其次,锁定整个表(表锁)或扫描到的所有行(实际上是锁住索引的所有记录和间隙),这会导致严重的并发性能问题。务必确保 WHERE 条件能有效利用索引!
隔离级别的影响:
读已提交(READ COMMITTED):
通常只使用记录锁。
语句执行期间会加锁,但语句执行完可能会释放不满足条件的行上的锁(具体实现依赖)。
不使用间隙锁(除了外键约束检查和重复键检查),因此可能发生幻读。
半一致性读(semi-consistent read)可能减少锁冲突。
可重复读(REPEATABLE READ) - InnoDB 默认:
使用记录锁、间隙锁和临键锁(Next-Key Locks)。
锁会一直持有到事务结束。
通过间隙锁和临键锁防止幻读(在同一个事务内重复执行相同的查询,保证得到相同的结果集)。
串行化(SERIALIZABLE):
所有普通的
SELECT
语句都会自动转换为SELECT ... LOCK IN SHARE MODE
,对读取的行加共享锁。这可能导致更多的锁冲突和性能下降。同样使用记录锁、间隙锁、临键锁来保证最高的隔离性。
死锁(Deadlock):
行锁机制下,两个或多个事务相互等待对方释放锁时,就可能发生死锁。
示例:
事务 A:锁定了行 1,然后尝试锁定行 2。
事务 B:锁定了行 2,然后尝试锁定行 1。
此时 A 等待 B 释放行 2,B 等待 A 释放行 1,形成死循环。
InnoDB 的处理:
InnoDB 有死锁检测机制(默认开启)。
一旦检测到死锁,它会选择一个“代价较小”的事务(通常涉及修改行数较少的)进行回滚(ROLLBACK),并返回
1213
(ER_LOCK_DEADLOCK
) 错误。另一个事务则可以继续执行。避免死锁策略:
尽量按相同顺序访问表和行。
保持事务短小精悍,尽快提交。
在事务中一次性锁定所有需要的资源(如果可行)。
合理使用索引,避免锁升级。
如果业务允许,可以降低隔离级别(如使用 READ COMMITTED)。
使用
SELECT ... FOR UPDATE NOWAIT
或SELECT ... FOR UPDATE SKIP LOCKED
(MySQL 8.0+)避免等待查看锁信息:
SHOW ENGINE INNODB STATUS;
: 查看LATEST DETECTED DEADLOCK
部分(最近死锁信息)和TRANSACTIONS
部分(当前事务和锁等待信息)。需要PROCESS
权限。INFORMATION_SCHEMA
表:INNODB_TRX
: 当前运行的事务信息。INNODB_LOCKS
: 当前事务持有的锁和正在等待的锁信息(MySQL 5.7 及更早)。INNODB_LOCK_WAITS
: 锁等待关系(MySQL 5.7 及更早)。performance_schema.data_locks
: MySQL 8.0+ 替代INNODB_LOCKS
,提供更详细的锁信息。performance_schema.data_lock_waits
: MySQL 8.0+ 替代INNODB_LOCK_WAITS
,提供锁等待信息。sys.innodb_lock_waits
(MySQL 5.6+, sys schema): 提供更易读的锁等待视图。
MySQL InnoDB 的行锁是支撑其高并发事务处理能力的核心机制。理解不同类型的行锁(记录锁、间隙锁、临键锁、插入意向锁)、它们的兼容性、索引如何影响锁范围、以及不同隔离级别的行为差异,对于设计高性能、高并发的数据库应用至关重要。合理使用索引、优化事务设计、选择合适的隔离级别以及监控锁状态是避免锁竞争和死锁的关键。