1. mysql锁知多少
我们进行insert,update,delete,select会加锁吗,如果加锁,加锁步骤是什么,加的什么类型的锁?
行锁里面LOCK_REC_NOT_GAP,LOCK_GAP,LOCK_ORDINARY(Next-Key Lock),LOCK_INSERT_INTENTION这都是些什么锁。
锁是在数据库中是怎么构成的,锁之间兼容与互斥关系
表上有无唯一索引,增删改动作加索引步骤有何区别
死锁是怎么形成的,如何分析
2. 前方高能
这是一篇冗余啰嗦的文章,如果要完整看完,保持耐心,或者自动忽略标记为非重点的内容
3. 锁类型
在InnoDB内部用uint32类型数据表示锁的类型, 最低的 4 个 bit 表示 lock_mode, 5-8 bit 表示 lock_type(目前只用了 5 和 6 位,大小为 16 和 32 ,表示 LOCK_TABLE 和 LOCK_REC), 剩下的高位 bit 表示行锁的类型record_lock_type
record_lock_type | lock_type | lock_mode |
---|
我们说锁的时候,一般都是讲什么lock_mode的record_lock_type锁。因为我们很少分析表锁,一般分析行锁。比如LOCK_S的LOCK_REC_NOT_GAP锁,表示共享的记录锁(非间隙锁)
3.1 lock_mode
3.1.1 lock_is/lock_ix(非重点)
LOCK_IS: 表级锁,意向共享锁。表示将要在表上加共享锁。
LOCK_IX:表级锁,意向排他锁。表示是将要在表上加排他锁。
当对记录加LOCK_S或LOCK_X锁的时候,要确保在表上加了LOCK_IS或LOCK_IX锁。
3.1.2 lock_s
表共享锁、也是行共享锁
3.1.2.1 表共享锁(非重点)
ALTER语句第一阶段,当ALTER语句不能ONLINE执行的时间添加
3.1.2.2 行共享锁
- 事务读在隔离级别为SERIALIZABLE时会给记录加 LOCK_S 锁
- SELECT … IN SHARE MODE
- 普通insert语句遇到duplicate key(普通INSERT语句如果没有duplicate key是不用加行锁的,当遇到duplicate key就需要加LOCK_S锁。 5.6版本加的是LOCK_S类型的LOCK_REC_NOT_GAP锁,导致了一个bug,5.7版本改为了LOCK_S类型的LOCK_ORDINARY锁,更严格了。具体可以参考如下链接:https://m.aliyun.com/yunqi/articles/50886 。这里水很深,小心淹死,后续会举例说明(总共4种 1.三事务并行插入相同记录,事务1回滚(有无ON DUPLICATE KEY UPDATE,两种不同隔离级别)2. 2个事务,事务1插入记录,事务2插入不同记录,事务1插入记录(或者事务1,2都只插入1条,速度够快) 3. 事务1,2,3都做删除,同一条记录 4. 事务1 删除1条记录,事务2,3插入同一条记录,为什么不死锁(on duplicate update 不会死锁(RR隔离级别),如果没有on duplicate update 就会死锁。 有on duplicate update RC隔离级别还未知) ))
3.1.3 lock_x
表排他锁、也是行排他锁
3.1.3.1 表排他锁(非重点)
略
3.1.3.2 行排他锁
UPDATE/DELETE需要阻止并发对同一行数据进行修改语句的执行
3.2 record_lock_type
3.2.1 LOCK_ORDINARY (next-key lock)
锁住记录本身和记录之前的 gap。当用RR隔离级别的时候,为了防止当前读语句的幻读使用。比如update t set a=3 where b>=4, b列有索引,会锁住b=4的行,并且b>4的间隙。(查看锁信息的时候,表现为lock_mode X,比较奇怪。应该叫next-key lock 或者lock_ordinary。我也是一脸懵逼)
3.2.2 LOCK_GAP(间隙锁)
只锁住索引记录之间或者第一条索引记录前或者最后一条索引记录之后的范围,并不锁住记录本身(查看锁信息的时候,表现为lock_mode X locks gap before rec. lock_mode还有可能是S)
你可以通过切换到RC隔离级别,或者开启选项innodb_locks_unsafe_for_binlog来避免GAP锁。这时候只有在检查外键约束或者duplicate key检查时才会使用到GAP LOCK。
例如在RR隔离级别下,非唯一索引条件上的等值当前读,会在等值记录上加NEXT-KEY LOCK同时锁住行和前面范围的记录,同时会在后面一个值上加LOCK_GAP锁住下一个值前面的范围。下面的例子就会在索引i_c2上给c2 = 5上NEXT-KEY LOCK(LOCK_ORDINARY|LOCK_X),同时给c2 = 10加上LOCK_GAP|LOCK_X锁。这里是因为非唯一索引,对同一个值可以多次插入,为确保当前读的可重复读,需要锁住前后的范围,确保不会有相同等值插入。
create table t1(c1 int primary key, c2 int, c3 int, index i_c2(c2));
insert into t1 values(1, 2, 3), (2, 5, 7), (3, 10, 9);
set tx_isolation='repeatable-read';
select * from t1 where c2 = 5 for update;
3.2.3 LOCK_REC_NOT_GAP(记录锁)
仅锁住记录行,不锁范围。(查看锁信息的时候,表现为lock_mode X locks rec but not gap。 lock_mode还有可能是S)
RC隔离级别下的当前读大多是该方式。同时在上述例子中,RR隔离级别下,非唯一索引上的等值当前读,也会给主键上对应行加LOCK_X|LOCK_REC_NOT_GAP锁
3.2.4 LOCK_INSERT_INTENTION(插入意向锁)
插入意向锁,当插入索引记录的时候用来判断是否有其他事务的范围锁冲突,如果有就需要等待。插入意向锁之间并不冲突,在一个GAP锁上可以有多个意向锁等待。主要表明我要在某某间隙插入记录,如果不想幻读,别来这个区间读。或者,某某事务已经在这个区间加了锁,我就等待,避免造成幻读。也就是GAP锁会阻塞插入意向锁。
4. 事务隔离级别和行锁
快照读和当前读,快照读使用MVCC读取数据记录某一个版本数据,不需要加锁。当前读读取最新数据,需要对记录或者某一个查询范围加锁。(某个版本的数据不会变,当然不用加锁(如果不是Serializable级别,所有普通select都是mvcc读,无需加锁)。如果是当前读,存在多个事务同时操作,需要锁来保证)
InnoDB支持的隔离级别有:
Read Uncommited
可以读未提交记录
Read Committed (RC)
读取已提交数据。会存在幻读。
Repeatable Read (RR)
可重复读。当前读的时候,部分语句会加范围锁,保证当前读的可重复。
Serializable
可串行化。不存在快照读,所有读操作都会加锁。
5. 加锁分析
mysql加锁2个目的:
- 当前读,避免多个事务同时操作某一行数据
- 隔离级别要求,比如RR隔离级别,要求可以重复读,mysql还要求实现避免幻读(如果主从同步模式是row-Statement),会加间隙锁
5.1 准备环境
- 打开锁监控
set GLOBAL innodb_status_output=ON;
set GLOBAL innodb_status_output_locks=ON;
具体可以参考这篇文章:
http://www.ywnds.com/?p=9767 - 查看和修改隔离级别(需要的时候改动)
查看 select @@tx_isolation;
修改当前会话:set tx_isolation='repeatable-read'; - 查看和修改锁等待时间(打开会话后修改,便于观察)
查看 select @@innodb_lock_wait_timeout
设置 set innodb_lock_wait_timeout=1000; - 人为停止表的purge操作
flush tables tbname for export (会加表锁,没啥用,本来想让停止purge然后看看插入和删除操作加锁是否有区别)
可以google一把什么是purge操作(删除的记录并不会立刻消失,会有另外一个任务来进行清理) - 准备数据
create table t(c1 int primary key, c2 int, c3 int, c4 int, unique index i_c2(c2), index i_c3(c3));
insert into t values (10, 11, 12, 13), (20, 21, 22, 23), (30, 31, 32, 33), (40, 41, 42, 43);
5.2 加锁分析
无论select, update 还是delete都是要找到数据之后,才能够加锁。而mysql加锁是加在索引上,那么查询条件就是我们分析加锁的关键了,查询条件的不同会导致使用不同的索引。我们从查询条件的维度来开始分析吧。(如果不懂mysql索引结构和加锁逻辑,可以先查看如下文章补习下:《非常好的加锁逻辑分析》:http://hedengcheng.com/?p=771
《mysql索引介绍》:https://blog.csdn.net/u010558660/article/details/53414456)
5.2.1 查询条件为主键等值
- select * from t where c1 = 20 for update;
只需要在c1 = 20的主键记录上加X锁即可,加锁为LOCK_X|LOCK_REC_NOT_GAP(RC(read-committed)还是RR(repeatable-read)都一样) - update t set c4 = 12 where c1 = 20;(未更新索引列)
只需要在c1 = 20的主键记录上加X锁即可,加锁为LOCK_X|LOCK_REC_NOT_GAP(RC,RR一样。以下如果未说明不一样,就是一样) - update t set c2 = 12 where c1 = 20;(更新了索引列)
除了主键记录加X锁,还需要在c2的索引上加LOCK_X|LOCK_REC_NOT_GAP。这里细想以下,可能引起死锁。因为先获得主键X锁,然后才获得C2索引的X锁,如果另外一个事务(针对同一条记录),先获得C2索引的X锁,后获得主键X锁,就会引起死锁(后续会说明) - delete from t where c1 = 20;
对主键、各个索引对应的记录都要加X锁,LOCK_X|LOCK_REC_NOT_GAP。主键,C2,C3索引列都加锁
5.2.2 查询条件为主键范围
-
select * from t where c1 >= 20 for update;
1.1 RC隔离级别:会分别对c1 in (20, 30, 40)加锁LOCK_X|LOCK_REC_NOT_GAP
1.2 RR隔离级别:这里会对c1=20加X锁(LOCK_X|LOCK_REC_NOT_GAP),对c1=30, c1=40对应的行加exclusive next-key lock(LOCK_X|LOCK_ORDINARY),同时会对表示记录上界的’supremum’加exclusive next-key lock。这样做到阻塞其他事务对c1>=20的加锁操作
如上图所示:20行record_lock锁,30行next-key lock(锁住的是30和gap3间隙),40行和30行雷同(锁住40行和gap4间隙)。40行前面有一个gap5,这个是记录上界supremum(可以看做是无穷大)的next-key lock锁住的。
- update t set c2 = c2 + 1 where c1 >= 20;
2.1 RC隔离级别:会分别对c1 in (20, 30, 40)依次对主键行加X锁,对应的索引行做加X锁操作
2.2 RR隔离级别:对主键c1=20加X锁,i_c2索引行加X锁,然后对c1=30,c1=40的主键行加exclusive next-key lock(LOCK_X|LOCK_ORDINARY),同时对应的i_c2索引行加X锁,最后对表示记录上界的’supremum’加exclusive next-key lock。(与RC区别是,主键上面的GAP会被锁住)
5.2.3 查询条件为唯一索引等值
- update t set c4 = 12 where c2 = 21;(未更新索引列)
对唯一索引上数据加X锁(LOCK_X|LOCK_REC_NOT_GAP),然后对应的主键行也需要加X锁 - update t set c3 = 12 where c2 = 21; (更新了索引列)
依次对唯一索引数据、主键行、索引数据加X锁。 - delete from t where c2 = 21;
会对唯一索引数据加X锁,根据唯一索引找到主键行后,会再依次对主键行、唯一索引、索引数据加X锁
以上3小点,RC和RR隔离级别加锁行为一样。
5.2.4 查询条件为唯一索引范围
-
select * from t force index(i_c2) where c2 >= 21 for update;
1.1 RC隔离级别:对满足条件的唯一索引、主键记录加X锁
1.2 RR隔离级别:那么会对c2 in (21, 31, 41)分别加exclusive next-key lock,对应主键行加X锁,同时对i_c2上’supremum’ record加exclusive next-key lock。如下图所示:
c2索引上21,31,41行都是next-key lock(为什么21行要next-lock锁,理论上record-lock锁就应该可以了,这里是因为21行可能已经删除还没有purge,需要锁住间隙,我猜的)。 gap5也被锁住,是无穷大的next-key lock。
查
主键索引上,20,30,40record lock。 - update t force index (i_c2) set c4 = 1 where c2 >= 21;
与1加锁模式一样 - update t force index (i_c2) set c3 = 1 where c2 >= 21;(更新索引列)
与2模式一样,增加c3索引上面22,32,42记录锁 - delete from t where c2 >= 41;
RC隔离级别:c2索引满足条件的41记录record锁,对应的主键索引40行record锁,对应的c3索引42记录record锁
RR隔离级别:上述语句选择了i_c2索引,会对c2 = 41加exclusive next-key lock,对应主键行加X锁,i_c2,i_c3上数据行进行加X锁操作,对i_c2上’supremum’ record加exclusive next-key lock。
5.2.5 查询条件为非唯一索引
RC隔离级别: 与唯一索引相同
RR隔离级别:
- 查询条件为非唯一索引等值
1.1 select * from t where c3 = 22 for update;
会对c3 =22在i_c3索引上加exclusive next-key lock(LOCK_X|LOCK_ORDINARY),对应主键加X锁(LOCK_X|LOCK_REC_NOT_GAP),然后在下一条记录上加exclusive gap lock(LOCK_X|LOCK_GAP)。即该语句会锁定范围(11, 31)
1.2 update t set c4 = 2 where c3 = 22;
加锁与上述FOR UPDATE一致
1.3 update t set c2 = 2 where c3 = 22;(更新索引列)
除了上述锁,对c1 = 20对应的唯一索引(i_c2)行加X锁 - 查询条件为非唯一索引范围
这里加锁与唯一索引的当前读范围查询一致
5.2.6 查询条件上无索引
select * from t where c4 = 23 for update;
RC隔离级别:会依次对c1 in (10, 20, 30, 40)依次加X锁,分析是否满足条件,不满足即释放。为c1 = 10行加锁,不满足条件释放锁;c1=20加锁,满足条件,保留锁;c1=30加锁,不满足条件,释放;c1=40行加锁,不满足条件,释放
RR隔离级别:c1上的记录都加record锁,所有的gap都加间隙锁。这个表相当于被锁死了,只能快照读,其他的都干不了。(原因是,没办法确定要锁住哪一行或者哪个范围。因为c4=23可能出现在c1索引的任何位置,假设你锁住了20行(20行的c4等于23)和20行的间隙,有的会话可能直接将40行的c4改为了23,就会有引起数据污染(RR隔离级别,row-statement的bin log)。如果c4上存在索引,那就是有一个控制点,在这个点上锁记录或者锁范围都是有的放矢。)
5.2.7 Serializable 级别加锁分析
Serializable的加锁与RR隔离级别下一致,不同点是Serializable下普通SELECT语句查询也是当前读。例如下面语句:
select * from t where c1 = 20就会对c1=20的主键行加S锁(LOCK_S|LOCK_REC_NOT_GAP)。