细说MySQL锁机制:S锁、X锁、意向锁…

好久没有深入地写文章了,这次来发一篇,通过mysql事物 | Joseph's Blog (gitee.io)和其他一些博客有感进行一些补充,InnoDB详解在下期发布

加锁机制

乐观锁和悲观锁

之前在JVM中其实也讲到,JVM在对象初始化的过程中其实也是使用的乐观锁

image-20230612231129466

感兴趣可以去看我的文章Karos – 一个热衷于前后端开发分享的个人博客网站 (wzl1.top)

锁粒度

表锁

表级别的锁定是MySQL各存储引擎中最大颗粒度的锁定机制。

该锁定机制最大的特点是实现逻辑非常简单,带来的系统负面影响最小,所以获取锁和释放锁的速度很快,由于表级锁一次会将整个表锁定,所以可以很好的避免困扰我们的死锁问题。

锁定颗粒度大所带来最大的负面影响就是出现锁定资源争用的概率也会最高,大大降低并发度。

使用表级锁定的主要是MyISAM,MEMORY,CSV等一些非事务性存储引擎。

行锁

侨总:与表锁正相反,行锁最大的特点就是锁定对象的颗粒度很小,也是目前各大数据库管理软件所实现的锁定颗粒度最小的。由于锁定颗粒度很小,所以发生锁定资源争用的概率也最小,能够给予应用程序尽可能大的并发处理能力从而提高系统的整体性能。

  虽然能够在并发处理能力上面有较大的优势,但是行级锁定也因此带来了不少弊端。由于锁定资源的颗粒度很小,所以每次获取锁和释放锁需要做的事情也更多,带来的消耗自然也就更大了。此外,行级锁定也最容易发生死锁。

  使用行级锁定的主要是InnoDB存储引擎。

适用场景:

从锁的角度来说,表级锁更适合于以查询为主,只有少量按索引条件更新数据的应用,如Web应用;而行级锁则更适合于有大量按索引条件并发更新数据的情况,同时又有并发查询的应用场景。

页锁

是MySQL中比较独特的一种锁定级别,在其他数据库管理软件中也并不是太常见。

页级锁定的特点是锁定颗粒度介于行级锁定与表级锁之间,所以获取锁定所需要的资源开销,以及所能提供的并发处理能力也同样是介于上面二者之间。另外,页级锁定和行级锁定一样,会发生死锁。

使用页级锁定的主要是BerkeleyDB存储引擎。

全局锁

对整个数据库实例加锁。使用场景一般在全库逻辑备份时。

Flush tables with read lock (FTWRL)

这个命令可以使整个库处于只读状态。使用该命令之后,数据更新语句、数据定义语句和更新类事务的提交语句等修改数据库的操作都会被阻塞。

风险:

  1. 如果在主库备份,在备份期间不能更新,业务停摆
  2. 如果在从库备份,备份期间不能执行主库同步的binlog,导致主从延迟同步

set global readonly=true

将整个库设置成只读状态,但这种修改global配置量级较重,和全局锁不同的是:如果执行Flush tables with read lock 命令后,如果客户端发生异常断开,那么MySQL会自动释放这个全局锁,整个库回到可以正常更新的状态。但将库设置为readonly后,客户端发生异常断开,数据库依旧会保持readonly状态,会导致整个库长时间处于不可写状态

思考

行级锁为什么容易发生死锁

产生死锁的四个条件

  1. 互斥条件:该资源==任意==⼀个时刻只由⼀个线程占⽤。
  2. 请求与保持条件:⼀个进程因请求资源⽽==阻塞==时,对已获得的资源保持不放。
  3. 不剥夺条件:线程已获得的资源在末使⽤完之前不能被其他线程强⾏剥夺,==只有⾃⼰使⽤完毕后才释放资源==(资源未释放,资源一直在占用)。
  4. 循环等待条件:若⼲进程之间形成⼀种头尾相接的==循环等待资源==关系。

tips:

这一块属于是操作系统的知识

如果有想要更多了解的小伙伴看一看看我的另外一篇文章:操作系统-超20000字的“总结” - Karos (wzl1.top)

总结

这里知识简单西索一下:从粒度上来区分几种锁,下面是几种引擎使用锁的情况,更多的东西请君看下文::dog:

在这里插入图片描述

兼容性

共享锁 S锁

共享锁就是多个事务对于同一数据共享一把锁,都能访问到数据,但是只能读不能修改。共享锁又称为读锁,简称S锁。

排他锁 X锁

排他锁就是不能与其它锁并存,如一个事务获取了一个数据行的排他锁,其他事务就不能再获取该行的其他锁,包括共享锁和排他锁,但是获取排他锁的事务是可以对数据就行读取和修改。排他锁又称为写锁,简称X锁。

原理:一个事务获取了一个数据行的排他锁,其他事务就不能再获取该行的其他锁(排他锁或者共享锁),即一个事务在读取一个数据行的时候,其他事务不能对该数据行进行增删改,不加锁的查是可以的,加锁的查是不可以的。

如何启用

#设置共享锁
SELECT ... LOCK IN SHARE MODE; #这种共享锁都是行锁
#设置表共享锁
-- 加共享锁
LOCK TABLE product_comment READ;
-- 解共享锁
UNLOCK TABLE product_comment;

#设置排他锁:
SELECT ... FOR UPDATE; #排它锁可以使行锁也可以是表锁
-- 加写锁
LOCK TABLE product_comment WRITE;
-- 解锁
UNLOCK TABLE product_comment;

InnoDB引擎默认的修改数据语句,update,delete,insert都会自动给涉及到的数据加上排他锁

select语句默认不会加任何锁类型,所以加过排他锁的数据行在其他事务中是不能修改数据的,也不能通过for update和lock in share mode锁的方式查询数据,但可以直接通过select ...from...查询数据,因为普通查询没有任何锁机制。

除此之外还有意向共享锁和意向排他锁,我将会在后面的意向锁中讲到

锁模式

下面是从模式上来划分,既然刚刚提到意向锁,那么我们就来西索一下意向锁吧

意向锁

意向锁属于表级锁,其设计目的主要是为了在一个事务中揭示下一行将要被请求锁的类型。

即:意向锁是有数据引擎自己维护的,用户无法手动操作意向锁,在为数据行加共享 / 排他锁之前,InooDB 会先获取该数据行所在在数据表的对应意向锁。

InnoDB 中的两个表锁:

意向共享锁(IS)

表示事务准备给数据行加入共享锁,也就是说一个数据行加共享锁前必须先取得该表的IS锁;

意向排他锁(IX)

类似上面,表示事务准备给数据行加入排他锁,说明事务在一个数据行加排他锁前必须先取得该表的IX锁。

意向锁的兼容互斥性

意向锁之间是互相兼容的

image-20230613001406669

但是和普通的 共享/排他锁 会产生互斥

img

InnoDB行锁是通过索引上的索引项来实现的,这一点MySQL与Oracle不同,后者是通过在数据中对相应数据行加锁来实现的。

InnoDB这种行锁实现特点意味者:只有通过索引条件检索数据,InnoDB才会使用行级锁,否则,InnoDB将使用表锁(具体原因后面西索)!
在实际应用中,要特别注意InnoDB行锁的这一特性,不然的话,可能导致大量的锁冲突,从而影响并发性能。

意向共享锁和意向排他锁锁定的是表。

注意:这里的排他 / 共享锁指的都是表锁!!!意向锁不会与行级的共享 / 排他锁互斥!!!

用SELECT .. IN SHARE MODE获得共享锁,主要用在需要数据依存关系时确认某行记录是否存在,并确保没有人对这个记录进行UPDATE或者DELETE操作。

但是如果当前事务也需要对该记录进行更新操作,则很有可能造成死锁,对于锁定行记录后需要进行更新操作的应用,应该使用SELECT ... FOR UPDATE方式获取排他锁。

意向锁的意义

如果另一个任务试图在该表级别上应用共享或排它锁,则受到由第一个任务控制的表级别意向锁的阻塞。第二个任务在锁定该表前不必检查各个页或行锁,而只需检查表上的意向锁。

意向锁的并发性

意向锁不会与行级的共享 / 排他锁互斥,所以意向锁并不会影响到多个事务对不同数据行加排他锁时的并发性

总结

  • InnoDB 支持多粒度锁,特定场景下,行级锁可以与表级锁共存。
  • 意向锁之间互不排斥,但除了 IS 与 S 兼容外,意向锁会与 共享锁 / 排他锁 互斥。
  • IX,IS是表级锁,不会和行级的X,S锁发生冲突。只会和表级的X,S发生冲突。
  • 意向锁在保证并发性的前提下,实现了行锁和表锁共存且满足事务隔离性的要求。

意向锁部分内容采纳于:详解 MySql InnoDB 中意向锁的作用 - 掘金 (juejin.cn)

间隙锁(gap lock)

:poodle:在上面引入的文章中,其实说了下间隙锁,那么这里也来聊聊。

间隙锁是一个在索引记录之间的间隙上的锁。

间隙锁的作用

保证某个间隙内的数据在锁定情况下不会发生任何变化。

what?

下面来解释下,假设为隔离级别为RR

当使用唯一索引来搜索唯一行的语句时,不需要间隙锁定。

select * from t where id = 10 for update; #注意:如果是普通查询则是快照读,不需要加锁,加了for update就不是普通查询
  • 如果,上面语句中id列没有建立索引或者是非唯一索引时,则语句会产生间隙锁。
  • 如果,搜索条件里有多个查询条件(即使每个列都有唯一索引),也是会有间隙锁的。

需要注意的是,当id列上没有索引时,SQL会走聚簇索引的全表扫描进行过滤,由于过滤是在MySQL Server层面进行的。因此每条记录(无论是否满足条件)都会被加上X锁。但是,为了效率考量,MySQL做了优化,对于不满足条件的记录,会在判断后放锁,最终持有的,是满足条件的记录上的锁。但是不满足条件的记录上的加锁/放锁动作是不会省略的。所以在没有索引时,不满足条件的数据行会有加锁又放锁的耗时过程。

间隙锁的范围

根据检索条件向下寻找最靠近检索条件的记录值A作为左区间,向上寻找最靠近检索条件的记录值B作为右区间,即锁定的间隙为(A,B)。

光说肯定不懂,看示例

number 1 2 3 4 5 ==6== ==6== ==6== 11
id 1 3 5 7 9 10 11 12 23

假如我们这里有一个select

select * from table where number = 6 for update;

那么这里会在(5,11)间加锁,其他事务再想insert就会阻塞

注意一下,间隙锁不是页锁

间隙锁是一种在数据库中用于处理并发事务的锁定方式。它的作用是防止其他事务在间隙(两个索引键之间的空白区域)中插入新记录。间隙锁通常用于防止幻读(Phantom Read)的问题,即在一个事务中多次执行同一查询时,查询的结果集合发生了变化。

页锁(Page Lock)是另一种锁定机制,它是在数据库页级别上对数据进行锁定。当一个事务锁定了一个数据页时,其他事务需要等待该事务释放锁才能继续对该页进行读取或写入操作。页锁通常用于处理较大数据块的并发访问问题。

同理,你插入number=6的数据也会被阻塞

记录锁

记录锁是 封锁记录,记录锁也叫行锁 \\ (\^_\^ ^~^) 什么玩意儿???

需要注意的是:

  • id 列必须为唯一索引列或主键列,否则上述语句加的锁就会变成临键锁(有关临键锁下面会讲)。
  • 同时查询语句必须为精准匹配(=),不能为 >、<、like等,否则也会退化成临键锁。

其他实现

在通过 主键索引唯一索引 对数据行进行 UPDATE 操作时,也会对该行数据加记录锁:

-- id 列为主键列或唯一索引列 
UPDATE SET age = 50 WHERE id = 1;

记录锁是锁住记录,锁住索引记录,而不是真正的数据记录.

如果要锁的列没有索引,进行全表记录加锁(上面说过没有索引时SQL会走聚簇索引的全表扫描进行过滤)

记录锁也是排它(X)锁,所以会阻塞其他事务对其插入、更新、删除

next-key锁

行锁+间隙锁

innoDB默认的隔离级别是可重复读(Repeatable Read),并且会以Next-Key Lock的方式对数据行进行加锁。Next-Key Lock是行锁和间隙锁的组合,当InnoDB扫描索引记录的时候,会首先对索引记录加上行锁(Record Lock),再对索引记录两边的间隙加上间隙锁(Gap Lock)。加上间隙锁之后,其他事务就不能在这个间隙修改或者插入记录。

当查询的索引含有唯一属性(唯一索引,主键索引)时,Innodb存储引擎会对next-key lock进行优化,将其降为record lock,即仅锁住索引本身,而不是范围。

插入意向锁

插入意向锁是一种间隙锁形式的意向锁,在真正执行 INSERT 操作之前设置。

当执行插入操作时,总会检查当前插入操作的下一条记录(已存在的主索引节点)上是否存在锁对象,判断是否锁住了 gap,如果锁住了,则判定和插入意向锁冲突,当前插入操作就需要等待,也就是配合上面的间隙锁或者临键锁一起防止了幻读操作。

(1)申请意向锁的动作是数据库完成的,就是说,事务A申请一行的行锁的时候,数据库会自动先开始申请表的意向锁,不需要我们程序员使用代码来申请。

(2)IX,IS是表级锁,不会和行级的X,S锁发生冲突。只会和表级的X,S发生冲突

行级别的X和S按照普通的共享、排他规则即可。所以之前的示例中第2步不会冲突,只要写操作不是同一行,就不会发生冲突。

总结

行锁(Row Lock)是针对数据库表中的行进行加锁的机制。当一个事务对某一行进行更新或删除操作时,会对该行加上行锁,防止其他事务同时对同一行进行操作,确保数据的一致性。

页锁(Page Lock)是针对数据库表中的页(通常是数据库中的一个数据页)进行加锁的机制。当一个事务对某一页中的多行进行操作时,可以选择对整个页进行加锁,减少锁的粒度,提高并发性能。

表锁(Table Lock)是针对整个数据库表进行加锁的机制。当一个事务需要对整个表进行操作时,可以选择对表进行加锁,防止其他事务对整个表进行操作,确保数据的一致性。但是表锁会降低并发性能,因为其他事务无法并发地对表中的不同行进行操作。

全局锁(Global Lock)是针对整个数据库进行加锁的机制。当需要对整个数据库进行备份、恢复或其他维护操作时,可以选择对数据库加上全局锁,防止其他事务对数据库进行操作。


S锁(Shared Lock)是一种共享锁,多个事务可以同时对同一资源加上S锁,读取资源但不进行修改。这样可以提高并发性能,允许多个事务同时读取同一资源。

X锁(Exclusive Lock)是一种独占锁,一个事务对某一资源加上X锁后,其他事务无法再对该资源进行读取或修改操作,直到该事务释放锁。


意向锁(Intent Lock)是一种锁的层次结构。当一个事务需要对某一行进行加锁时,会先获取意向锁,表明该事务将对该行进行加锁操作,同时其他事务可以获取表级别的锁,但不能获取同一行的行级别锁。

间隙锁(Gap Lock)是在索引范围内的间隙上设置的锁,用于防止其他事务在这个范围内插入新的记录。间隙锁可以避免幻读问题。

Next Key锁(Next-Key Lock)是InnoDB存储引擎中一种组合锁,结合了行锁和间隙锁的特性。它在行锁和间隙锁之间建立了一个边界,保证了范围查询的正确性,并避免了幻读问题。

记录锁(Record Lock)是行级别的锁,用于保护单个记录。当一个事务对某一行进行修改或删除操作时,会对该行加上记录锁,防止其他事务同时对同一行进行操作。

插入意向锁(Insert Intention Lock)是意向锁的一种,用于在插入新记录时保护间隙。当一个事务在某个间隙内进行插入操作时,会先获取插入意向锁,表明该事务将在该间隙内插入新记录,防止其他事务在同一间隙内插入新记录。


使用场景根据具体的数据库设计和业务需求而定,不同的锁机制适用于不同的并发控制需求。一般来说:

  • 行锁适用于多个事务同时对同一张表的不同行进行并发读写的场景。

  • 页锁适用于同时对同一页上多行进行并发操作的场景,可以提高并发性能。

  • 表锁适用于需要对整个表进行操作的场景,例如备份、恢复或其他维护操作。

  • 全局锁适用于需要对整个数据库进行操作的场景,例如全库备份或整库迁移。

  • S锁适用于多个事务需要同时读取某一资源但不进行修改的场景。

  • X锁适用于一个事务需要独占某一资源并进行读取或修改的场景。

  • 意向锁适用于并发操作的场景,用于协调行级锁和表级锁之间的关系。

  • 间隙锁适用于避免幻读问题的场景,用于保护索引范围内的间隙。

  • Next Key锁适用于范围查询的场景,确保范围内的数据一致性,避免幻读问题。

  • 记录锁适用于对单个记录进行操作的场景,保护特定行的数据一致性。

  • 插入意向锁适用于在间隙内进行插入操作的场景,避免多个事务在同一间隙内插入新记录。

    image-20230613173334125

  • 微信或QQ扫一扫

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注

目录