MySQL 锁

178 阅读22分钟

MySQL 锁

1、锁的分类

  • 按互斥性划分
    • 共享锁(S锁)
    • 排他锁(X锁)
    • 共享排他锁(SX锁)
  • 按锁粒度划分
    • 全局锁:对整个mysql实例加锁,加锁后所有表只能读,不能改
    • 表级锁
      • 元数据锁(MDL)
      • 意向锁
      • 自增锁(Auto-Inc)
    • 行级锁
      • 记录锁(Record)
      • 间隙锁(Gap)
      • 临键锁(Next-key)
      • 插入意向锁(insert intention lock)
  • 按加锁思想划分
    • 乐观锁
    • 悲观锁
  • 按加锁方式划分
    • 显式锁:手动加锁
    • 隐式锁: 事务下自动加的锁

2、什么时候释放锁

MySQL绝大数时候在事务结束时释放锁,也有例外

  1. 自增锁,在语句结束时,释放锁\color{red}{自增锁,在语句结束时,释放锁}
  2. 不满足where检索条件,也会快速释放锁\color{red}{不满足where检索条件, 也会快速释放锁}

3、共享锁、排他锁

  • 共享锁:(S锁)shared lock

    • 行级S锁

    select ... from 表名 lock in share mode 显式锁

    • 表级S锁

    lock tables 表名 read

  • 排他锁:(X锁)exclusive lock

    • 行级X锁

    select ... from 表名 for update 显式锁
    除了手动显式加锁以外,update、delete也会触发行级X锁

    • 表级X锁

    lock tables 表名 write 行级S、X锁兼容性 |兼容性| S | X | |---|---|---| |S|兼容|不兼容| |X|不兼容|不兼容|

  • 共享排他锁(SX锁) MySQL5.7引入的新式锁,主要为了解决SMO(Structure modification opertion 索引页分裂、索引页删除等)带来的问题;此锁主要加在B+ tree上

兼容性SXSX
S兼容不兼容兼容
X不兼容不兼容不兼容
SX兼容不兼容兼容

3、MySQL全局锁

针对于整个mysql实例加锁,加锁之后,实例下所有表只能读,表数据以及表定义的修改均被禁止;一般用于数据库备份;

加锁 flush tables with read lock
释放锁 unlock tables

4、MySQL表级锁

4.1、 意向锁

先说结论,属于表级别锁;意向锁的引入主要是为了可以快速判断加表级别S、X锁时,表中的记录是否已有对应的行级锁,从而避免遍历整张表,提升性能。
加表级别S锁,需要判断表中的行记录是否有X锁,如果有则加表级S锁失败;同样加表级别X锁,需要判断表中的行记录是否有S锁、X锁,如果有加表级X锁失败;引入意向锁的概率后,在表中行记录加行级S锁时,同时会增加表级别IS锁,标记表中存在行级S锁;加行级X锁时,同样也会添加表级别IX锁,这样当添加表级别S锁,如果看到有IX锁,就明白无法获取到表级别S锁,需要阻塞等待;同理加表级别X锁时,如果看到IS、IX锁,也无法加锁成功,需要阻塞等待;避免遍历表中所有行记录。
表级别锁IS、S、IX、X的兼容性说明

兼容性ISIXSX
IS兼容兼容兼容不兼容
IX兼容兼容不兼容不兼容
S兼容不兼容兼容不兼容
X不兼容不兼容不兼容不兼容

4.2、 元数据锁(MDL锁)

MDL锁的引入主要为了解决一张表的DML和DDL操作一致性的问题;以student为例

create table student(
    id not null,
    name varchar(10),
    primary key(id)
);

目前只有一条记录

idname
1貂蝉
序号trx 100trx 200
begin;
begin;
select * from student where id = 1;
alter table drop column name;
commit;
select * from student where id = 1;
commit;

在RR级别下,③语句执行后,结果为 id=1,name=貂蝉;⑥语句后,结果为id=1;两次的结果不一致,这样就会触发不可重复读问题;
为了解决类似问题,引入了MDL锁,规定:

  1. 执行DML语句,会在表上加MDL读锁(MDL S锁)
  2. 执行DDL语句,会在表上加MDL写锁(MDL X锁) 上诉示例,③语句执行时,会先对student表加MDL读锁,再执行④语句时此时会对student尝试加MDL写锁,因为trx 100尚未结束,所以student表上MDL读锁仍在,从而会阻塞④语句获得MDL写锁,直至trx 100事务结束释放MDL读锁,才能继续执行。

4.3、 自增锁 (AUTO-INC锁)

自增锁顾名思义,专门为auto_increment的自增主键准备,用来控制自增主键的分配。
自增锁有两种模式:

  1. 采用AUTO-INC锁,执行insert插入时,①在表级别加上AUTO-INC锁->②为auto_increment修饰的自增列分配自增序列值->③执行insert插入->④释放表级别的AUTO-INC锁,至此其他insert语句才可以拿到AUTO-INC锁继续执行否则阻塞等待;
  2. 轻量锁,执行insert插入时,①在表级别加上轻量锁->②为auto_increment修饰的自增列分配自增序列值->③释放表级别轻量锁->④执行insert插入。 两者的差别在于轻量锁在获得自增序列值之后就可以释放,所以效率更高。 MySQL中通过innodb_autoinc_lock_mode来控制使用哪种方式: |innodb_autoinc_lock_mode|含义| |---|---| |0|传统模式,采用AUTO-INC锁| |1|连续模式,AUTO-INC锁、轻量锁混用, mysql8.0之前默认| |2|交错模式,全部使用轻量锁,MySQL8.0之后默认|
  1. insert-like:所有的插入语句,例如 insert、replace、insert ... select、replace ... select、load data等
  2. simple-insert:插入前就可以明确新增行数的语句,例如 insert、replace,不包括insert into on duplicate key update
  3. bulk-insert:插入前无法明确新增行数的语句,例如 insert ... select、replace ... select
  4. mixed-insert:混合插入 例如 insert into 表名(col1, col2) values(1, 'a'), (null, 'b');
  • 传统模式 该模式下,所有插入语句,都会加AUTO-INC锁;优点:可以确保auto_increment递增分配,且不会浪费;缺点:并发insert效率较低
  • 连续模式 该模式下,如果是simple-insert,采用轻量级锁;如果是bulk insert,采用AUTO-INC锁;
  • 交错模式 该模式下,所有插入语句,都采用轻量级锁;(如果时bulk insert这种无法提前确认插入记录行数的语句,就只能一条一条去争抢轻量级锁)
    此模式下最大的弊端:如果bin-log的“binlog-formar”选择的是statement,这种基于语句的复制同步方式,在在主从复制时,可能会导致同一记录行所分配的自增主键不一致。

(没找到比较好的解释答案)\color{red}{(没找到比较好的解释答案)} 我的个人理解,因为轻量级锁,只会锁住auto_increment的生成,从库在依据bin-log回放SQL,并不能完美复现主库当时主键分配顺序。
trx 1: insert into student select * from student_copy limit 10;
trx 2: insert into student values(xxx);
trx1先执行,执行过程中trx2开始执行,因为只会锁自增ID分配这一步,极大概率trx2会争抢到轻量级锁,形成如下顺序

自增主键事务
1trx1
2trx1
3trx2
4trx1
事务提交时,trx2先于trx1结束;最终回放时,trx1先执行,trx2后执行,那么从库的主键顺序就会跟主库不同。

5、MySQL行级锁

5.1、记录锁(record lock)

顾名思义,锁住一条记录, 不同条件区分S, X;
死锁日志分析关键字:rec but not gap, 例如lock_mode X lock rec but not gap

MySQL的锁均是加在索引上,如果没有主键或者索引,那么锁会加在隐藏自增列DB_ROW_ID上

5.2、 间隙锁(gap lock)

间隙锁引入的目的:为了解决幻读问题。以student表为例

create table student(
    id int not null,
    name varchar(10) not null,
    primary key(id)
);
idname
1貂蝉
2蔡文姬
7甄姬

如果想解决幻读问题,单凭记录锁已经显的力不从心,record lock无论加在id=2或者id=7,都无法阻止2-7中间插入新记录,所以就诞生了间隙锁, 锁住(4, 7)之间的间隙。(前后开区间),上诉示例,间隙锁加在id=7的聚簇索引上
死锁分析关键字:lock_mode X locks gap before rec

5.3、 临键锁(next-key lock)

临键锁相当于记录锁和间隙锁的合体(前开后闭)
死锁分析关键字:lock_mode X locks

5.4、 插入意向锁(insert intention lock)

插入意向锁,也是一种间隙锁形式的意向锁,当insert时,会检查当前插入的下一条记录上是否存在间隙锁或者临键锁,如果存在则insert语句会被阻塞等待,同时会生成一个插入意向锁。
横向为持有锁,纵向为请求获得的锁

兼容性记录锁间隙锁临键锁插入意向锁
记录锁冲突兼容冲突兼容
间隙锁兼容兼容兼容兼容
临键锁冲突兼容冲突兼容
插入意向锁兼容冲突冲突兼容

6、乐观锁

乐观锁:或者叫无锁模式,利用CAS的思想;举个例子: update stock = n, version = version + 1 where id = xx and version = version;
update order status = B where id = xx and status = A; 上诉两种都可以认为乐观锁的使用

7、悲观锁

悲观锁:一句话就是干啥先上锁

8、锁是怎么加上去的

以student表为例

create table student(
    id int not null,
    name varchar(10) not null,
    country varchar(10) not null,
    primary key(id),
    key idx_name(name)
);

表中数据

idnamecountry
1b白起
3g关羽
5l李牧
7c曹操
10d邓艾

8.1、RU/RC级

8.1.1、主键

8.1.1.1、精确
  • select ... lock in share mode/select ... for update select * from student where id = 3 lock in share mode; 因为通过主键查询,直接在id=3的聚簇索引上加S锁即可。image.png select ... for update 加锁过程与上诉相似,唯一差别就是加X记录锁
  • update
    • 不涉及二级索引列更新 加锁过程与select ... for update一致,只会对聚簇索引记录加锁;
    • 涉及二级索引列更新 update student set name = 'g关羽123' where id = 3;
      与select ... for update过程相似,差别在于先对定义聚簇索引加锁,在对涉及索引列加锁。
  • delete 与update涉及二级索引加锁过程一致,先对聚簇索引加锁,再对关联的二级索引加锁;
8.1.1.2、 范围
  • select ... lock in share mode/select ... for update select * from student where id <= 5;
  1. 从聚簇索引根部开始遍历,找到符合查询条件的第一条记录(id=1的记录),对该记录加S记录锁;
  2. 判断该记录是否符合索引下推中的条件;因为id <= 5,不符合索引下推条件,且走的是聚簇索引。
  3. 引擎层判断下是否符合范围查询的边界条件(MySQL规定聚簇索引中的记录需要判断是否符合范围查询边界条件),如果符合返回给server层继续处理,否则释放掉记录上加的锁,同时返回server层一个查询结束的标志;
  4. server层收到引擎层提供的信息,如果是查询结束标记,那就结束查询;否则在server层中继续判断记录是否符合非索引下推的剩余查询条件,如果符合条件返回记录给client端,否则释放该记录上加的锁;
  5. 依据该记录顺着单向链表取出下一条记录,对记录同样加S记录锁,然后依次执行2、3、4、5步骤。

依据上诉查询过程,上诉语句在边界值的下一个记录会经历加锁再快速解锁的历程

序号trx 1trx 2
begin;
begin;
select * from student where id <= 5 lock in share mode;
select * from student where id = 7 for update;

两个事务均可以正常执行,加锁示意如下image.png

序号trx 1trx 2
begin;
begin;
select * from student where id = 7 for update;
select * from student where id <= 5 lock in share mode;

此时会发现trx 1被阻塞等待,直至trx2事务提交或者回滚 select * from student where id >= 5;

依据聚簇索引根节点定位到符合查询条件的第一条记录(id = 5的记录),加上S记录锁;判断是否符合索引下推条件;引擎层判断是否符合范围查询边界;server层继续判断是否符合查询条件中非索引下推条件;根据记录单向链表依次往后找,重复上诉步骤,直至最后一条supermum记录,引擎层识别出为supermum记录,返回server层查询结束标记,且不会对supermum加锁。image.png select ... for update 加锁过程与上诉相似,唯一差别就是加X记录锁

  • update
    • 不涉及二级索引更新 加锁过程与select ... for update一致,只涉及聚簇索引加锁; update student set country = '楚' where id <= 3;
      我的理解,应该与上诉select * from student where id <= 5 lock in share mode;过程类似,逐步找到记录然后依次对聚簇索引加锁,对于边界值之后的一条记录“id=5”,也有个快速的加锁解锁过程, 但是实际sql验证居然没有??此处不知道如何解释,还是update到了边界值之后不会再往下找一条记录??\color{red}{但是实际sql验证居然没有??此处不知道如何解释,还是update到了边界值之后不会再往下找一条记录??} |序号|trx 1|trx 2| |---|---|---| |①|begin;| | |②| |begin;| |③|select * from student where id = 5 for update;| | |④| |update student set country = '楚' where id <= 3;| ④语句并没有如预期设想的被阻塞\color{red}{④语句并没有如预期设想的被阻塞}
    • 涉及二级索引更新 加锁过程与select ... for update相似,会先对聚簇索引加锁,然后再对更新列涉及的二级索引加锁。update student set name = 'g关云长' where id <= 3;加锁示意图如下:image.png
    • delete 与update涉及二级索引范围更新加过过程类似,先对聚簇索引加锁,再对关联的二级索引加锁;神奇的是此时聚簇索引会对边界值的下一条记录,快速先加锁再解锁,验证SQL如下\color{red}{神奇的是此时聚簇索引会对边界值的下一条记录,快速先加锁再解锁,验证SQL如下} |序号|trx 1|trx 2| |---|---|---| |①|begin;| | |②| |begin;| |③|select * from student where id = 5 for update;| | |④| |delete from student where id <= 3;| 此时会发现④语句发生阻塞等待,select * from information_schema.INNODB_LOCKS; image.png

8.1.2、二级索引

8.1.2.1、 精确
  • select ... lock in share mode/select ... for update select * from student where name = 'g关羽';
  1. 从index_name定位到name=g关羽 记录加上S记录锁;
  2. 因为返回为*,不满足索引覆盖,需要走一次回表,对对应的聚簇索引同样加上S记录锁; 总结就是先对idx_name记录加锁在对关联聚簇索引加锁,索引示意图如下image.png select ... for update 加锁过程与上诉相似,唯一差别就是加X记录锁。PS:此外还有一点最大的差别,如果是二级索引,哪怕符合索引覆盖,MySQL还是会做回表处理,也就是关联聚簇索引也是会被加锁\color{red}{PS:此外还有一点最大的差别,如果是二级索引,哪怕符合索引覆盖,MySQL还是会做回表处理, 也就是关联聚簇索引也是会被加锁}
序号trx 1trx 2
begin;
begin;
select name from student where name = 'g关羽' for update;
select * from student where id = 3 lock in share mode;
commit;
commit;

执行完第④步之后,会发现出现锁等待现象,select * from information_schema.INNODB_LOCKS image.png 在同一个聚簇索引上分别加上了S,X锁;从而佐证二级索引加X记录,哪怕是符合索引覆盖,也是会对关联聚簇索引加锁。 ??上诉没有找到比较好的资料,根据现象推断,充满了疑问??\color{red}{??上诉没有找到比较好的资料,根据现象推断,充满了疑问??}

  • update 加锁过程与select ... for update精确查询类似,先对二级索引加锁,再对关联的聚簇索引加锁;如果更新字段涉及到其他二级索引,那么推测加锁顺序应该是 二级索引->聚簇索引->其他二级索引
  • delete 加锁过程与update精确更新一致,先对二级索引加锁,再对聚簇索引加锁;涉及到其他二级索引,再对其他二级索引加锁。
8.1.2.2、范围
  • select ... lock in share mode/select ... for update select * from force index(idx_name) student where name <= 'c曹操';
  1. 从idx_name定位到第一条符合条件的记录(name = b白起),对该索引记录加S记录锁;
  2. 判断是否符合索引下推条件,此处符合索引下推条件;如果不满足范围边界条件,跳过到下一条,如果已经是查询的最后一条,返回给server层结束查询标识;(PS:不满足条件的记录并不会立即释放锁,等到事务结束统一释放\color{red}{PS:不满足条件的记录并不会立即释放锁,等到事务结束统一释放}
  3. 因为返回的是*,所以需要执行一次回表,到对应的聚簇索引中拿到完整的记录,返回给server层,并且对聚簇索引中该记录加锁;
  4. server层拿到记录后,判断是否为结束查询标识,如果不是,再判断非索引下推条件的剩余查询条件是否满足,如果不满足释放掉聚簇索引上的锁;
  5. 依据idx_name单向链表,取下一条索引记录,依次执行2、3、4步骤; 上诉语句索引加锁示例图image.png select * from student force index(idx_name) where name >= 'g关羽';
    与前面主键id>5过程类似,先在idx_name中定位到第一条记录,加S记录锁,然后再从聚簇索引中取相应记录,同样加S记录锁。直到走到idx_name中的supermum,识别出最后一条结束查询,且supermum上不会加锁。加锁示意图如下:image.png select ... for update 加锁过程与上诉相似,唯一差别就是加X记录锁
  • update 加锁过程与select ... for update范围查询类似,先对二级索引加锁,再对关联的聚簇索引加锁,如果更新字段涉及其他二级索引,再对其他二级索引加锁;

例如update student set country = '汉' where name <= 'c曹操';\color{red}{跟}select ... lock in share mode/for update;不同的是,边界值的下一记录貌似没有加锁\color{red}{不同的是,边界值的下一记录貌似没有加锁}

序号trx 1trx 2
begin;
begin;
update student set country = '汉' where name <= 'c曹操';
select * from student where name = 'd邓艾' lock in share mode;

语句④并未发生阻塞等待\color{red}{语句④并未发生阻塞等待}

  • delete 加锁过程与update范围更新一致,先对二级索引加锁,再对关联的聚簇索引加锁,如果涉及其他二级索引,再对其他二级索引加锁;

例如delete from student where name <= 'c曹操';\color{red}{跟}select ... lock in share mode/for update;不同的是,边界值的下一记录貌似没有加锁\color{red}{不同的是,边界值的下一记录貌似没有加锁}

序号trx 1trx 2
begin;
begin;
delete from student where name <= 'c曹操';
select * from student where name = 'd邓艾' lock in share mode;

语句④并未发生阻塞等待\color{red}{语句④并未发生阻塞等待}

8.1.3、无索引

8.1.3.1、精确
  • select ... lock in share mode/select ... for update select * from student where country = '蜀' lock in share mode;
    因为无索引,只能全表扫描,MySQL如下操作:
  1. 从聚簇索引取第一条记录,加S记录锁,返回给server层;
  2. server层判断是否符合查询条件,如果成立返回给客户端;如果不符合,释放锁;
  3. 从聚簇索引取下一条,重复上诉步骤;
  • update 与select ... lock in share mode/for update, 加锁过程一致,依次遍历聚簇索引,加S记录锁,判断不符合则释放锁;如果更新列涉及到其他二级索引,再对关联二级索引加锁。
  • delete 与上诉update加锁一致
8.1.3.2、 范围
  • select ... lock in share mode/select ... for update 与无索引精确select一致
  • update 与无索引精确update一致
  • delete 与无索引精确delete一致

8.1.4、insert

8.1.4.1、主键insert

主键插入流程图image.png

8.1.4.2、唯一索引insert

插入流程与主键相似, 差别在于出现重复记录,请求的是S临键锁image.png

8.2、RR级别

8.2.1、主键

8.2.1.1、精确
  • select ... lock in share mode/for update select * from student where id = 3 lock in share mode;加锁示意图image.png 与RU/RC级别一致,都是在对应聚簇索引上加S记录锁;
    PS:如果记录不存在,RR级别和RC处理则不相同\color{red}{PS:如果记录不存在,RR级别和RC处理则不相同}
    select * from student where id = 2 lock in share mode; RR级别下因为要解决幻读的问题,所以需要对下一条记录上加GAP锁,加锁示意图如下image.png而RC则不需要加任何锁;
  • update
    • 不涉及二级索引列更新 与select ... lock in share mode/for update相似,对于关联聚簇索引加X记录锁;如果不存在也会对下一条记录加GAP锁(superman好像加的是next-key锁)
    • 涉及二级索引列更新 如果涉及到二级索引列更新,那么在聚簇索引加X记录锁,再对涉及得二级索引加X记录锁;
  • delete 对聚簇索引加X记录锁,再对涉及得二级索引加记录锁;
8.2.1.2、范围
  • select ... lock in share mode/for update select * from student where id <= 3 lock in share mode,RR级别下与RC不同之处会对于符合记录加next-key锁,且边界记录的下一条记录也会加next-key锁(mysql8版本好像对于边界的下一条不会加锁\color{red}{mysql 8版本好像对于边界的下一条不会加锁}),加锁示意图如下image.png select * from student where id >= 7 lock in share mode;与上一语句不同点在于边界值的处理,id=7加S记录锁,id=10,supermum加next-key锁,加锁示意图如下image.png
  • update
    • 更新列不涉及二级索引 与select ... for update 加锁情况一致;
    • 更新列涉及二级索引 聚簇索引的加锁情况与select ... for update一致,同时对关联的二级索引加X记录锁;例如:update student set name = contact(name, '1') where id >= 7; 加锁示意图如下:image.png
  • delete 与"更新列涉及二级索引"的update,加锁一致

8.2.2、唯一二级索引

将idx_name调整为唯一二级索引

8.2.2.1、精确
  • select ... lock in share mode/for update select * from student name = 'c曹操' lock in share mode; 唯一索引,RR级别加锁与RC很相似,先对唯一索引加S记录锁,再对关联聚簇索引加S记录锁;加锁示意图image.png select * from student where name = 'f法正' lock in share mode; 对于不存在的记录,RR级别会在下一记录上加上Gap锁(这样可以避免幻读),因为不存在的记录,所以不会在聚簇索引加锁。加锁示意图如下:image.png
  • update 与select ... for update,加锁类似;都是先对唯一索引加X记录锁,再对聚簇索引加X记录锁(如果再涉及其他二级索引,再对涉及的二级索引加X记录锁)
  • delete 与update加锁类似
8.2.2.2、范围
  • select ... lock in share mode/for update select * from student where name <= 'c曹操' lock in share mode;唯一索引加锁过程与主键范围查询类似,然后再对对应聚簇索引加锁。加锁示意图如下 image.png select * from student where name >= 'g关羽' lock in share mode;唯一索引加锁过程与主键范围查询类似(差别在于主键范围查询,边界值加的是记录锁,而此处加的也是nextkey\color{red}{差别在于主键范围查询,边界值加的是记录锁,而此处加的也是next-key锁}此处很神奇),再对对应聚簇索引加锁。加锁示意图如下 image.png
  • update
    • 更新列不涉及其他二级索引 与select ... for update;加锁类似
    • 更新列涉及其他二级索引 与select ... for update;加锁类似,再对其他二级索引加X记录锁
  • delete 与“更新列涉及其他二级索引”加锁类似

8.2.3、普通二级索引

将idx_name调整为普通索引

8.2.3.1、精确
  • select ... lock in share mode/for update select * from student where name = 'c曹操' lock in share mode; 此处与主键和唯一索引的加锁差异较大,name='c曹操' 加S next-key锁,同时下一条记录加gap锁;加锁示意图如下:image.png

为啥在本记录加了记录锁之后,才需要对本记录加gap锁?我的理解还是为了解决幻读问题,因为普通二级索引允许重复值,且MySQL加锁加在索引记录上,只有把前后的间隙均锁住,才能避免幻读的发生;

select * from student where name = 'f法正' lock in share mode; 对下一条记录加gap锁,加锁示意图如下 image.png

  • update
    • 更新列不涉及其他二级索引 与select ... for update加锁一致
    • 更新列涉及其他二级索引 与select ... for update加锁一致,在此基础上再对其他二级索引加X记录锁
  • delete 与“更新列涉及其他二级索引”的update 加锁一致
8.2.3.2、范围
  • select ... lock in share mode/for update select * from student where name <= 'c曹操' lock in share mode;加锁示意图 image.png select * from student where name >= '关羽' lock in share mode; 加锁示意图 image.png
  • update
    • 更新列不涉及其他二级索引 与select ... for update 加锁一致
    • 更新列涉及其他二级索引 与select ... for update 加锁类似,先对索引加临键锁,再对聚簇索引加记录锁,再对涉及的其他二级索引加记录锁
  • delete 与“更新列涉及其他二级索引”update 加锁一致

8.2.4、无索引

移除idx_name

8.2.4.1、精确
  • select ... lock in share mode/for update select * from student where name = 'c曹操' lock in share mode;,加锁示意图 image.png

对于无索引的查询,只能逐条遍历索引,因为RR级别需要解决幻读的问题,只能将所有间隙都锁住,避免在一个事务未结束时,其他事务新增记录,带来幻读问题 对于不存在的记录查询,加锁与上面一致,也是会锁住所有记录
回顾下RC级别无索引\color{red}{回顾下RC级别无索引},也会对所有聚簇索引加锁,但是如果不符合会立马释放掉,因为RC没有禁止幻读的诉求。

  • update
    • 更新列不涉及其他二级索引 与select ... for update加锁一致
    • 更新列涉及其他二级索引 举措索引加锁与select ... for update一致,再对涉及二级索引加记录锁;
  • delete 与“更新列涉及其他二级索引”的update加锁过程一致
8.2.4.2、范围
  • select ... lock in share mode/for update
  • update
  • delete RR级别无索引条件下,与精确场景下加锁类似

8.2.5、insert

8.2.5.1、主键insert

主键插入流程图image.png 对于主键插入,RC/RR差别不大

8.2.5.2、唯一索引insert

插入流程与主键相似, 差别在于出现重复记录,请求的是S临键锁

本事务删除的记录,再次插入也会认为是重复记录,触发S临键锁\color{red}{本事务删除的记录,再次插入也会认为是重复记录,触发S临键锁} image.png 唯一索引的插入,RR与RC加锁也基本一致

9、死锁

MySQL死锁现象,比较好理解,两个事务相互等待各自持有的锁,就会导致死锁; MySQL解决死锁的两种方式:

  • 锁超时机制:事务在等待锁时,超出一定时间内自动放弃锁等待;
  • 外力干预解除死锁:MySQL通过wait-for graph主动检测死锁,强制将导致死锁的某个事务结束;

9.1、锁超时机制

MySQL中默认锁等待超时时间=50s

9.2、死锁检测机制

  • wait-for graph 何时触发 mysql 在事务请求锁无法立即获取而需要进入等待时,会触发wait-for graph算法,来检测是否存在死锁;如果存在,那么会选择将其中的小事务进行回滚,来解决死锁问题;
  • wait-for graph 怎么检测死锁 MySQL会分别记录事务所持有锁信息以及所需要等待的锁; 锁的信息链表:每个锁持有的事务是谁; 事务等待链表:每个阻塞等待事务等待的锁是谁; image.png 以上图为例,会发现T1->T2->T3->T1,锁的等待会形成一个闭环,那么MySQL就判断发生了死锁,会选择其中的小事务进行回滚,解决死锁;
  • 如何判定小事务? MySQL更改行数据会产生相应的undo-log,undo-log少的为小事务,如果undo-log量相同,那么选择造成死锁的事务;

9.3、常见的几种死锁情况

参见《Mysql常见死锁分析》