Skip to content

MySQL有哪些锁?

image

MySQL的锁主要有全局锁、表级锁、和行锁。

全局锁上了,整个数据库就只支持读操作。 表锁会限制本线程以及其他线程的读写操作。

但是注意在InnoDB引擎下,我们可以用粒度更细的行级锁。

元数据锁(MDL)

这个不需要显示使用,因为我们对数据库表进行操作时,会自动给表加上MDL。

对表进行CRUD,加的是MDL读锁,对表做结构变更,加的是MDL写锁。

如果有长事务在执行,同时有线程修改了表的字段,那么这个时候就会被阻塞,此后如果有大量的select请求到来,由于长事务没有释放,表就卡死了。

为什么线程C因为申请不到MDL写锁,而导致后续的读锁也会冲突?因为在等待队列中,写锁的优先级比读锁高。

意向锁

  • 在使用InnoDB对某些记录加上共享锁之前,要在表级别上加一个意向共享锁。

  • 在使用InnoDB对某些记录加上独占锁之前,要在表级别上加一个意向独占锁。

当执行插入、更新、删除操作的时候对表加上意向独占锁,然后加独占锁。

普通的select不会加行级锁,是通过MVCC实现的一致读。

意向锁的用处是为了快速判断表里面是否有记录被枷锁。

当尝试给表加锁的时候,就会检测这个表是否有意向锁。如果有,说明表里面的某些数据是已经被上锁了,就阻塞。

AUTO-INC锁

这个锁是特殊的机制,用于自增字段,在执行完插入语句后立即释放。插入数据的时候,会加一个表级别的AUTO-INC锁。

但是这样会有问题,高并发插入数据的时候,会影响插入性能。

因此从MySQL 5.1.22开始。InnoDB存储引擎提供了一种轻量级的锁来实现自增。

一样在插入数据的时候加锁,但是在给自增的字段赋值完毕之后就释放掉,而不是等整个语句执行完成。

但是这样当搭配binlog的日志格式是statement一起使用的时候,在主从复制的时候会发生数据不一致的问题。

具体为什么比较复杂,复习的时候直接看原文。

行级锁

查询会枷锁的语句称为锁定读。

  • 行级锁主要有三类:Record Lock 记录锁,仅仅锁上一条记录。
  • Gap Lock间隙锁,锁定范围,但是不包括记录本身。
  • Next-Key Lock 记录锁+间隙锁的组合,锁定范围与记录本身。

MySQL是怎么加锁的?

在不同隔离级别下,行级锁的种类是不同的。

在读已提交隔离级别上,只会加记录锁。

而在可重复读隔离级别下,除了加记录锁,还会加间隙锁,因为要防止在事务执行过程中发生幻读现象。

而临键锁的规则如下:假如一个事务持有范围是(1,10]的next-key lock,那么另一个事务如果同样获取这个,就会被阻塞。

MySQL是怎么加行级锁的?

加锁的对象是索引,加锁的基本单位是next-key lock,是由记录锁和间隙锁组合而成的,临键锁是左开右闭区间,间隙锁两侧都是开区间。

但是临键锁在一些场景下会退化成记录锁或间隙锁。

下面所说的都是基于可重复读隔离场景下的。

在用唯一索引进行等值查询的时候,如果记录存在,那么临键锁退化成记录锁(可能是默认加锁都是临键锁?)

如果查询索引不存在,数据库会找第一条大于查询记录的索引的记录,并把这个记录索引的临键锁退化成间隙锁。

加锁的对象是针对索引。

为什么唯一索引等值查询并且查询记录存在的场景下,该记录的索引中的 next-key lock会退化成记录锁?

原因就是在唯一索引等值查询并且査询记录存在的场景下,仅靠记录锁也能避免幻读的问题 幻读的定义就是,当一个事务前后两次查询的结果集,不相同时,就认为发生幻读。所以,要 避免幻读就是避免结果集某一条记录被其他事务删除, 或者有其他事务插入了一条新记录,这 样前后两次查询的结果集就不会出现不相同的情况。 由于主键具有唯一性,所以其他事务插入id =1的时候, 会因为主键冲突,导致无法插入id =1的新记录。这样事务 A在多次査询 id=1的记录的时候,不会出现前后两次查询的结果 集不同,也就避免了幻读的问题。 由于对 id =1加了记录锁,其他事务无法删除该记录,这样事务 A在多次査询 id =1的记 录的时候,不会出现前后两次查询的结果集不同,也就避免了幻读的问题。

我观察到一个问题,假设MySQL执行下列语句:

mysql
select * from user where id < 6 for update;

image

这里可以发现,最后一个间隙锁锁住的区间是(5,10)。因为前一个元素是5,后一个是10.这样会导致锁住无关数据!我认为这是一个mysql的设计缺陷,但是一般情况下,唯一索引之间可能值的差别不是很大,所以没优化吧。

update没加索引会锁全表?

MySQL记录锁+间隙锁可以防止删除操作导致的幻读吗?

MySQL死锁了,怎么办?

什么情况会产生死锁?

image

假如此时两个事务,一个插入1007,一个插入1008.因为要对订单做幂等性校验,两个事务要先查询订单是否存在,不存在才插入记录。

image

这里就会产生死锁!为什么呢?

主要是MySQL的默认可重复读机制,在当前读的状态下会使用临键锁来尽量避免幻读问题。而正是临键锁导致了这里死锁的情况发生。

按理说,查询1006的行记录,应该不会影响查询到1007的行记录才对。

在正常的快照读的情况下没问题,但是这里是当前读,会加临键锁。

观察当前表中的数据,可以发现,最后一条数据是1005,那么在查询1006的时候,事务A在二级索引上加了X型的临键锁,锁范围是(1006,+∞]。

那么事务B尝试插入的时候,就会死锁了。

因为执行插入语句的时候,会在插入间隙上获取插入意向锁,而插入意向锁和间隙锁是冲突的。所以如果其他事务持有这个间隙的间隙锁时,需要等待其他事务释放间隙锁之后才能获取到插入意向锁。

而间隙锁与间隙锁之间是兼容的,所以事务A、B开始执行的select不会相互影响,没有问题。

Insert语句怎么加行级锁的?

正常情况下,Insert不会生成锁结构,它是靠聚簇索引记录自带的trx_id隐藏列作为隐式锁来保护记录的。

当事务需要加锁时,如果可以知道锁不可能发生冲突,InnoDB会跳过加锁环节。这个机制叫做隐式锁。隐式锁是InnoDB实现的一种延迟加锁机制。

只有在特殊情况下,才会将隐式锁转换为显示锁,列举两个场景。

  • 如果记录之间加有间隙锁,为了避免幻读,此时不能插入记录。
  • 如果Insert的记录和已有记录存在唯一键冲突,此时不能插入记录。

第一种情况,记录之间有间隙锁

每插入一条新记录,都要看待插入的下一条记录上是否已经被加了间隙锁,如果已经被加了,此时会生成一个插入意向锁,锁设置为等待状态。现象就是Insert语句会被阻塞。

第二种情况,遇到唯一键冲突

如果是主键索引冲突,插入会报错,同时给冲突的记录加上S型记录锁。为什么?因为事务访问了这个数据,加上S锁,防止其他事务对这个数据进行删除。

如果是唯一二级索引冲突,插入报错,然后加上S型临键锁。(防止其他事务把这行数据删掉,如果删了,后续再插入就会成功,导致前后不一致了。)

接下来分析两个事务执行了相同的insert语句的场景

当然后插入的会被阻塞辣。

image

两个事务的加锁过程:事务A先插入,可以插入成功,事务B也插入,由于事务A已经插入记录成功了,事务B在插入时遇到重复的唯一二级索引列值,此时事务B想获取S型临键锁,但是事务A没提交,事务A插入的记录上的隐式锁会变成显示锁,类型X,那么B就阻塞了。

为什么要这么设计而不是直接让事务B报错?

主要区别是事务A还没提交。事务A既然没提交,就是有可能回滚的。B尝试提交时只阻塞,万一事务A回滚了,轮到事务B继续执行,那么B就可以正常插入了。

当然如果不是唯一二级索引就正常插入成功辣。

如何避免死锁?

死锁的四个必要条件:

  • 互斥
  • 占有且等待
  • 不可剥夺
  • 循环等待

数据库层面,有两种策略通过打破循环等待条件来解除死锁.

  • 设置事务等待锁的超时时间。超过这个时间就对事务回滚,这样锁释放了,别的就可以继续执行了。InnoDB默认是50秒。
  • 开启主动死锁检测。开启之后,数据库主动回滚死锁链条钟的某个事务,让其他事务可以继续运行。

字节面试,加了什么锁导致死锁?

文章

wow!