锁的分类
- 全局锁
要使用全局锁,则要执行这条命令:
flush tables with read lock
执行后,整个数据库就处于只读状态了,这时其他线程执行以下操作,都会被阻塞:
- 对数据的增删改操作,比如 insert、delete、update等语句;
- 对表结构的更改操作,比如 alter table、drop table 等语句。
全局锁的应用场景:全局锁主要应用于做全库逻辑备份,这样在备份数据库期间,不会因为数据的更新,而出现备份文件的数据与预期的不一样。
-
表锁
- 意向锁:使用 InnoDB 引擎的表里对某些记录加上「共享锁」之前,需要先在表级别加上一个「意向共享锁」;对某些纪录加上「独占锁」之前,需要先在表级别加上一个「意向独占锁」;意向锁的目的是为了快速判断表里是否有记录被加锁。
- AUTO-INC 锁:在插入数据时,会加一个表级别的 AUTO-INC 锁,然后为被
AUTO_INCREMENT
修饰的字段赋值递增的值,等插入语句执行完成后,就会把 AUTO-INC 锁释放掉。而不是再一个事务提交后才释放。AUTO-INC 锁再对大量数据进行插入的时候,会影响插入性能,因此InnoDB 存储引擎提供了一种轻量级的锁来实现自增。在插入数据的时候,为被AUTO_INCREMENT
修饰的字段加上轻量级锁,然后给该字段赋值一个自增的值,就把这个轻量级锁释放了,而不需要等待整个插入语句执行完后才释放锁。
-
行级锁
-
Record Lock(记录锁):也就是仅仅把一条记录锁上;
-
Gap Lock(间隙锁):锁定一个范围,但是不包含记录本身;
-
Next-Key Lock(临键锁):Record Lock + Gap Lock 的组合,锁定一个范围,并且锁定记录本身。
-
插入意向锁:一个事务在插入一条记录的时候,需要判断插入位置是否已被其他事务加了间隙锁(next-key lock 也包含间隙锁)。
如果有的话,插入操作就会发生阻塞,直到拥有间隙锁的那个事务提交为止(释放间隙锁的时刻),在此期间会生成一个插入意向锁,表明有事务想在某个区间插入新记录,但是现在处于等待状态。
MySQL中的死锁
假设表内已经有了6条记录,order_no 字段为非唯一索引:
假设这时有两事务,一个事务要插入订单 1007 ,另外一个事务要插入订单 1008,因为需要对订单做幂等性校验,所以两个事务先要查询该订单是否存在,不存在才插入记录,过程如下:
事务 A 在执行下面这条语句的时候:
select id from t_order where order_no = 1007 for update;
在二级索引(INDEX_NAME : index_order)上加了 X 型的 next-key 锁,锁范围是(1006, +∞]
由于临键锁可以被两个事务同时取得,所以事务 B在执行下面这条语句的时候:
select id from t_order where order_no = 1008 for update;
事务B也在二级索引(INDEX_NAME : index_order)上加了锁范围为(1006, +∞]
X 型的 next-key 锁,而接下来的插入操作为了获取到插入意向锁,都在等待对方事务的间隙锁释放,于是就造成了循环等待,导致死锁。
如何避免死锁?
死锁的四个必要条件:互斥、占有且等待、不可强占用、循环等待。只要系统发生死锁,这些条件必然成立,但是只要破坏任意一个条件就死锁就不会成立。
在数据库层面,有两种策略通过打破循环等待条件来解除死锁状态:
- 设置事务等待锁的超时时间。当一个事务的等待时间超过该值后,就对这个事务进行回滚,于是锁就释放了,另一个事务就可以继续执行了。
- 开启主动死锁检测。主动死锁检测在发现死锁后,主动回滚死锁链条中的某一个事务,让其他事务得以继续执行。
在业务的角度来预防死锁,对订单做幂等性校验的目的是为了保证不会出现重复的订单,那我们可以直接将 order_no 字段设置为唯一索引列,利用它的唯一性来保证订单表不会出现重复的订单,不过有一点不好的地方就是在我们插入一个已经存在的订单记录时就会抛出异常。