针对请求加共享、排它锁的原因在于,读请求天生是幂等性的,不论你读多少次数据不会发生变化,所以给读请求加上锁就应该为共享锁。 不然怎么保证它的特点呢?
而写请求,本身就需对数据进行修改,所以就需要排它锁来保证数据修改的一致性。
吒吒辉:?如果按照锁的颗粒度划分看,就有表锁和行锁
- 表锁:
是MySQL中最基本的锁策略,并且是开销最小的策略。并发处理较少。表锁由MySQL服务或存储引擎管理。多数情况由服务层管理,具体看SQL操作。
例如:服务器会为诸如?ALTER TABLE 之类的语句使用表锁
,而忽略存储引擎的锁。
加锁机制:
它会锁定整张表。一个用户在对表进行写操作(插入、删除、更新等)前,需要先获得写锁,这会阻塞其他用户对该表的所有读写操作。只有没有写锁时,其他用户才能获取到读锁。
- 行锁:
锁定当前访问行的数据,并发处理能力很强。但锁开销最大。具体视行数据多少决定。由innoDB存储引擎支持。
- 页级锁:
页级锁是 MySQL 中锁定粒度介于行级锁和表级锁中间的一种锁。表级锁速度快,但冲突多,行级冲突少,但速度慢。因此,采取了折衷的页级锁,一次锁定相邻的一组记录。由BDB 存储引擎管理页级锁。
面试官:?为啥是表锁开销小,而不是行锁呢? 毕竟表锁锁定是整张表
吒吒辉:?表锁锁定的是表没错,但它不是把表里面所有的数据行都上锁,相当于是封锁了表的入口,这样它只是需要判断每个请求是否可以获取到表的锁,没有就不锁定。
而行锁是针对表的每一行数据,数据量一多,锁定内容就多,故开销大。 但因它颗粒度小,锁定行不会影响到别的行。所以并发就高。而如果表锁在一个入口就卡死了,那整体请求处理肯定就会下降。
面试官:?我记得行锁里面有几种不同的实现方式,你知道吗?
您可真贴心啊,替我考虑这么多,大佬都是这么心比针细? 我要是说不知道,你老是不是又准备给出穿小鞋啦。强忍内心啃人的冲动
ps:读懂图,说明你有故事
吒吒辉:?innodb虽支持行锁,但锁实现的算法却和SQL的查询形式有关系:
- Record Lock(记录锁):单个行记录上的锁。也就是我们日常认为的行锁。由
<br/>where =<br/>
的形式触发
- Gap Lock(间隙锁):间隙锁,锁定一个范围,但不包括记录本身(它锁住了某个范围内的多个行,包括根本不存在的数据)。
GAP锁的目的,是为了防止事务插入而导致幻读的情况。该锁只会在隔离级别是RR或者以上的级别内存在。间隙锁的目的是为了让其他事务无法在间隙中新增数据。 SQL里面用 where >、>=等范围条件触发,但会根据锁定的范围内,是否包含了表中真实存在的记录进行变化,如果存在真实记录就会进化为 临建锁。反之就为间隙所。
- Next-Key Lock(临键锁):它是记录锁和间隙锁的结合,锁定一个范围,并且锁定记录本身。对于行的查询,都是采用该方法,主要目的是解决幻读的问题。next-key 锁是InnoDB默认的。是一个左开右闭的规则
- IS锁:意向共享锁、Intention Shared Lock。当事务准备在某条记录上加S(读)锁时,需要先在表级别加一个IS锁。
- IX锁:意向排它锁、Intention Exclusive Lock。当事务准备在某条记录上加X(写)锁时,需要先在表级别加一个IX锁。
面试官:?那这个东西是怎么实现的?
t(id PK, name KEY, sex, flag);
表中有四条记录:
1, zhazhahui, m, A
3, nezha, m, A
5, lisi, m, A
9, wangwu, f, B
- 记录锁
select * from t where id=1 for update;
锁定 id =1的记录
- 间隙锁
select * from t where id > 3 and id < 9 ;
锁定(3,5],(5,9)范围的值,因为当前访问3到9的范围记录,就需要锁定表里面已经存在的数据来解决幻读和不可重复读的问题
- 临建锁
select * from t where id >=9 ;
会锁定 [9,+∞) 。查询会先选中 9 号记录,所以锁定范围就以9开始到正无穷数据。
面试官:?那意向排它、共享锁呢?是怎么个内容
吒吒辉:?意向排它锁和意向共享锁,是针对当前SQL请求访问数据行时,会提前进行申请访问,如果最终行锁未命中就会退化为该类型的表锁。
面试官:?那有这个意向排它锁有什么好处呢?
吒吒辉:?可提前做预判,每次尝试获取行锁之前会检查是否有表锁,如果存在就不会继续申请行锁,从而减少锁的开销。从而整个表就退化为表锁。
面试官:?那你动手给我演示下每个场景
嗯。。。(瞳孔放大2倍)我这不说的很明白吗?
难道故意和作对,这是干嘛啊。欺负人嘛不是
只见那面试官忽然翘起来二郎腿,还有节拍的抖动着腿,看向我。一看就是抖音整多了
哎,没办法 官大以及压死人。打碎了牙齿自己咽。你给我看细细看好了,最好眼睛都别眨
吒吒辉:?因为锁就是解决事务并发的问题,所以记录锁就不演示了,直接游荡在间隙和临建锁里面。
建立语句:
CREATE TABLE `t1` (
`id` int(10) NOT NULL AUTO_INCREMENT,
`name` varchar(64) COLLATE utf8mb4_general_ci DEFAULT NULL,
`age` tinyint(3) unsigned DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
表数据:
间隙锁:
- 关闭 MySQL 默认的事务自动提交机制。关闭前:
- 关闭后:
加锁:
直接插入 >8 的数据就阻塞,都会上锁。为的就解决插入新数据而导致幻读。
【啊!幻读不知道呀。下篇文章给大家安排上】
面试官:?你这条件不是>=8吗? 那等于8呢? 被吃辣?
吒吒辉:?别着急嘛,这不还没说完吗。为什么不指定8呢?
因为?>=8?的条件会从间隙锁升级为临建锁,因为你条件里面包含了 8 这个真实存在的数据。所以会把它锁起来。如下:
所以,最终的行锁会和SQL语句的条件触发有关系,一旦范围查询包含了数据库里面真实存在数据,就会升级为临建锁。不要问我为什么? 看前面的定义
面试官独白:这小伙多少看来还有有点货,不错。此刻面试官露出一丝笑容。殊不知他内心又开酝酿起了新的想法。就等我入瓮
面试官:?那什么场景下行锁不会生效呢?锁 锁定的又是什么?
此刻,我呆了,这都什么跟什么啊。不带这么玩的吧。天杀的,净使坏
锁的触发机制
吒吒辉:
innodb的行锁是根据索引触发,如果没有相关的索引,那行锁将会退化成表锁(即锁定整个表里的行)。
而?锁?锁定的是索引即索引树里面的数据库字段的值。
- id为主键索引字段。
- 给 age 字段上锁
- age 字段没索引,退化成表锁。直接查询将失败。
有索引,用索引字段查询可得数据,其余字段查询将失败。因为获取不到行锁,只能等待。而锁定的是索引,故此其它用其它索引值查询能拿查询数据
- 索引字段上锁
- 索引当前字段锁定,用其余索引字段可查询
- 不是索引字段都差不到。
面试官:?你前面说到的锁可以解决事务并发,然而MVCC也是用于解决并发,那干嘛还用锁来呢?你给说说
吒吒辉:?通过MVCC可以解决脏读、不可重复读、幻读这些读一致性问题,但实际上这只是解决了普通select语句的数据读取问题。
事务利用MVCC进行的读取操作称之为快照读,所有普通的SELECT语句在READ COMMITTED、REPEATABLE READ隔离级别下都算是快照读。
除了快照读之外,还有一种是锁定读,即在读取的时候给记录加锁,在锁定读的情况下依然要解决脏读、不可重复读、幻读的问题。
比如:如果 1 4 7 9 的数据。如果条件为 where > 4 的,那如果不锁定到 (4,7] (7,9],(9,+∞)。那势必就会早幻读,不可重复读的问题。
ps:不重复读?脏读是如何产生的?
死锁
面试官:?那你说下数据库的死锁是个什么情况?
吒吒辉:?死锁是指两个或多个事务在同一资源上相互占用,并请求锁定对方占用的资源,从而导致恶性循环。
当事务试图以不同的顺序锁定资源时,就可能产生死锁。多个事务同时锁定同一个资源时也可能会产生死锁。
一般可通过死锁检测和死锁超时机制来解决该问题。
死锁检查:
像InnoDB存储引擎,就能检测到死锁的循环依赖,并立即返回一个错误。否则死锁会导致出现非常慢的查询。通过参数 innodb_deadlock_detect 设置为on,来开启。
超时机制:
就是当查询的时间达到锁等待超时的设定后放弃锁请求。InnoDB目前处理死锁的方法是,将持有最少行级排他锁的事务进行回滚(这是相对比较简单的死锁回滚算法)。
可通过配置参数 innodb_lock_wait_timeout 用来设置超时时间。如果有些用户使用哪种大事务,就设置锁超时时间大于事务执行时间。
但这种情况下死锁超时检查的发现时间是无法接受的。
面试官:?那你说说InnoDB和MyisAM是如何发现死锁的?
吒吒辉:
- innodb
数据库会把事务单元锁维持的锁和它所等待的锁都记录下来,Innodb提供了wait-for graph算法来主动进行死锁检测,每当加锁请求无法立即满足需要进入等待时,wait-for graph算法都会被触发。当数据库检测到两个事务不同方向地给同一个资源加锁(产生循序),它就认为发生了死锁,触发wait-for graph算法。
比如:事务1给A加锁,事务2给B加锁,同时事务1给B加锁(等待),事务2给A加锁就发生了死锁。那么死锁解决办法就是终止一边事务的执行即可,这种效率一般来说是最高的,也是主流数据库采用的办法。
Innodb目前处理死锁的方法就是将持有最少行级排他锁的事务进行回滚。这是相对比较简单的死锁回滚方式。死锁发生以后,只有部分或者完全回滚其中一个事务,才能打破死锁。
对于事务型的系统,这是无法避免的,所以应用程序在设计必须考虑如何处理死锁。大多数情况下只需要重新执行因死锁回滚的事务即可。
- MyisAM
MyisAM自身只支持表级锁,故加锁后一次性获取的。所以资源上不会出现多个事务之间互相需要对方释放锁之后再来进行处理。故不会有死锁
面试官:?wait-for graph 算法怎么理解?
吒吒辉:?如下所示,四辆车就是死锁
它们相互等待对方的资源,而且形成环路!每辆车可看为一个节点,当节点1需要等待节点2的资源时,就生成一条有向边指向节点2,最后形成一个有向图。我们只要检测这个走向图是否出现环路即可,出现环路就是死锁!这就是wait-for graph算法。
Innodb将各个事务看为一个个节点,资源就是各个事务占用的锁,当事务1需要等待事务2的锁时,就生成一条有向边从1指向2,最后行成一个有向图。
面试官:?既然死锁无法避免,那如何减少发生呢?
吒吒辉:
原创文章,作者:ItWorker,如若转载,请注明出处:https://blog.ytso.com/140463.html