今日分享开始啦,请大家多多指教~
锁分类
当多个事务或进程访问同一个资源时,为了保证数据的一致性就会用到锁机制,在MySQL中锁有多种不同的分类。
以操作粒度区分
行级锁、表级锁和页级锁
表级锁:每次操作锁住整张表。锁定的粒度大、开销小、加锁快;不会发生死锁,但发生锁冲突的概率极高,并发度最低,应用在InnoDB、MyISAM、BDB中;行级锁:每次操作锁住一行数据。锁定的粒度小、开销大、加锁慢;会出现死锁,发生锁冲突的概率极低,并发度最高,应用在InnoDB中;页级锁:每次锁定相邻的一组记录。锁定粒度、开销、加锁时间介于行级锁和表级锁之间;会出现死锁,并发度一般,应用在BDB中;
以操作类型区分
读锁、写锁
读锁(S):共享锁,针对同一份数据,多个读操作可以同时进行不会互相影响;写锁(X):排它锁,当前写操作没有完成时,会阻塞其他读和写操作;为了允许行锁和表锁的共存,实现多粒度的锁机制,InnoDB还有两种内部使用的意向锁,这两种意向锁都是表锁:
意向读锁(IS)、意向写锁(IX):属于表级锁,S和X主要针对行级锁。在对表记录添加S或X锁之前,会先对表添加IS和IX锁,表明某个事务正在持有某些行的锁、或该事务准备去持有锁;意向锁存在是为了协调锁之间的关系,支持多粒度锁共存;为什么意向锁是表级锁?
为了减少确认次数,提升性能:如果意向锁是行锁,需要遍历每一行去确认数据是否已经加锁;如果是表锁的话,只需要判断一次就知道有没有数据行被锁定;
意向锁是如何支持行级锁、表级锁共存的?
举例
S锁:事务A对记录添加了S锁,可以对记录进行读取操作,不能做修改,其它事务可以对该记录追加S锁,但是不能追加X锁,追加X锁需要等记录的S锁全部释放;X锁:事务A对记录添加了X锁,可以对记录进行读和修改操作,其它事务不能对该记录做读和修改操作。意向锁、共享锁和排它锁之间的兼容关系
意向锁相互兼容,因为IX和IS只是表明申请更低层次的级别元素的X、S操作;表级S和X、IX不兼容,因为上了表级S锁后,不允许其它事务再加X锁;上了表级X锁后,会修改数据,所以表级X锁和IS、IX、S、X(即使是行排他锁,因为表级锁定的行肯定包括行级锁定的行,所以表级X和IX、行级X)不兼容。以操作性能区分
乐观锁、悲观锁
乐观锁:一般采用的方式是对数据记录版本进行对比,在数据更新提交时才会进行冲突检测,如果发现冲突了,则提示错误信息;悲观锁:在对一条记录进行修改时,为了避免被其他人修改,在修改数据之前先锁定再修改的方式。共享锁和排它锁是悲观锁的不同实现。InnoDB的行锁
行锁的实现原理
意向锁是InnoDB自动加的,不需要用户干预;对于UPDATE、DELETE和INSERT语句,InnoDB会自动给涉及的数据集增加排他锁(X);对于普通的SELECT语句,InnoDB不会加任何锁;事务也可以通过以下语句显式的给记录集加共享锁
SELECT*FROMtable_nameWHERE...LOCKINSHAREMODE和排它锁SELECT*FROMtable_nameWHERE...FORUPDATE。
在InnoDB中,支持行锁和表锁,行锁又分为共享锁和排它锁。InnoDB行锁是通过对索引数据页上的记录加锁实现的。由于InnoDB行锁的实现特点,导致只有通过索引条件检索并且执行计划中真正使用到索引时InnoDB才会使用行锁;并且不论使用主键索引、唯一索引、普通索引,InnoDB都会使用行锁来进行加锁,否则InnoDB将使用表锁。
由于InnoDB是针对索引加锁,而不是针对记录加锁,所以即使多个事务访问不同行的记录,但如果使用的是相同的索引,还是会出现锁冲突的情况,甚至出现死锁。
行锁的不同实现
行锁的主要实现有三种:RecordLock、GapLock和Next-KeyLock。
RecordLock:记录锁,锁定单个行记录的锁,RC和RR隔离级别支持。
GapLock:间隙锁,锁定索引记录间隙,确保索引记录的间隙不变。范围锁,RR隔离级别支持。(加锁之后间隙范围内不允许插入数据,防止发生幻读)
InsertIntention:插入意向锁,插入意向锁中虽然含有意向锁三个字,但是它不属于意向锁,而是属于间隙锁,在insert时产生;意向锁是表锁,而插入意向锁是行锁。Next-KeyLock:临键锁,它是记录锁和间隙锁的结合体,锁住数据的同时锁住数据前后范围。记录锁+范围锁,RR隔离级别支持。
insert的加锁流程:
执行insert之后,如果没有任何冲突,在showengineinnodbstatus命令中是看不到任何锁的,这是因为insert加的是隐式锁。什么是隐式锁?隐式锁的意思就是没有锁!
所以,根本就不存在先加插入意向锁,再加排他记录锁的说法,在执行insert语句时,什么锁都不会加。当其他事务执行select...lockinsharemode时触发了隐式锁的转换。
InnoDb在插入记录时,是不加锁的。如果事务A插入记录且未提交,这时事务B尝试对这条记录加锁:事务B会先去判断记录上保存的事务id是否活跃,如果活跃的话,那么就帮助事务A去建立一个锁对象(排他记录锁),然后自身进入等待事务A状态,这就是所谓的隐式锁转换为显式锁。
结论:
执行insert语句,判断是否有和插入意向锁冲突的锁,如果有,加插入意向锁,进入锁等待;如果没有,直接写数据,不加任何锁;
执行select...lockinsharemode语句,判断记录上是否存在活跃的事务,如果存在,则为insert事务创建一个排他记录锁,并将自己加入到锁等待队列;
MySQL使用间隙锁的目的
间隙锁的主要目的是为了防止幻读,其主要通过两个方面实现这个目的:
防止间隙内有新数据被插入防止已存在的数据,更新成间隙内的数据另外一方面,是为了满足其恢复和复制的需要。对于基于语句的日志格式的恢复和复制而言,由于MySQL的BINLONG是按照事务提交的先后顺序记录的,因此要正确恢复或者复制数据,就必须满足:在一个事务未提交前,其他并发事务不能插入满足其锁定条件的任何记录,根本原因还是不允许出现幻读。
锁规则
规则1:加锁的基本单位是临键锁(Next-keyLock)规则2:查找过程中访问的对象才会加锁优化1:索引上的等值查询,给唯一键加索引的时候,如果查询值存在,临键锁(Next-keyLock)会退化成记录锁(RecordLock);如果查询值不存在,会按照优化2进行优化优化2:索引上的等值查询,向右遍历时且最近一个值不满足等值条件时,临键锁(Next-keyLock)会退化成间隙锁(GapLock)bug1:唯一索引上的范围查询会访问到不满足条件的第一个值为止。在mysql8.0.18及以上已经没有这个bug
锁结构
对不同记录加锁时,如果符合下边这些条件:
在同一个事务中进行加锁操作被加锁的记录在同一个页面中加锁的类型是一样的等待状态是一样的那么这些记录的锁就可以被放到一个锁结构中。
锁的兼容性
从图中可以看出,横向为事务A拥有的锁,竖向为事务B想要获取的锁;举例:如果前一个事务A持有gap锁或者next-key锁的时候,后一个事务B如果想要持有InsertIntention锁的时候会不兼容,出现锁等待。
加锁
SELECT...FROM...:InnoDB采用MVCC机制实现非阻塞读,对于普通的SELECT语句,InnoDB不加锁。SELECT...FROM...LOCKInSHAREMODE:显式追加共享锁,InnoDB会使用临键锁(Next-keyLock)进行处理,如果发现了唯一索引,可以降级为记录锁(RecordLock)。SELECT...FROM...FORUPDATE:显式追加排它锁,InnoDB会使用Next-KeyLock锁进行处理,如果发现唯一索引,可以降级为RecordLock锁。UPDATE...WHERE:InnoDB会使用临键锁(Next-keyLock)进行处理,如果扫描发现唯一索引,可以降级为记录锁(RecordLock)。DELETE...WHERE:InnoDB会使用临键锁(Next-keyLock)进行处理,如果扫描发现唯一索引,可以降级为记录锁(RecordLock)。insert:InnoDB会在将要插入的那一行设置一个排他的记录锁(RecordLock)。以updatet1setname=‘XX’whereid=10操作为例:
主键加锁
加锁行为:仅在id=10的主键索引记录上加X锁。
唯一键加锁
加锁行为:先在唯一索引id上加X锁,然后在id=10的主键索引记录上加X锁。
非唯一键加锁
加锁行为:对满足id=10条件的记录和主键分别加X锁,然后在(6,c)-(10,b)、(10,b)-(10,d)、(10,d)(11,f)范围分别加GapLock。
无索引加锁
加锁行为:表里所有行和间隙都会加X锁。(当没有索引时,会导致全表锁定,因为InnoDB引擎锁机制是基于索引实现的记录锁定)。
锁模拟
查看事务、锁的语句:
输出结果解析:
数据准备:
锁举例
锁等待超时:ERROR(HY):Lockwaittimeoutexceeded;tryrestartingtransaction
死锁:Deadlockfoundwhentryingtogetlock
等值查询间隙锁
分析:
由于表T没有id=7这条记录,加锁单位是Next-keyLock,事务1加锁范围是(5,10],因为id=7是一个等值查询,根据优化规则,id=10不满足条件,Next-keyLock退化成GapLock,因此最终加锁范围是(5,10)。Session2想要向这个间隙中插入id=8的记录必须等待Session1事务提交后才可以。Session3想要插入id=11,不在加锁范围,所以可以插入成功。这是如果有Session4想要更新id=8的记录,是可以执行成功的,因为间隙锁之间互不冲突;
非唯一键等值锁
分析:
Session1给索引c上的c=5这一列加上读锁,根据规则1,加锁单位为Next-keyLock,因此会给(0,5]区间加上Next-keyLock因为c是普通索引,所以访问c=5之后还要向右遍历,直到c=10停止,根据规则2访问到的都要加锁,所以加锁范围为(5,10],根据优化2,等值查询退化为GapLock,变为(5,10),所以最终的加锁范围是(0,10);Session2想要插入id=7的记录,要等待Session1提交之后才可以成功插入,因为Session1的间隙范围是(5,10);根据原则2,访问到的对象才会加锁,这个查询使用覆盖索引,并不需要访问主键索引,所以主键索引上没有加任何锁。所以Session3的语句可以正常执行;LOCKINSHAREMODE;只锁覆盖索引,FORUPDATE;会顺便锁上主键索引;
主键索引范围锁
select*fromtwhereid=10forupdate;
select*fromtwhereid=10andid11forupdate;
对于以上两条SQL,加锁的范围不一致,第一条是id=10的行锁,第二条是(10,15]的Next-keyLock。
分析:
Session1根据规则1,加锁单位为Next-keyLock,因为id=10是范围查询,直到找到id=15停止,最终Session1的加锁范围是(10,15]Session3当去update一个存在的值是,给该行添加RecordLock,由于RecordLock和Next-keyLock不兼容,所以阻塞如果Session3更新一个(10,15)的值,则会阻塞;
非唯一索引范围锁
分析:
Session1给索引c加上了(5,10],(10,15]两个Next-keyLock;由于是范围查询,不触发优化,不会退化成间隙锁
非唯一索引等值锁forUpdate
数据准备:
在表t中,a列有普通索引,所以可能锁定的范围有:
(-∞,1],(1,3],(3,5],(5,8],(8,11],(11,+∞)
Session1执行完成之后预期加锁范围为(5,8]和(8,11],由于锁优化策略,退化成间隙锁,范围变成(5,8]和(8,11),也就是(5,11),插入12和4不会阻塞很好理解。但是5不在锁的范围内,还是被锁上了。
是因为如果索引值相同的话,会根据id进行排序加锁,所以最终的加锁范围是索引a的(5,4)到(11,6)的范围。
死锁模拟
死锁模拟-场景1
ABBA操作问题
数据准备:
死锁模拟-场景2
S-lock升级X-lock
数据准备:
沿用简单场景1数据
分析:
Session1获取到S-LockSession2尝试获取到X-Lock,但是被Session1的S-Lock阻塞Session1想要获取到X-Lock,本身拥有一个S-Lock,但是Session2申请X-Lock在前,需要等待Session2释放之后才能提升到X-Lock,两个事务造成资源争抢导致死锁死锁模拟-场景3
数据准备:
分析:
事务一在插入时由于跟事务二插入的记录唯一键冲突,所以对a=10这个唯一索引加S锁(Next-key)并处于锁等待,事务二再插入a=9这条记录,需要获取插入意向锁(lock_modeXlocksgapbeforerecinsertintention)和事务一持有的Next-key锁冲突,从而导致死锁。
死锁模拟-场景4
死锁日志:
UPDATE的WHERE子句没有满足条件的记录,而对于不存在的记录并且在RR级别下,UPDATE加锁类型为间隙锁(GapLock),间隙锁(GapLock)之间是兼容的,所以两个事务都能成功执行UPDATE;这里的gap范围是索引id列(5,10)的范围。INSERT时,其加锁过程为先在插入间隙上获取插入意向锁,插入数据后再获取插入行上的排它锁。又插入意向锁与间隙锁(GapLock)和临键锁(Next-keyLock)冲突,即一个事务想要获取插入意向锁,如果有其他事务已经加了(GapLock)或临键锁(Next-keyLock),则会阻塞。场景中两个事务都持有间隙锁(GapLock),然后又申请插入意向锁,此时都被阻塞,循环等待造成死锁。记录锁(LOCK_REC_NOT_GAP):lock_modeXlocksrecbutnotgap间隙锁(LOCK_GAP):lock_modeXlocksgapbeforerecNext-key锁(LOCK_ORNIDARY):lock_modeX插入意向锁(LOCK_INSERT_INTENTION):lock_modeXlocksgapbeforerecinsertintention并不是在日志里看到lock_modeX就认为这是Next-key锁,因为还有一个例外:如果在supremumrecord上加锁,locksgapbeforerec会省略掉,间隙锁会显示成lock_modeX,插入意向锁就会显示成lock_modeXinsertintention。
INSERT语句,会尝试获取lockmodeSwaiting锁,这是为了检测唯一键是否重复,必须进行一次当前读,要加S锁。
INSERT加锁分几个阶段:先检查唯一键约束,加S锁,再加插入意向锁,最后插入成功时升级为X锁。
今日份分享已结束,请大家多多包涵和指点!