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就没办法给行加锁了,这时候就会扫描全表,把所有行都锁一遍,相当于变成了表锁。
这就是为什么索引失效会导致性能差:不仅查询慢,还会把所有行都锁了,并发性能直接崩了。
行锁的两种模式
-
共享锁(S锁):读锁,加了S锁之后,其他事务也能加S锁,但是不能加X锁
- 比如:SELECT ... LOCK IN SHARE MODE
-
排他锁(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会给这个范围的间隙也加上锁,这样其他事务就不能往这个范围里插数据了,就不会出现幻读了。
间隙锁的注意点
- 间隙锁只会在可重复读(RR)隔离级别下才有
- 间隙锁和间隙锁之间不冲突,但是间隙锁和插入操作是冲突的
- 间隙锁会导致加锁的范围变大,本来你只想锁一条记录,结果把周围的间隙也锁了,并发性能就差了
五、临键锁(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有自动检测死锁的机制,发现死锁之后,会自动回滚其中一个事务,让另一个事务继续执行。
怎么避免死锁
- 按相同的顺序访问表和行:比如所有事务都先锁id=1,再锁id=2,就不会死锁了
- 大事务拆成小事务:事务越小,持锁时间越短,死锁概率越低
- 尽量用索引访问数据:不用索引的话会锁更多的行,更容易死锁
- 降低隔离级别:比如从可重复读降到读提交,间隙锁就没了,死锁概率也会低一点
七、怎么查看当前有哪些锁
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锁机制的核心知识点:
- 表锁:锁整张表,开销小,并发低,MyISAM默认用
- 行锁:锁某一行,开销大,并发高,InnoDB默认用
- 行锁是锁索引实现的,没有用到索引的话会变成表锁
- 共享锁(S锁):读锁,多个事务可以同时加
- 排他锁(X锁):写锁,只能有一个事务加
- 间隙锁:锁两个值之间的间隙,用来防止幻读,只有可重复读隔离级别才有
- 临键锁:行锁 + 间隙锁,InnoDB默认的加锁方式
- 死锁:两个事务互相等对方释放锁,MySQL会自动回滚其中一个
- 避免死锁的方法:按相同顺序访问、小事务、用索引
锁是数据库里比较复杂的一个知识点,但是也是必须掌握的。搞懂了锁,你才能写出高并发的数据库代码。
遇到问题加QQ23979811 协助处理