MySQL有哪些锁?
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执行下列语句:
select * from user where id < 6 for update;
这里可以发现,最后一个间隙锁锁住的区间是(5,10)。因为前一个元素是5,后一个是10.这样会导致锁住无关数据!我认为这是一个mysql的设计缺陷,但是一般情况下,唯一索引之间可能值的差别不是很大,所以没优化吧。
update没加索引会锁全表?
MySQL记录锁+间隙锁可以防止删除操作导致的幻读吗?
MySQL死锁了,怎么办?
什么情况会产生死锁?
假如此时两个事务,一个插入1007,一个插入1008.因为要对订单做幂等性校验,两个事务要先查询订单是否存在,不存在才插入记录。
这里就会产生死锁!为什么呢?
主要是MySQL的默认可重复读机制,在当前读的状态下会使用临键锁来尽量避免幻读问题。而正是临键锁导致了这里死锁的情况发生。
按理说,查询1006的行记录,应该不会影响查询到1007的行记录才对。
在正常的快照读的情况下没问题,但是这里是当前读,会加临键锁。
观察当前表中的数据,可以发现,最后一条数据是1005,那么在查询1006的时候,事务A在二级索引上加了X型的临键锁,锁范围是(1006,+∞]。
那么事务B尝试插入的时候,就会死锁了。
因为执行插入语句的时候,会在插入间隙上获取插入意向锁,而插入意向锁和间隙锁是冲突的。所以如果其他事务持有这个间隙的间隙锁时,需要等待其他事务释放间隙锁之后才能获取到插入意向锁。
而间隙锁与间隙锁之间是兼容的,所以事务A、B开始执行的select不会相互影响,没有问题。
Insert语句怎么加行级锁的?
正常情况下,Insert不会生成锁结构,它是靠聚簇索引记录自带的trx_id隐藏列作为隐式锁来保护记录的。
当事务需要加锁时,如果可以知道锁不可能发生冲突,InnoDB会跳过加锁环节。这个机制叫做隐式锁。隐式锁是InnoDB实现的一种延迟加锁机制。
只有在特殊情况下,才会将隐式锁转换为显示锁,列举两个场景。
- 如果记录之间加有间隙锁,为了避免幻读,此时不能插入记录。
- 如果Insert的记录和已有记录存在唯一键冲突,此时不能插入记录。
第一种情况,记录之间有间隙锁
每插入一条新记录,都要看待插入的下一条记录上是否已经被加了间隙锁,如果已经被加了,此时会生成一个插入意向锁,锁设置为等待状态。现象就是Insert语句会被阻塞。
第二种情况,遇到唯一键冲突
如果是主键索引冲突,插入会报错,同时给冲突的记录加上S型记录锁。为什么?因为事务访问了这个数据,加上S锁,防止其他事务对这个数据进行删除。
如果是唯一二级索引冲突,插入报错,然后加上S型临键锁。(防止其他事务把这行数据删掉,如果删了,后续再插入就会成功,导致前后不一致了。)
接下来分析两个事务执行了相同的insert语句的场景
当然后插入的会被阻塞辣。
两个事务的加锁过程:事务A先插入,可以插入成功,事务B也插入,由于事务A已经插入记录成功了,事务B在插入时遇到重复的唯一二级索引列值,此时事务B想获取S型临键锁,但是事务A没提交,事务A插入的记录上的隐式锁会变成显示锁,类型X,那么B就阻塞了。
为什么要这么设计而不是直接让事务B报错?
主要区别是事务A还没提交。事务A既然没提交,就是有可能回滚的。B尝试提交时只阻塞,万一事务A回滚了,轮到事务B继续执行,那么B就可以正常插入了。
当然如果不是唯一二级索引就正常插入成功辣。
如何避免死锁?
死锁的四个必要条件:
- 互斥
- 占有且等待
- 不可剥夺
- 循环等待
数据库层面,有两种策略通过打破循环等待条件
来解除死锁.
- 设置事务等待锁的超时时间。超过这个时间就对事务回滚,这样锁释放了,别的就可以继续执行了。InnoDB默认是50秒。
- 开启主动死锁检测。开启之后,数据库主动回滚死锁链条钟的某个事务,让其他事务可以继续运行。