暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

MySQL之锁机制

蒂姆的冒险之旅 2021-09-26
270

MySQL中存在很多锁相关的内容,且往往与存储引擎、索引、事务隔离级别存在关联,这里主要聊聊InnoDB中的一些锁。

MySQL锁的分类

首先,列举一下MySQL中部分存储引擎支持的锁:

由上图引出的针对不同存储引擎,所支持的各种锁的分类,我们大概知道这里面存在很多锁的类型与具体概念,要完全了解他们很不容易,需要我们在工作中遇到他们、使用他们,并且总结和学习他们。

在这篇文章中,我们可以了解到锁的分类、存储引擎支持的锁、锁的具体实现、锁与索引的关系、锁与事务的关系。

这怎么查看MySQL占用的锁?

MySQL为我们提供了查询当前活跃事务所加的锁,我们可以直接通过命令行查询到MySQL中持有锁的情况:针对库information_schema

# MySQL 8.0 以下版本
# 1.查看MySQL中事务持有锁的情况,可以在TRANSACTION信息部分看到加锁情况
mysql> show engine innodb status;

# 2.查看InnoDB中所有表所有事务加锁情况,但是无法分辨gap、record、next-key锁的具体类型
mysql> select * from information_schema.innodb_locks;

# 3.查看InnoDB下所有表所有事务阻塞的情况,谁被block住了
mysql> select * from information_schema.innodb_locks_wait;

MyISAM引擎支持的锁

MyISAM属于MySQL内的一种存储引擎,myisam只支持表锁,不支持行锁与页级锁

在MyISAM引擎中只支持表级锁,按照分类可以分为:表级共享锁、表级排它锁。

  • 表级共享锁(Table Read Lock, S锁)

    获得表级共享锁的事务可以查询表的任意行数据,在执行select时myisam自动为表增加表级共享锁。

  • 表级排它锁 (Table Write Lock, X锁)

    获得表级排它锁的事务可以更新、删除表任意行数据,在执行update、delete、insert时myisam自动为表增加表排它锁。

针对上面的两种表级锁,MyISAM可以做到:读读共享,读写互斥,写写互斥。互斥发生时将阻塞后加锁的线程,直至先加锁的线程释放排它锁。

InnoDB引擎支持的锁

在MySQL官网中,我们可以查看到InnoDB所持的锁,传送门:InnoDB支持的锁,下面简单列举一下:

  • 共享锁和排他锁 (Shared and Exclusive Locks)

  • 意向锁(Intention Locks)

  • 记录锁(Record Locks)

  • 间隙锁(Gap Locks)

  • 临键锁 (Next-Key Locks)

  • 插入意向锁(Insert Intention Locks)

  • 主键自增锁 (AUTO-INC Locks)

  • 空间索引断言锁(Predicate Locks for Spatial Indexes)

这里总体归纳可以见最上面的“MySQL锁的分类”脑图,下面就针对上面设计的锁进行讲解。

InnoDB的表级锁

我们都知道InnoDB能支持表级锁、行级锁,而MyISAM只支持表锁、不支持行锁,这里主要讲讲表锁。

表锁,并不是一种真正的数据库锁,而是表明的对数据库表层级进行数据锁定,具体实现有多种形式,如:共享锁、排它锁、意向锁。那为什么需要表锁呢?不是有行锁吗?这是因为某些场合下需要用到,通过不同数据层级进行加锁,可以更好的控制数据的安全性。我们知道行锁是锁住具体的行,而表锁是锁住整个表,他们锁的粒度不一样,性能、代价也不一样:粒度越大,代价越小,实现简单,性能降低;粒度越小,代价越大,实现复杂,性能提高。

在InnoDB的表级锁中,最常见的是:表级共享锁、表级互斥锁,我们可以通过:show status like 'table%'
来查询当前数据库使用的锁定情况:


另外,可以通过手动加表锁:

  • lock table T read;  添加表级共享锁

  • lock table T write;  添加表级互斥锁

InnoDB的共享锁和排它锁

InnoDB实现的共享锁和排它锁,在表级有实现,在行级也有实现,这只是体现在不同的数据层级上的一种锁,但是他们的共性是一样的:

  • 共享锁(share lock)即S锁,又称读锁

  • 排它锁(Exclusive Lock) 即X锁,又称写锁、独占锁

如果我们在某个事务中针对表级数据,或者特定行数据增加了共享锁,此时其他事务也可以获取到相同的表级共享锁或行级共享锁,此为:读读共享,而如果有一个事务获取了某块数据的排它锁,则其他事务不可以获得对应排它锁,将会阻塞至第一个事务提交且释放锁后,才能获取到这个排它锁,这为:读写互斥、写写互斥

那怎么获取共享锁和排它锁呢?可以采用以下命令:

  • select * from table lock in share mode; 为table添加表级共享锁

  • select * from talbe for updare;  为table添加表级排它锁

  • select * from table where id = 1 for update;  为table添加行级排它锁

  • select * from table where id = 1 lock in share mode; 为table添加行级共享锁

以上就是共享锁、排他锁的加锁方式。我们主要到,当SQL语句指定了主键(唯一索引)且被命中时,表级锁会降级为行级锁,否则为表级锁。

这里我们做一个表级共享锁和排它锁的实践:

表级共享锁实践:

事务一事务二
lock table user read; -- 表读锁
select * from user; -- 可查select * from user; -- 可查

update user set name = '33' where id = 3; 

-- 报错,当前是读锁,不可更新

update user set name = '33' where id = 3;

-- 等待另外一个事务释放读锁

unlock tables;... 等待...

执行成功!


表级互斥锁实践:

事务一务二
lock table user write; -- 表写锁

select * from user where id = 3;

-- 等待,不可查询

select * from user where id = 3; -- 可查
update user set name = '333' where id = 3; -- 可更新
select * from user where id = 3; -- 可查
unlock tables;

InnoDB的意向锁

InnoDB引擎为我们提供了行级锁定和表级锁定,但是他们存在互相冲突的问题,比如事务A加了数据行级X锁,此时事务B去加表级S锁是会被阻塞的,反之事务A也会被阻塞。但是有时表级锁和行级锁同时存在,更有利于实现多粒度的控制,为了实现这种多粒度的控制,则衍生出了“意向锁”

意向锁,就是不与行级锁冲突的表级锁,他是一种意向,表明在某个时刻,事务将要对某行数据进行加共享锁或排它锁了!针对将要对行增加的S锁、X锁,意向锁也会有区分:

  • 意向共享锁 也称IS锁,表明事务有意向给行数据加S锁前,增加该表的IS锁。

  • 意向排它锁 也称IX锁,表明事务有意向给行数据加X锁前,增加该表的IX锁。

意向锁是一种比较弱的锁,他们相互之间互不互斥,但是对于表级锁则会有:读读共享、读写互斥

       标题意向共享锁(IS)意向排他锁(IX)表级共享锁(S)表级排他锁(X)
意向共享锁(IS)兼容兼容兼容不兼容
意向排他锁(IX)兼容兼容不兼容不兼容
表级共享锁(S)兼容不兼容兼容不兼容
表级排他锁(x)不兼容不兼容不兼容不兼容

值得注意的是,表级的意向锁并不需要开发人员去设置,这是在存储引擎隐式的帮我们做了这个动作,当需要给数据行增加行级S/X锁时,存储引擎会先获取表对应的IS/IX锁。

说到这里,可能大家会有疑惑:怎么突然冒出这么多:表级S锁、表级X锁、表级IS锁、表级IX锁、行级S锁、行级X锁,他们是怎么协作的呢?怎么跟我以前理解的数据库读锁、写锁不一样了呢?确实,介绍到了这一步,会让人产生很大的疑惑,接下来我们慢慢解惑。

首先,我们通俗理解的行锁、写锁其实是表级、行级、S锁、X锁、IS锁、IX锁简化后的一个说法,在InnoDB内部执行时会涉及很多的判断,比如:

  • 我们要对table某行增加S共享锁,则加锁步骤是:1.添加IS锁,2.判断是否有表级X锁,3.添加行S锁

  • 我们要对table某行增加X排它锁,则加锁步骤是:1.添加IX锁,2.判断是否有表级X锁,3.添加行X锁

具体协作我们可以从几个例子来找到规律。

示例一
  1. 事务A向table表申请某一行数据的S锁(行级读锁),InnoDB引擎自动给table表增加IS锁,同时进行锁冲突判定,此时table无表X、表IX锁,也无行X锁。此时增加表IS锁,行S锁成功!

  2. 事务B向table表申请全表数据的S锁(行级读锁),InnoDB引擎自动给table表增加IS锁,同时进行锁冲突判定,此时table无表X锁,有表IS锁,因为表级S锁与表级IS锁兼容,此时加表S锁成功!

  3. 此时事务A占有table表表级IS锁,行级S锁,事务B占有table表表级S锁

示例二
  1. 事务A向table表申请a行数据行级S锁,此时InnoDB做锁冲突判定尝试增加表级IS锁,发现table无锁,则事务A给table表增加表级IS锁,a行行级S锁

  2. 事务B向table表申请全表的表级X锁,此时InnoDB做锁冲突判定尝试增加表级IX锁、表级X锁,发现table存在IS锁,X与IS冲突,则事务B的表级X锁申请被事务A阻塞,直至事务A提交释放锁

  3. 事务C向table表申请b行数据行级X锁,此时InnoDB做锁冲突判定尝试增加表级IX锁,发现table存在IS锁,IX与IS兼容,然后尝试增加b行行级X锁,加锁成功!

InnoDB的记录锁,间隙锁,临键锁

说到InnoDB的记录锁(record lock)、间隙锁(gap lock)、临键锁(next_key lock),也就是我们常说的行锁,只是在一些场景行锁可能并不仅仅值得一条数据,也有可能是多行数据。

记录锁 Record lock

记录锁,也就是我们最常见的行锁,就是对一条数据进行加锁后,就是对该条数据增加了行锁。实现上很简单,如语句:select * from table where id = 1 for update
就是为id=1的数据增加了记录锁。

这里有一点需要注意,我们常常说的给数据加锁,其实在InnoDB内部实现原理是对聚簇索引的加锁,我们知道聚簇索引通常是主键ID,如果没有主键ID则取第一个非空的唯一索引,如果唯一索引不存在则取隐藏的ROW_ID。所以总结就是:InnoDB的行锁,实质就是锁住了聚簇索引的索引记录。

间隙锁 Gap Lock

间隙锁指的就是,锁住索引记录中的一个开区间范围(index1,index2),或者index1之前的范围,或者index2之后的范围,使得其他事务不能在这些区间的数据加锁。

假设有数据库表table,数据:[1,3,5,6,9,12],此时有以下操作:

# 锁住(5, 10)之间的数据,其他事务对id在5~8之间的数据进行加锁,则会被阻塞,这里面包括:id = {6,7,8,9}
mysql> select * from table where id between 5 and 10 for update;

# 锁住(6, ~)之间的数据,其他事务对id在7~ 之间的数据进行加锁,则会被阻塞,这里面包括:id = {7,8,9,10,11,12}
mysql> select * from table where id > 6 for update;

这里,针对间隙锁有几个注意:

  • 间隙锁之间是兼容的,因为他们都是为了防止其他事务在这个间隙‘插入’

  • 间隙锁需要在RR隔离级别下进行,RC级别会有幻读的问题。

临键锁 Next_key Lock

临键锁,其实不算是一种新的锁,而是记录锁和间隙锁的组合,他即锁住了记录本身,也锁住了记录的间隙。

在InnoDB中,临键锁会对数据进行锁定一个范围,左开右闭(index1, index2],我们举个栗子,某个table(id PK, age KEY, name),表中有4条数,则数据和临键锁锁住聚簇索引的区间可能是:

表数据(id, age,name)临键锁区间
(1,10,a)(-∞, 10]
(3,24,b)(10, 24]
(5,32,c)(24, 32]
(7,45,d)(32, 45]

(45, +∞]


如上4条数据,我们可以看到,当我们对其中的数据加锁时,会形成临键锁,且锁住的临键锁区间内不允许其他事务进行加锁操作。
事务A事务B
UPDATE table SET name = Vladimir WHERE age = 24;     (1)
SELECT * FROM table WHERE age = 24 FOR UPDATE;   (2)

INSERT INTO table VALUES(100, 26, 'Ezreal'); (3)失败

INSERT INTO table VALUES(100, 30, 'Ezreal'); (4)失败

INSERT INTO table VALUES(100, 33, 'Ezreal'); (5)成功
们发现,不管在执行(1)还是(2),其中(3)和(4)都是失败的,但(5)是成功。这是由于我们对age=24加锁了,但是age是非唯一索引,所以将会形成临键锁,锁住一块区域(24,32],之内的区域都不能增减。
总结

这节我们讲了记录锁、间隙锁、临键锁,这里做一个总结。

  • 记录锁就是普通的行锁,锁的是单行数据,数据是真实存在的。

  • 间隙锁就是锁住一个区间中的多行数据,这些数据有可能不存在,总体就是锁住真实数据之间的范围。

  • 临键锁就是记录锁+间隙锁的组合,即锁住索引项本身的真实树,也锁住两索引项之间的范围。

插入意向锁

首先,插入意向锁和意向锁完全没有关系,只是名称相似而已。插入意向锁其实是一种特殊的间隙锁,属于行锁。主要针对某个区间范围可以做到写写共享,这个写写共享不是针对同一行数据,而是针对范围内的不同行做到写写共享。总结:引入插入意向锁,是允许多个事务对某个索引区间内的数据做到并发写,只要不是同一行即可。

上面讲到,插入意向表是一种特殊的间隙锁,那么他们有什么区别呢?我们知道,间隙锁是锁住一个区间的多行数据,以达到一种防止其他事务更新这些数据的效果,其中一个作用就是避免了RR级别下的幻读。而插入意向锁引入的目的其实是替换行锁中的间隙锁,因为普通间隙锁锁住间隙的同时是不允许其他事务进入的,这在高并发下可能降低事务的效率,而插入意向锁是允许其他事务对一个间隙内不同的数据并行的写入,这就弥补了普通间隙锁的缺点。

参考文章

https://blog.csdn.net/SnailMann/article/details/88353099


文章转载自蒂姆的冒险之旅,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论