首先说一下 这个加锁是个啥子过程呢
我们拿一条记录举例,这个记录就放在这,没人操作它,他就没生成锁结构, 直到有个事务操作它了,然后给它才生成了个锁结构,锁结构两个参数 trx(生成该锁的事务) is_waiting(正在等待就是:true 没在等待就是 false) (锁里面很多参数 这里这是为了方便理解例举了两个),然后下一个事务试图操作这条记录,就会发现本记录已经加上锁了,就要再生成一个锁(本次锁的 等待状态就是:true)
举个例子
有个记录 我们称为 record
然后事务A 对record进行操作 事务开始 record加了一个锁 参数 trx:A is_waiting:false
然后来了个byd事务B 又想对record进行操作 事务开始 发现 哎?这有个锁了 然后它也生成一个锁
锁参数 trx:B is_waiting:true is_waiting:true就代表它正在等待 事务B就停在这里了 直到变成false才能继续执行
然后事务A提交 释放锁 然后事务B获得锁 参数变成trx:B is_waiting:false
表级锁
S锁
共享锁(英语原文是share什么玩意 反正S是share 查了一下是Share Locks) 共享锁就是咋的呢 一个事务A对此表加了个S锁 然后又来个事务B试图加了个S锁 这次不会像正常流程一样阻塞 而是会加锁成功 此条记录同时有两个锁(都是is_waiting:false的)
X锁
独占锁(Exclusive Locks) 与其他锁冲突(包括S锁)
这里就得具体描述了
事务A加了个 X锁 事务B加了个S锁 会阻塞
事务A加了个 S锁 事务B加了个X锁 会阻塞 懂了吧 就是不管X锁先加后加 都会阻塞
事务A加了个 X锁 事务B加了个X锁 阻塞
事务A加了个S锁 事务B加了个S锁 不阻塞
IS锁:意向共享锁
IX锁:意向独占锁
这里是啥意思呢 就是说我们的表级锁和行级锁之间也是冲突的 就比如我们行级加了一个X锁
这时我们想在表上加一个X锁 会阻塞 但是我们怎么去看这个表里面有没有行级X锁吗 遍历每条记录吗 那老MYSQL一整几百万条记录 遍历累死
所以提出了 意向锁 IS锁就是说明当前表内有S锁 IX锁就是说明当前表内有X锁
那么这个阻塞的问题呢
IX锁和IX锁冲不冲突? 不冲突 我不同记录加了X锁 这个表也被加入了多次IX锁
那IX和IS锁呢? 也不冲突 不同记录加了X锁 S锁 表上加了IX IS 可以
那X和IX呢? (这里X指表级的) 冲突 IX就说明当前表内有X锁了 而X表级锁和行级锁冲突 (可以把表级锁当做 所有记录都加了行锁呢)
同理 S和IX X和IS....
AUTO-INC锁
眼熟不?像不像某个字段(AUTO_INCREMENT 自增列)
我们在插入记录的时候 自增列是不需要我们自己指定值的 那么如果多个事务同时插入的时候
就会乱套 所以MYSQL提供了两种解决方法
1.AUTO-INC锁:行插入语句时就在表级别加一个 AUTO-INC 锁,然后为每条待插入记录的 AUTO_INCREMENT 修饰的列分配递增的值,在该语句执行结束后,再把 AUTO-INC 锁释放掉。这样一个事务在持有 AUTO-INC 锁的过程中,其他事务的插入语句都要被阻塞,可以保证一个语句中分配的递增值是连续的。
2.轻量级锁 如果因为一列就阻塞所有插入操作未免有点太耽误时间了 所以就提出了个这么个玩意
在为插入语句生成 AUTO_INCREMENT 修饰的列的值时获取一下这个轻量级锁,然后生成本次插入语句需要用到的 AUTO_INCREMENT 列的值之后,就把该轻量级锁释放掉,并不需要等到整个插入语句执行完才释放锁。
如果我们的插入语句在执行前就可以确定具体要插入多少条记录,比方说我们上边举的关于表 t 的例子中,在语句执行前就可以确定要插入2条记录,那么一般采用轻量级锁的方式对 AUTO_INCREMENT 修饰的列进行赋值。这种方式可以避免锁定表,可以提升插入性能。
对吧对吧 提前知道插入多少记录了 就可以提前准备了
可以通过innodb_autoinc_lock_mode的系统变量来控制到底使用上述两种方式中的哪种来为AUTO_INCREMENT修饰的列进行赋值,当innodb_autoinc_lock_mode值为0时,一律采用AUTO-INC锁;当innodb_autoinc_lock_mode值为2时,一律采用轻量级锁;当innodb_autoinc_lock_mode值为1时,两种方式混着来(也就是在插入记录数量确定时采用轻量级锁,不确定时使用AUTO-INC锁)。不过当innodb_autoinc_lock_mode值为2时,可能会造成不同事务中的插入语句为AUTO_INCREMENT修饰的列生成的值是交叉的,在有主从复制的场景是不安全的.(性能的代价)
顺便说下:表级的X锁 S锁基本用不上 锁住全表实在太重了 耽误事 对应的IX IS锁也不常用
行级锁
重点来了啊重点来了
记录锁(Record Locks)
记录锁分x锁和s锁不多赘述
间隙锁(Gap Locks)
锁如其名 锁住间隙的
假如说有 1 2 3 4这几条记录在 记录3上加间隙锁就是 锁住了2和3之间的间隙 让其他事务无法在这个间隙上面加记录 假如说有一条记录来了 想在2和3之间插入 看一眼它后面的记录 我超!!!!锁!!!!
额 然后就阻塞了
这个 gap锁 的提出仅仅是为了防止插入幻影记录而提出的,虽然有 共享gap锁 和 独占gap锁 这样的说法,但是它们起到的作用都是相同的。而且如果你对一条记录加了 gap锁 (不论是 共享gap锁 还是 独占gap锁 ),并不会限制其他事务对这条记录加 正经记录锁 或者继续加 gap锁 ,再强调一遍, gap锁 的作用仅仅是为了防止插入幻影记录的而已。
啊 你会不会想 那我要是想在最后一条记录前那个卵间隙加记录 那么我缺失的这个锁这一块谁来给我补呢?
每个数据页中有 两个伪记录
Infimum 记录,表示该页面中最小的记录。Supremum 记录,表示该页面中最大的记录。
假如说我在那个(4,正无穷)的这个间隙上面加锁 就直接在这个Supremum 记录就可以了
Next-Key Locks
我又想锁住记录 又想锁住记录前面的间隙 就使用Next-Key Locks吧!
Next-key Locks本质上等于俩锁 记录锁+间隙锁
Insert Intention Locks
插入意向锁 当一条记录想插入这个间隙的时候 发现这个位置被间隙锁锁住了 于是它在这个时候开始等待 等待的过程生成一个插入意向锁 表示有记录想在这个间隙里面插入记录 但是还在等待
插入意向锁是一种特殊的间隙锁
有个事务A对这个间隙加锁了 然后来了一个事务B 事务C想在这个间隙里面加记录 B C来一看 好嘛
这有个间隙锁 然后他俩生成了插入意向锁 is_wait是false 然后A的间隙锁释放后,他俩就可以获得到插入意向锁(实质上只是is_waiting调整成了true) B和C之间也不会阻塞 可以同时获取到锁 所以有啥用呢
只有当被间隙锁阻塞的时候才会生成插入意向锁
隐式锁
我们前边说一个事务在执行 INSERT 操作时,如果即将插入的 间隙 已经被其他事务加了 gap锁 ,那么本次INSERT 操作会阻塞,并且当前事务会在该间隙上加一个 插入意向锁 ,否则一般情况下 INSERT 操作是不加锁的。那如果一个事务首先插入了一条记录(此时并没有与该记录关联的锁结构)
如果此时对它进行读操作(可以看下面的部分) SELECT ... LOCK IN SHARE MODE 语句读取这条事务,也就是在要获取这条记录的 S锁 ,或者使用 SELECT ... FOR UPDATE 语句读取这条事务或者直接修改这条记录,也就是要获取这条记录的 X锁,阁下如果不生成任何锁应对的话 就可能产生脏读
立即修改这条记录,也就是要获取这条记录的 X锁 ,该咋办?如果允许这种情况的发生,那么可能产生 脏写 问题。
我们分两种情况讨论解决方案
对于聚簇索引记录来说,有一个 trx_id 隐藏列,该隐藏列记录着最后改动该记录的 事务id 。那么如果在当前事务中新插入一条聚簇索引记录后,该记录的 trx_id 隐藏列代表的的就是当前事务的事务id ,如果其他事务此时想对该记录添加 S锁 或者 X锁 时,首先会看一下该记录的 trx_id 隐藏列代表的事务是否是当前的活跃事务,如果是的话,就代表着有一个插入操作正在进行 ,那么就帮助当前事务创建一个 X锁 (也就是为当前事务创建一个锁结构, is_waiting 属性是 false ),然后自己进入等待状态(也就是为自己也创建一个锁结构, is_waiting 属性是 true )
先发现锁冲突 然后再加锁 所以是隐式锁
对于二级索引记录来说,本身并没有 trx_id 隐藏列,但是在二级索引页面的 Page Header 部分有一个 PAGE_MAX_TRX_ID 属性,该属性代表对该页面做改动的最大的 事务id ,如果PAGE_MAX_TRX_ID 属性值小于当前最小的活跃 事务id ,那么说明对该页面做修改的事务都已经提交了,(如果当前事务正在活跃 那么这个PAGE_MAX_TRX_ID 属性值要么等于这个修改事务 要么是比这个修改事务的id更大 说明后面又新来了一个修改操作 而比它还小 只能说明修改操作都已经提交了)否则就需要在页面中定位到对应的二级索引记录,然后回表找到它对应的聚簇索引记录,然后再重复 情景一 的做法。
Insert的隐式锁
当事务需要加锁的时,如果这个锁不可能发生冲突,InnoDB会跳过加锁环节,这种机制称为隐式锁。隐式锁是 InnoDB 实现的一种延迟加锁机制,其特点是只有在可能发生冲突时才加锁,从而减少了锁的数量,提高了系统整体性能。
隐式锁就是在 Insert 过程中不加锁,只有在特殊情况下,才会将隐式锁转换为显示锁,这里我们列举两个场景。
- 如果记录之间加有间隙锁,为了避免幻读,此时是不能插入记录的;
- 如果 Insert 的记录和已有记录存在唯一键冲突,此时也不能插入记录;
间隙锁情况
每插入一条新记录,都需要看一下待插入记录的下一条记录上是否已经被加了间隙锁,如果已加间隙锁,此时会生成一个插入意向锁,然后锁的状态设置为等待状态(PS:MySQL 加锁时,是先生成锁结构,然后设置锁的状态,如果锁状态是等待状态,并不是意味着事务成功获取到了锁,只有当锁状态为正常状态时,才代表事务成功获取到了锁),现象就是 Insert 语句会被阻塞。
索引重复
如果在插入新记录时,插入了一个与「已有的记录的主键或者唯一二级索引列值相同」的记录(不过可以有多条记录的唯一二级索引列的值同时为NULL,这里不考虑这种情况),此时插入就会失败,然后对于这条记录加上了 S 型的锁。
如果主键索引重复,插入新记录的事务会给已存在的主键值重复的聚簇索引记录添加 S 型记录锁。
如果唯一二级索引重复,插入新记录的事务都会给已存在的二级索引列值重复的二级索引记录添加 S 型 next-key 锁。
总之就是先发现冲突然后再加锁 如果没有冲突就不加锁 (比喻就是有一个隐形的锁 一旦碰到冲突才会显现出来)
读时的锁操作
我们前边说在采用 加锁 方式解决 脏读 、 不可重复读 、 幻读 这些问题时,读取一条记录时需要获取一下该记录的 S锁 ,其实这是不严谨的,有时候想在读取记录时就获取记录的 X锁 ,来禁止别的事务读写该记录
对读取的记录加 S锁 : SELECT ... LOCK IN SHARE MODE;
对读取的记录加 X锁 : SELECT ... FOR UPDATE;
这里的X锁仍与S锁冲突(会阻塞)
当前读时(update、insert、delete,这些语句执行前都会查询最新版本的数据,然后再做进一步的操作) 会在读的范围上加一个间隙锁
写时的锁操作
DELETE :对一条记录做 DELETE 操作的过程其实是先在 B+ 树中定位到这条记录的位置,然后获取一下这条记录的 X锁 ,然后再执行 delete mark 操作(先标记 在事务提交后才由专门的线程做purge操作,把它加入到垃圾链表
中)。我们也可以把这个定位待删除记录在 B+ 树中位置的过程看成是一个获取 X锁 的 锁定读
UPDATE:
如果未修改该记录的键值并且被更新的列占用的存储空间在修改前后未发生变化 则先在 B+ 树中定位到这条记录的位置,然后再获取一下记录的 X锁 ,最后在原记录的位置进行修改操作。其实我们也可以把这个定位待修改记录在 B+ 树中位置的过程看成是一个获取 X锁 的 锁定读
如果未修改该记录的键值并且至少有一个被更新的列占用的存储空间在修改前后发生变化,则先在B+ 树中定位到这条记录的位置,然后获取一下记录的 X锁 ,将该记录彻底删除掉(就是把记录彻底移入垃圾链表),最后再插入一条新记录。这个定位待修改记录在 B+ 树中位置的过程看成是一个获取 X锁 的 锁定读 ,新插入的记录由 INSERT 操作提供的 隐式锁 进行保护。
如果修改了该记录的键值,则相当于在原记录上做 DELETE 操作之后再来一次 INSERT 操作,加锁操作就需要按照 DELETE 和 INSERT 的规则进行了。文章来源:https://www.toymoban.com/news/detail-579434.html
注意后两种的区别 记录移入垃圾链表和DELETE操作是不同的文章来源地址https://www.toymoban.com/news/detail-579434.html
到了这里,关于MYSQL中的锁(面试难点重点)的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!