MySQL锁机制详解,行锁表锁间隙锁一次搞懂

前言

做网站开发的同学肯定都遇到过这种情况:两个人同时修改同一条数据,结果后提交的把先提交的覆盖了,数据就不对了。或者两个事务互相等对方释放锁,结果都卡住了,这就是死锁。

这些问题其实都是跟MySQL的锁机制有关的。很多新手同学对锁的概念很模糊,不知道什么是表锁、行锁、间隙锁,也不知道什么时候会加什么锁。这篇文章就把MySQL的锁机制从头到尾讲清楚,看完之后你就对锁有一个完整的认识了。

一、为什么需要锁

为什么数据库需要锁?很简单,因为要保证数据的一致性。

如果没有锁,两个事务同时修改同一条数据,就会出现问题。比如:

  • A事务读了一条数据,值是100
  • B事务也读了同一条数据,值也是100
  • A事务给它加1,变成101,写回去
  • B事务也给它加1,变成101,写回去

结果两个操作加起来,应该变成102,结果实际变成了101。这就出问题了。

锁的作用就是:当一个事务在操作数据的时候,其他事务不能操作,这样就保证了数据的一致性。

二、表锁

表锁就是锁整张表。当一个事务给表加了表锁,其他事务就不能操作这张表了。

表锁的特点

  • 开销小,加锁快:因为不用锁每一行,直接锁整张表就行
  • 不会出现死锁:因为一次就锁整张表,不存在互相等的情况
  • 锁粒度大,并发度低:一张表同一时间只能有一个事务写,并发高了性能就差

什么时候用表锁

  • 基本上我们用的InnoDB引擎很少用表锁,MyISAM引擎默认就是表锁
  • InnoDB引擎在某些特殊情况下也会用表锁,比如:
    • 没有用到索引,导致行锁失效,升级成表锁
    • 要修改很多行数据,优化器觉得用表锁更划算

怎么手动加表锁

-- 加读锁(共享锁)
LOCK TABLES test READ;

-- 加写锁(排他锁)
LOCK TABLES test WRITE;

-- 释放锁
UNLOCK TABLES;

三、行锁

行锁就是锁某一行数据。当一个事务给某一行加了行锁,其他事务就不能操作这一行了,但是可以操作表里的其他行。

行锁的特点

  • 开销大,加锁慢:要找到具体的行,然后加锁
  • 可能会出现死锁:两个事务分别锁了不同的行,又互相等对方释放
  • 锁粒度小,并发度高:不同的行可以同时被不同的事务操作,并发性能好

InnoDB的行锁是怎么实现的

很多同学以为InnoDB的行锁是真的锁数据行,其实不是。InnoDB的行锁是锁索引实现的。

也就是说,你WHERE条件用到了哪个索引,InnoDB就会给那个索引上加锁。如果你的WHERE条件没有用到索引,那InnoDB就没办法给行加锁了,这时候就会扫描全表,把所有行都锁一遍,相当于变成了表锁。

这就是为什么索引失效会导致性能差:不仅查询慢,还会把所有行都锁了,并发性能直接崩了。

行锁的两种模式

  1. 共享锁(S锁):读锁,加了S锁之后,其他事务也能加S锁,但是不能加X锁

    • 比如:SELECT ... LOCK IN SHARE MODE
  2. 排他锁(X锁):写锁,加了X锁之后,其他事务既不能加S锁,也不能加X锁

    • 比如:SELECT ... FOR UPDATE
    • UPDATE、DELETE语句默认都会加X锁

四、间隙锁

间隙锁是InnoDB里比较特殊的一种锁,很多同学都搞不懂。

简单来说,间隙锁就是锁两个值之间的间隙。比如你的表有1、3、5三个值,那间隙锁可以锁 (1,3)、(3,5) 这些区间,让你不能往这个区间里插数据。

间隙锁是干嘛的

间隙锁是为了防止幻读。

什么是幻读?比如:

  • 事务A查询id > 5的记录,查到了5、6两条
  • 事务B插入了一条id=7的记录
  • 事务A再查一次,发现多了一条id=7的,这就是幻读

间隙锁就是为了解决这个问题:当你查询某个范围的数据时,InnoDB会给这个范围的间隙也加上锁,这样其他事务就不能往这个范围里插数据了,就不会出现幻读了。

间隙锁的注意点

  1. 间隙锁只会在可重复读(RR)隔离级别下才有
  2. 间隙锁和间隙锁之间不冲突,但是间隙锁和插入操作是冲突的
  3. 间隙锁会导致加锁的范围变大,本来你只想锁一条记录,结果把周围的间隙也锁了,并发性能就差了

五、临键锁(Next-Key Lock)

临键锁就是行锁 + 间隙锁的组合。InnoDB在可重复读隔离级别下,默认用的就是临键锁。

比如你有一个索引,值是1、3、5、7。如果你查询id=3,那InnoDB加的临键锁就是 (1, 3],也就是锁了1到3这个区间,包括3本身。

临键锁的作用就是:既锁了这条记录,又锁了它前面的间隙,这样就防止了幻读。

六、死锁

死锁就是两个或者多个事务互相等对方释放锁,结果谁也动不了,就卡住了。

比如:

  • 事务A锁了id=1的行,想要锁id=2的行
  • 事务B锁了id=2的行,想要锁id=1的行
  • 这时候两个事务互相等,就死锁了

怎么解决死锁

MySQL有自动检测死锁的机制,发现死锁之后,会自动回滚其中一个事务,让另一个事务继续执行。

怎么避免死锁

  1. 按相同的顺序访问表和行:比如所有事务都先锁id=1,再锁id=2,就不会死锁了
  2. 大事务拆成小事务:事务越小,持锁时间越短,死锁概率越低
  3. 尽量用索引访问数据:不用索引的话会锁更多的行,更容易死锁
  4. 降低隔离级别:比如从可重复读降到读提交,间隙锁就没了,死锁概率也会低一点

七、怎么查看当前有哪些锁

MySQL8.0可以用下面的命令查看当前的锁:

SELECT * FROM performance_schema.data_locks;

或者看一下当前的事务:

SELECT * FROM information_schema.innodb_trx;

常见坑

坑1:没有用索引,行锁变成表锁

很多同学以为自己加的是行锁,结果发现整张表都被锁了。这是因为你的WHERE条件没有用到索引,InnoDB没办法定位到具体的行,只能扫描全表,把所有行都锁了,相当于表锁。

解决方法: 确保你的查询语句用到了索引,这样才会加行锁而不是表锁。

坑2:间隙锁导致并发性能差

很多同学发现自己的并发不高,但是锁冲突很严重。这就是因为间隙锁把范围锁大了。本来你只想锁一条记录,结果把周围的间隙也锁了,其他事务插数据就会被挡住。

解决方法: 如果不需要解决幻读,可以把隔离级别降到读提交(RC),这样就没有间隙锁了,并发性能会好很多。

坑3:事务太长,持锁时间久

很多同学写代码的时候,事务里包含了RPC调用、查Redis、做复杂计算这些耗时操作。结果这个事务持锁的时间特别长,其他事务都在等锁,性能就差了。

解决方法: 事务里只放数据库操作,其他耗时操作都放到事务外面。

坑4:死锁了不知道怎么排查

很多同学遇到死锁就慌了,不知道怎么查。其实MySQL默认会把死锁的信息记录到错误日志里。可以看看MySQL的错误日志,里面有详细的死锁信息。

也可以用这个命令查看最近的死锁:

SHOW ENGINE INNODB STATUS;

里面有个LATEST DETECTED DEADLOCK部分,就是最近一次死锁的信息。

总结

MySQL锁机制的核心知识点:

  1. 表锁:锁整张表,开销小,并发低,MyISAM默认用
  2. 行锁:锁某一行,开销大,并发高,InnoDB默认用
  3. 行锁是锁索引实现的,没有用到索引的话会变成表锁
  4. 共享锁(S锁):读锁,多个事务可以同时加
  5. 排他锁(X锁):写锁,只能有一个事务加
  6. 间隙锁:锁两个值之间的间隙,用来防止幻读,只有可重复读隔离级别才有
  7. 临键锁:行锁 + 间隙锁,InnoDB默认的加锁方式
  8. 死锁:两个事务互相等对方释放锁,MySQL会自动回滚其中一个
  9. 避免死锁的方法:按相同顺序访问、小事务、用索引

锁是数据库里比较复杂的一个知识点,但是也是必须掌握的。搞懂了锁,你才能写出高并发的数据库代码。

遇到问题加QQ23979811 协助处理


emer 发布于  2026-10-5 08:50