【MySQL系列5】深入分析MySQL中锁并详解锁解决幻读问题

但是这样就会有一个问题,假如事务A给t表其中一行数据上锁了,这时候事务B想给t表上一个表锁,这时候怎么办呢?事务B怎么知道t表有没有行锁的存在,如果采用全表遍历的情况,当表中的数据很大的话,加锁都要加半天,所以MySQL中就又引入了意向锁

意向锁


意向锁为表锁,分为两种类型,分为:意向共享锁(Intention Shared Lock)和意向排他锁(Intention Exclusive Lock),这两种锁又分别可以简称为IS锁和IX锁。

意向锁是MySQL自己维护的,用户无法手动加意向。

意向锁有两大加锁规则:

  • 当需要给一行数据加上S锁的时候,MySQL会先给这张表加上IS锁。

  • 当需要给一行数据加上X锁的时候,MySQL会先给这张表加上IX锁。

这样的话上面的问题就迎刃而解了,当需要给一张表上表锁的时候,只需要看这张表是否有对应的意向锁就可以了,无需遍历整张表。

各种锁的兼容关系

=====================================================================

下面这张图是InnoDB引擎中各种锁的兼容关系,参考自官网:

| | X | IX | S | IS |

| — | — | — | — | — |

| X | 互斥 | 互斥 | 互斥 | 互斥 |

| IX | 互斥 | 共享 | 冲突 | 共享 |

| S | 互斥 | 互斥 | 共享 | 共享 |

| IS | 互斥 | 共享 | 共享 | 共享 |

锁到底锁的是什么

=====================================================================

建立以下两张表,并初始化5条数据,注意test表有2个索引而test2没有索引:

CREATE TABLE test (

id int(11) NOT NULL,

name varchar(50) DEFAULT NULL,

PRIMARY KEY (id),

KEY NAME_INDEX (name)

) ENGINE=InnoDB DEFAULT CHARSET=utf8;

INSERT INTO test VALUE(1,‘张1’);

INSERT INTO test VALUE(5,‘张5’);

INSERT INTO test VALUE(8,‘张8’);

INSERT INTO test VALUE(10,‘张10’);

INSERT INTO test VALUE(20,‘张20’);

CREATE TABLE test2 (

id varchar(32) NOT NULL,

name varchar(32) DEFAULT NULL

) ENGINE=InnoDB DEFAULT CHARSET=utf8;

INSERT INTO test2 VALUE(1,‘张1’);

INSERT INTO test2 VALUE(5,‘张5’);

INSERT INTO test2 VALUE(8,‘张8’);

INSERT INTO test2 VALUE(10,‘张10’);

INSERT INTO test2 VALUE(20,‘张20’);

举例猜测


在行锁中,假如我们对一行记录加锁,那么到底是把什么东西锁住了,我们来看下面两个例子:

举例1(操作test表):

| 事务A | 事务B |

| — | — |

| BEGIN; | |

| SELECT * FROM test WHERE id=1 FOR UPDATE; | |

| | SELECT * FROM test WHERE id=1 FOR UPDATE;

阻塞 |

| | SELECT * FROM test WHERE id=5 FOR UPDATE;

加锁成功 |

| COMMIT;(释放锁) | |

| | SELECT * FROM test WHERE id=1 FOR UPDATE;

加锁成功 |

举例2(操作test2表):

| 事务A | 事务B |

| — | — |

| BEGIN; | |

| SELECT * FROM test2 WHERE id=1 FOR UPDATE; | |

| | SELECT * FROM test2 WHERE id=1 FOR UPDATE;

阻塞 |

| | SELECT * FROM test2 WHERE id=5 FOR UPDATE;

阻塞 |

| COMMIT;(释放锁) | |

| | SELECT * FROM test2 WHERE id=1 FOR UPDATE;

加锁成功 |

从上面两个例子我们可以发现,test表好像确实是锁住了id=1这一行的记录,而test2表好像不仅仅是锁住了id=1这一行记录,实际上经过尝试我们就知道,test2表是被锁表了,所以其实MySQL中InnoDB锁住的是索引,当没有索引的时候就会锁表

接下来再看一个场景:

| 事务A | 事务B |

| — | — |

| BEGIN; | |

| SELECT * FROM test WHERE name=‘张1’ FOR UPDATE; | |

| | SELECT name FROM test WHERE name=‘张1’ FOR UPDATE;

阻塞 |

| | SELECT id FROM test WHERE id=1 FOR UPDATE;

阻塞 |

| COMMIT;(释放锁) | |

| | SELECT id FROM test WHERE id=1 FOR UPDATE;

加锁成功 |

这个例子中我们是把name索引锁住了,然后我们在事务B中通过主键索引只查id,这样就用到name索引了,但是最后发现也被阻塞了。所以我们又可以得出下面的结论,MySQL索引不但锁住了辅助索引,还会把辅助索引对应的主键索引一起锁住

到这里,可能有人会有怀疑,那就是我把辅助索引锁住了,但是假如加锁的时候,只用到了覆盖索引(关于覆盖索引的相关知识,想要详细了解的,可以点击这里),然后我再去查主键会怎么样呢?

接下来让我们再验证一下:

| 事务A | 事务B |

| — | — |

| BEGIN; | |

| SELECT name FROM test WHERE name=‘张1’ FOR UPDATE; | |

| | SELECT name FROM test WHERE name=‘张1’ FOR UPDATE;

阻塞 |

| | SELECT * FROM test WHERE id=1 FOR UPDATE;

阻塞 |

| | SELECT id FROM test WHERE id=1 FOR UPDATE;

阻塞 |

| COMMIT;(释放锁) | |

| | SELECT id FROM test WHERE id=1 FOR UPDATE;

加锁成功 |

我们可以看到,就算只是用到了辅助索引加锁,MySQL还是会把主键索引锁住,而主键索引的B+树叶子节点中,又存储了整条数据,所以查询任何字段都会被锁定。

到这里,我们可以明确的给锁到底锁住了什么下结论了:

结论


InnoDB引擎中,锁锁的是索引:

  • 假如一张表没有索引,MySQL会进行锁表(其实锁住的是隐藏列ROWID的主键索引)

  • 假如我们对辅助索引加锁,那么辅助索引所对应的主键索引也会被锁住

  • 主键索引被锁住,实际上就等于是整条记录都被锁住了(主键索引叶子节点存储了整条数据)

行锁的算法

==================================================================

上一篇介绍事务的时候我们提到了,MySQL通过加锁来防止了幻读,但是如果行锁只是锁住一行记录,好像并不能防止幻读,所以行锁锁住一条记录的话只是其中一种情况,实际上行锁有三种算法:记录锁(Record Lock),间隙锁(Gap Lock)和临键锁(Next-Key Lock),而之所以能做到防止幻读,正是临键锁起的作用。

记录锁(Record Lock)


记录锁就是上面介绍的,当我们的查询能命中一条记录的时候,InnoDB就会使用记录锁,锁住所命中的这一行记录。

间隙锁(Gap Lock)


当我们的查询没有命中记录的时候,这时候InnoDB就会加上一个间隙锁。

| 事务A | 事务B |

| — | — |

| BEGIN; | |

| SELECT * FROM test WHERE id=1 FOR UPDATE; | |

| | INSERT INTO test VALUE (2,‘张2’);

阻塞 |

| | INSERT INTO test VALUE (3,‘张3’);

阻塞 |

| | SELECT * FROM test WHERE id=2 FOR UPDATE;

加锁成功 |

| COMMIT;(释放锁) | |

从上面的例子中,我们可以得出结论:

  • 间隙锁与间隙锁之间不冲突,也就是事务A加了间隙锁,事务B可以在同一个间隙中加间隙锁。(之所以会用到间隙锁就是没有命中数据的时候,所以并没有必要去阻塞读,也没有必要阻塞其他事务对同一个间隙加锁)

  • 间隙锁主要是会阻塞插入操作

间隙是如何确定的

test表中有5条记录,主键值分别为:1,5,8,10,20。那么就会有如下六个间隙:

(-∞,1),(1,5),(5,8),(8,10),(10,20),(20,+∞)

而假如主键不是int类型,那么就会转化为ASCII码之后再确定间隙。

临键锁(Next-Key Lock)


临键锁就是记录锁和间隙锁的结合。当我们进行一个范围查询,不但命中了一条或者多条记录,且同时包括了间隙,这时候就会使用临键锁,临键锁是InnoDB中行锁的默认算法。

注意了,这里仅针对RR隔离级别,对于RC隔离级除了外键约束和唯一性约束会加间隙锁,没有间隙锁,自然也就没有了临键锁,所以RC级别下加的行锁都是记录锁,没有命中记录则不加锁,所以RC级别是没有解决幻读问题的

临键锁在以下两个条件时会降级成为间隙锁或者记录锁:

  • 当查询未命中任务记录时,会降级为间隙锁。

  • 当使用主键或者唯一索引命中了一条记录时,会降级为记录锁。

| 事务A | 事务B |

| — | — |

| BEGIN; | |

| SELECT * FROM test WHERE id>=2 AND id<=6 FOR UPDATE; | |

| | |

| | INSERT INTO test VALUE (2,‘张2’);

阻塞 |

| | INSERT INTO test VALUE (6,‘张6’);

阻塞 |

| | INSERT INTO test VALUE (8,‘张8’);

阻塞 |

| | SELECT * FROM test WHERE id=8 FOR UPDATE;

阻塞 |

| | INSERT INTO test VALUE (9,‘张9’);

插入成功 |

| COMMIT;(释放锁) | |

上面这个例子,事务A加的锁跨越了(1,5)和(5,8)两个间隙,且同时命中了5,然后我们发现我们对id=8这条数据进行操作也阻塞了,但是9这条记录插入成功了。

临键锁加锁规则

临键锁的划分是按照左开右闭的区间来划分的,也就是我们可以把test表中的记录划分出如下区间:(-∞,1],(1,5],(5,8],(8,10],(10,20],(20,+∞)。

那么临键锁到底锁住了哪些范围呢?

临键锁中锁住的是最后一个命中记录的 key 和其下一个左开右闭的区间

那么上面的例子中其实锁住了(1,5]和(5,8]这两个区间。

临键锁为何能解决幻读问题

临键锁为什么要锁住命中记录的下一个左开右闭的区间?答案就是为了解决幻读。

我们想一想上面的查询范围id>=2且id<=6,如果我们事务A只锁住了(1,5]这个区间,假如这时候事务B插入一条数据id=6,那么事务A再去查询,就会多出来了一条记录id=6,就会出现了幻读,所以我把你下一个区间5,10]也给锁住,就可以避免了幻读。

当然,其实如果我们执行的查询刚好是id>=2且id<=5,那么就算只锁住了(1,5],同样能避免幻读问题,只是我们要考虑到查询范围的最大值没有命中记录的情况,而锁住了下一个区间,可以确保不论是哪种范围查询,都可以避免幻读的产生。

在我们使用锁的时候,有一个问题是需要注意和避免的,我们知道,排它锁有互斥的特性。一个事务持有锁的时候,会阻止其他的事务获取锁,这个时候会造成阻塞等待,那么假如事务一直等待下去,就会一直占用CPU资源,所以,锁等待会有一个超时时间,在InnoDB引擎中,可以通过参数:innodb_lock_wait_timeout查询:

SHOW VARIABLES LIKE ‘innodb_lock_wait_timeout’;

默认超时时间是50s,超时后会自动释放锁回滚事务。但是我们想一下,假如事务A在等待事务B释放锁,而事务B又在等待事务A释放锁,这时候就会产生一个等待环路了,而这种情况是无论等待多久都不可能会获取锁成功的,所以是没有必要去等50s的,这种形成等待环路的现象又叫做死锁。

死锁(Dead Lock)

==========================================================================

什么是死锁


死锁是指的两个或者两个以上的事务在执行过程中,因为争夺锁资源而造成的一种互相等待的现象。

| 事务A | 事务B |

| — | — |

| BEGIN; | |

| SELECT * FROM test WHERE id=10 FOR UPDATE; | |

| | BEGIN; |

| | SELECT * FROM test WHERE id=20 FOR UPDATE; |

| SELECT * FROM test WHERE id=20 FOR UPDATE; | |

| | SELECT * FROM test WHERE id=10 FOR UPDATE; |

| | ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction |

| 查询出结果 | |

在这里插入图片描述

我们可以看到,发生死锁之后就会立刻回滚,而不会漫无目的的去等待50s之后超时再回滚事务,那么MySQL是如何知道产生了死锁的,是如何检测死锁的发生呢?

死锁的检测


目前数据库大部分采用wait-for graph(等待图)的方式来进行死锁检测,InnoDB引擎也是采用这种方式来检测死锁。数据库中会记录两种信息:

  • 锁的信息链表

  • 事务的等待链表

wait-for graph 算法会根据这两个信息构建一张图,当图中存在回路,则证明存在死锁:

如下图中,t1和t2之间存在回路,这就证明t1和t2事务之间存在死锁

在这里插入图片描述

死锁的避免


  • 尽量将长事务拆分成多个小事务

  • 查询时避免没有where条件语句查询,并尽可能使用索引查询

  • 可以的话尽量使用等值查询

锁信息查询

==================================================================

InnoDB在information_schema库下提供了3张表供我们查询并排查事务和锁相关问题。

INNODB_TRX


记录了当前在InnoDB中执行的每个事务的信息,包括事务是否在等待锁、事务何时启动以及事务正在执行的SQL语句(如果有的话)。

| 列名 | 含义 |

| — | — |

| trx_id | InnoDD引擎中的事务的唯一ID |

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数Java工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
img
img
img
img
img
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新

如果你觉得这些内容对你有帮助,可以添加V获取:vip1024b (备注Java)
img

写在最后

还有一份JAVA核心知识点整理(PDF):JVM,JAVA集合,JAVA多线程并发,JAVA基础,Spring原理,微服务,Netty与RPC,网络,日志,Zookeeper,Kafka,RabbitMQ,Hbase,MongoDB,Cassandra,设计模式,负载均衡,数据库,一致性哈希,JAVA算法,数据结构,加密算法,分布式缓存,Hadoop,Spark,Storm,YARN,机器学习,云计算…

image

3610)]
[外链图片转存中…(img-CNJes62v-1711991313611)]
[外链图片转存中…(img-lLpgNwvm-1711991313611)]

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新

如果你觉得这些内容对你有帮助,可以添加V获取:vip1024b (备注Java)
[外链图片转存中…(img-ehnWgzhW-1711991313612)]

写在最后

还有一份JAVA核心知识点整理(PDF):JVM,JAVA集合,JAVA多线程并发,JAVA基础,Spring原理,微服务,Netty与RPC,网络,日志,Zookeeper,Kafka,RabbitMQ,Hbase,MongoDB,Cassandra,设计模式,负载均衡,数据库,一致性哈希,JAVA算法,数据结构,加密算法,分布式缓存,Hadoop,Spark,Storm,YARN,机器学习,云计算…

[外链图片转存中…(img-GEwPuSKA-1711991313612)]

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值