Lock
锁
一般规律
MySQL的锁基本上都有X型(exclusive:独占)和S(share:共享)型之分,用于读的更高并发。
除了全局锁(只读)和插入意向锁(只有IX(intention exclusive)型,插入只能独占)。
MySQL 加锁时,是先生成锁结构,然后设置锁的状态,如果锁状态是等待状态,并不是意味着事务成功获取到了锁,只有当锁状态为正常状态时,才代表事务成功获取到了锁 。
全局锁
命令如下
flush tables with read lock; // lock all tables;
unlock tables; //unlock;表级锁
表锁
lock tables table1 table2 ... read;
lock tables table1 table2 ... write;
unlock tables;元数据锁
MDL主要用于维护表结构。
关于表结构的锁。
- 在执行CRUD时,加的是MDL read lock。
- 在修改表结构时,加的是MDL write lock。
在执行命令时上锁,事务提交时释放。
由于MDL write lock具有更高优先级,更改结构的语句开始时(还没获得锁,但在等待锁),会阻塞后续的CRUD的操作。
意向锁
表级别的意向锁是用于快速判断有没有更细粒度的锁,如行级别的锁。
在加行级锁时,会先在表级别加上对应的意向锁。如
- 在使用 InnoDB 引擎的表里对某些记录加上「共享锁」之前,需要先在表级别加上一个「意向共享锁」;
- 在使用 InnoDB 引擎的表里对某些纪录加上「独占锁」之前,需要先在表级别加上一个「意向独占锁」;
如下列语句,会先加表级意向锁,再加行级锁。
select ... for update; //先加表级意向独占锁,再加行级独占锁。
select ... lock in share mode; //先加表级意向共享锁,再加行级共享锁。ATUO-INC锁
AUTO-INC 锁用于插入记录时,控制分配自增主键的并发。
InnoDB 存储引擎提供了个 innodb_autoinc_lock_mode 的系统变量,是用来控制选择用 AUTO-INC 锁,还是轻量级的锁。
- 当 innodb_autoinc_lock_mode = 0,就采用 AUTO-INC 锁,语句执行结束后才释放锁;
- 当 innodb_autoinc_lock_mode = 2,就采用轻量级锁,申请自增主键后就释放锁,并不需要等语句执行后才释放。
- 当 innodb_autoinc_lock_mode = 1:
- 普通 insert 语句,自增锁在申请之后就马上释放;
- 类似 insert … select 这样的批量插入数据的语句,自增锁还是要等语句结束后才被释放;
当 innodb_autoinc_lock_mode = 2 是性能最高的方式,但是当搭配 binlog 的日志格式是 statement 一起使用的时候,在「主从复制的场景」中会发生数据不一致的问题。
因为binlogi记录的是statement(即原始命令),主从复制时,从库接受到statement之后,按顺序执行。
考虑如下场景
当 innodb_autoinc_lock_mode = 2,
主库:session B的插入语句是插入了四条记录,每条记录之间都有可能执行sessionA的插入,拿到了对应的id。
从库:由于binlog中都是按顺序记录的,所以从库不能知道这俩其实是并发执行的。只是会顺序执行binlog。
所以这里sessionA插入的id只能是1或者5。出现了主从库数据不一致的情况。
总结来看,当 innodb_autoinc_lock_mode = 2,事务的多条插入就不会被视为一个整体。可能被其他事务临门一脚。主库和从库会有数据不一致的情况。
解决上述问题的方法是,将binlog设置为row。这样在 binlog 里面记录的是主库分配的自增值,到备库执行的时候,主库的自增值是什么,从库的自增值就是什么。
所以,当 innodb_autoinc_lock_mode = 2 时,并且 binlog_format = row,既能提升并发性,又不会出现数据一致性问题。
行级锁
Record Lock
Record Lock称为记录锁,顾名思义就是锁住一个记录的。
Gap Lock
Gap Lock称为间隙锁,顾名思义就是锁住一个区间,特指左开右开区间。
注:虽然Gap Lock也有X型(exclusive)和S型(share),但作用上没有区别,都是防止插入,与插入意向锁互斥。加X还是S,要看SQL语句是读还是写。
Next-Key Lock
Next-Key Lock称为临键锁,实际上是Record Lock + Gap Lock,锁住上一条记录(不含)到本条记录(含),即左开右闭区间。
由于含有Gap Lock,因此也有Gap Lock的作用。
插入意向锁
顾名思义是用于插入的锁,与间隙锁、另外的插入意向锁互斥。
Insert 语句在正常执行时是不会生成锁结构的,它是靠聚簇索引记录自带的 trx_id 隐藏列来作为隐式锁来保护记录的。
当事务需要加锁的时,如果这个锁不发生冲突,InnoDB会跳过加锁环节,这种机制称为隐式锁。
隐式锁是 InnoDB 实现的一种延迟加锁机制,其特点是只有在可能发生冲突时才加锁,从而减少了锁的数量,提高了系统整体性能。
加锁分析
MySQL实现的行级锁是加在每一个记录上的索引上的,要判断相应的地方是否上锁,需要查找到附近的记录上,根据记录的值和锁的范围、锁的类型,来判断是否能够上锁。
在加行级锁之前都会先加对应表级别的锁。
MySQL加行级锁都是先加next-key锁,再判断是否需要缩小加锁范围,变成间隙锁或者记录锁。
MySQL的加锁原则是
- 加锁范围只能大于等于请求范围。即加锁区域完全覆盖请求区域。
- 加锁加在具体的记录上,因此可能 “误伤”。可能加在并非当前查询的区域上。即加锁区域可能大于请求区域。
注:此部分存疑,实测聚簇索引在更新二级索引的情况下不会给该二级索引加锁。
由于加锁都是加在索引上的,有如下规则。
- 如果是二级索引, 还需要回表到聚簇索引给聚簇索引加相应的锁.
- 如果是聚簇索引, 并且更新了二级索引, 需要到相应二级索引上X锁.
为什么两边都要上锁呢? 考虑如下情况:
二级索引到聚簇索引的情况:
- 二级索引查询, 若仅对二级索引上锁, 第一次查到一个记录符合条件, 给二级索引上相应的锁
- 第二次查询之前, 有一个事务修改了上面符合条件的记录, 导致不符合条件.
- 第二次查询时, 就出现了问题.
聚簇索引到二级索引的情况:
- 根据聚簇索引更新记录, 更新了二级索引, 更新前后读到的二级索引不一致.
- 就出现了问题.
行级锁的分析,主要是分唯一索引和非唯一索引讨论,具体情况分析如下。
唯一索引
等值查询
- 存在则加记录锁
- 记录不存在则加间隙锁
范围查询
根据范围加next-key锁,如果临界点不是对应的范围边界,则会退化成记录锁或者间隙锁。
查询的是(a, b)或者 [a, b),则b处或其下一条记录会变为间隙锁。
查询的是 [a, b)或者 [a, b],则a处可能可能变为记录锁(取决于是否存在a记录,存在则退化)。
举例,user表结构和表内容如下。
请求范围为(-∞, 5]:加锁为 (-∞, 5],即:(-∞, 1] + (1, 2] + (2, 5]。 即都是next-key锁。
请求范围为(-∞, 5):加锁为 (-∞, 5),即:(-∞, 1] + (1, 2] + (2, 5)。 id=5的记录处的next-key退化为了间隙锁。
请求范围为(6, +∞):加锁为(5, +∞)。即:(5, 20] + (20, 30] + (30, +∞]。即都是next-key锁。
请求范围为[5, +∞):加锁为[5, +∞)。即:5 (记录锁) + (5, 20] + (20, 30] + (30, +∞)。不难发现id=5处的锁为记录锁。
非唯一索引
由于是非唯一的,因此除了需要把整个查询区域都锁住外,还需要一部分冗余的加锁空间。
等值查询
假设查询为X,实际加锁范围为(a, b),且有a < X < b
注意,由于是非唯一索引,因此实际加锁范围一定是左开右开(当然如果边界是+-∞就无所谓开闭)。
范围查询
范围查询类似上述非唯一索引等值查询。
假设查询范围[x, y],查询区间可以是任意开闭。实际加锁范围为(a, b),且有a < x <= y < b
以上4种情况,如果更新了二级索引,也会给二级索引加上作用类似X型记录锁的隐式锁。
非索引查询
如果是非索引查询的话,走的是全表查询,会将所有记录都加上next-key锁。相当于是锁全表。
insert加锁分析
插入意向锁主要用于并发插入,是一种隐式锁。隐式锁就是在 Insert 过程中不加锁,只有在特殊情况下,才会将隐式锁转换为显示锁,这里我们列举两个冲突场景。
- 间隙锁冲突:如果记录之间加有间隙锁,为了避免幻读,此时是不能插入记录的;
- 唯一键冲突:如果 Insert 的记录和已有记录存在唯一键冲突,此时也不能插入记录;
- 外键检查:需要对有外键约束的进行加锁。
插入意向锁与间隙锁的冲突
当下一条记录已有间隙锁,此时会生成一个插入意向锁,然后锁的状态设置为等待状态,现象就是 Insert 语句会被阻塞。
唯一键冲突
- 插入意向锁与其他插入意向锁的冲突:顾名思义仅有唯一性的冲突,发生在如下情况下。
- 主键索引重复和唯一二级索引:插入失败并,添加S型的next-key锁,防止幻读。
上述都是插入失败的情况,会加上S型的next-key锁防止记录被delete导致的幻读。
- 两个事务并发(不一定都是插入事务)的唯一键冲突:事务没有提交,但此时来了引发上述唯一性冲突的事务。此时,原事务会加上上述X型的锁。新来的事务会阻塞等待S型的锁。
但没有唯一键冲突、且没有间隙锁的话,多个insert命令的插入意向锁不会变成显式锁,可以并发运行。
外键加锁
当外键能够在表中找到则加S型记录锁,找不到加S型Gap锁。
死锁
MySQL有死锁检测机制。
死锁发生时 lnnoDB 会选择一个资源最少的事务进行回滚.
可以通过执行 SHOW ENGINE INNODB STATUS 语句来查看最近发生的一次死锁信息.