mysql的锁
一,锁的分类
①,以锁粒度的维度划分:
- 表锁:
- 全局锁:加上全局锁之后,整个数据库只能允许读,不允许做任何写操作。
- 元数据锁 /
MDL锁:基于表的元数据加锁,加锁后整张表不允许其他事务操作。 - 意向锁:这个是
InnoDB中为了支持多粒度的锁,为了兼容行锁、表锁而设计的。 - 自增锁 /
AUTO-INC锁:这个是为了提升自增ID的并发插入性能而设计的。
- 行锁:
- 记录锁 /
Record锁:也就是行锁,一条记录和一行数据是同一个意思。 - 间隙锁 /
Gap锁:InnoDB中解决幻读问题的一种锁机制。 - 临键锁 /
Next-Key锁:间隙锁的升级版,同时具备记录锁+间隙锁的功能。 - 插入意向锁
- 记录锁 /
- 页面锁
②,以互斥性的维度划分:
- 共享锁 /
S锁:不同事务之间不会相互排斥、可以同时获取的锁。 - 排他锁 /
X锁:不同事务之间会相互排斥、同时只能允许一个事务获取的锁。 - 共享排他锁 /
SX锁:MySQL5.7版本中新引入的锁,主要是解决SMO带来的问题。
③,以操作类型的维度划分:
- 读锁:查询数据时使用的锁。
- 写锁:执行插入、删除、修改、
DDL语句时使用的锁。
④,以加锁方式的维度划分:
- 显示锁:编写
SQL语句时,手动指定加锁的粒度。 - 隐式锁:执行
SQL语句时,根据隔离级别自动为SQL操作加锁。
⑤,以思想的维度划分:
- 乐观锁:每次执行前认为自己会成功,因此先尝试执行,失败时再获取锁。
- 悲观锁:每次执行前都认为自己无法成功,因此会先获取锁,然后再执行。
放眼望下来,是不是看着还蛮多的,但总归说来说去其实就共享锁、排他锁两种,只是加的方式不同,加的地方不同,因此就演化出了这么多锁的称呼
二,共享锁和排他锁
有些地方也将共享锁称之为读锁,排他锁称之为写锁,但这个说法并不完全正确,因为同时发生读读操作时,这里的读操作表现是可以共享,但是同时发生读写操作时,这里的读操作表现可以是排他的,比如:一条线程在读数据时加了一把锁(读锁),此时当另外一条线程来尝试对相同数据做写操作时,这条线程会陷入阻塞,因为mysql中一条线程在读时不允许其他线程改。因此共享锁称之为读锁,排他锁称之为写锁这说法并不正确,共享锁就是共享锁,排他锁就是排他锁,不能与读锁、写锁混为一谈
2.1 共享锁
共享锁的意思很简单,也就是不同事务之间不会排斥,可以同时获取锁并执行,这就类似于之前聊过的,但这里所谓的不会排斥,仅仅只是指不会排斥其他事务来读数据,但其他事务尝试写数据时,就会出现排斥性。
在MySQL中,我们可以在SQL语句后加上相关的关键字来使用共享锁,语法如下:
SELECT ... LOCK IN SHARE MODE;
-- MySQL8.0之后也优化了写法,如下:
SELECT ... FOR SHARE; 这种通过在SQL后添加关键字的加锁形式,被称为显式锁,而实际上为数据库设置了不同的事务隔离级别后,MySQL也会对SQL自动加锁,这种形式则被称之为隐式锁。
举个例子理解共享锁:
打开两个cmd窗口并与MySQL建立连接,执行如下命令:
begin;
select * from user where id = 1 lock in share mode;
此时两个事务都是执行查询的操作,可以正常执行。
紧接着再在白色窗口中,尝试修改ID=1的数据:
update user set name = '小3' where id = 1;
一个事务尝试对具备共享锁的数据进行写操作时,会被共享锁排斥,共享锁表现出了排他性。
2.2 排他锁
上面简单的了解了共享锁之后,紧着来看看排他锁,排他锁也被称之为独占锁。当一个线程获取到独占锁后,会排斥其他线程,如若其他线程也想对共享资源/同一数据进行操作,必须等到当前线程释放锁并竞争到锁资源才行。
在MySQL中,可以通过如下方式显式获取独占锁:
SELECT ... FOR UPTATE;举个例子理解独占锁:

当一个事务获取到排他锁,另一个事务也尝试获取排他锁,读取一条相同的数据时,就会陷入阻塞。

当一个事务获取到排他锁,另一个事务尝试获取共享锁,读取一条相同的数据时,就也会陷入阻塞。

当一个事务获取到排他锁,另一个事务不获锁读数据,而是以普通的方式读数据,这种方式则可以立刻执行,这是为什么呢?是因为读操作默认加共享锁吗?显然不是第二个例子证明尝试获取共享锁时,也会陷入阻塞,究竟是因为啥原因才导致的能读到数据,其实这跟另一种并发控制技术有关,即MVCC机制。
在前面的实验中,每次都仅获取了锁,但好像从未释放过锁呀?其实MySQL中释放锁的动作都是隐式的,毕竟如果交给咱们来释放,很容易由于操作不当造成死锁问题发生,在不同的隔离级别中也并不相同,比如在“读未提交”级别中,是SQL执行完成后就立马释放锁,而在“可重复读”级别中,是在事务结束后才会释放。
2.3 共享排他锁
MySQL5.7.2版本中引入了一种新的锁,被称之为(SX)共享排他锁,这种锁是共享锁与排他锁的杂交类型,至于为何引入这种锁呢?聊它之前需要先理解SMO问题
在SQL执行期间一旦更新操作触发B+Tree叶子节点分裂,那么就会对整棵B+Tree加排它锁,这不但阻塞了后续这张表上的所有的更新操作,同时也阻止了所有试图在B+Tree上的读操作,也就是会导致所有的读写操作都被阻塞,其影响巨大。因此,这种大粒度的排它锁成为了InnoDB支持高并发访问的主要瓶颈,而这也是MySQL 5.7版本中引入SX锁要解决的问题。
那想一下该如何解决这个问题呢?最简单的方式就是减小SMO问题发生时,锁定的B+Tree粒度优化,当发生SMO问题时,就只锁定B+Tree的某个分支,而并不是锁定整颗B+树,从而做到不影响其他分支上的读写操作。
在聊之前首先得搞清楚SQL执行时的几个概念:
- 读取操作:基于
B+Tree去读取某条或多条行记录。 - 乐观写入:不会改变
B+Tree的索引键,仅会更改索引值,比如主键索引树中不修改主键字段,只修改其他字段的数据,不会引起节点分裂。 - 悲观写入:会改变
B+Tree的结构,也就是会造成节点分裂,比如无序插入、修改索引键的字段值。
上述讲到的三种执行情况,对于读操作、乐观写入操作而言,并不会加SX锁,共享排他锁仅针对于悲观写入操作会加,由于读操作、乐观写入执行前对整颗树加的是S锁,因此悲观写入时加的SX锁并不会阻塞乐观写入和读操作,但当另一个事务尝试执行SMO操作变更树结构时,也需要先对树加上一个SX锁,这时两个悲观写入的并发事务就会出现冲突,新来的事务会被阻塞。
三,表锁
表锁应该是听的最多的一种锁,因为实现起来比较简单,同时应用范围也比较广泛,几乎所有的存储引擎都会支持这个粒度的锁,比如常用的MyISAM、InnoDB、Memory等各大引擎都实现了表锁,但要注意,不同引擎的表锁也在实现上以及加锁方式上有些许不同,但归根结底,表锁的意思也就以表作为锁的基础,将锁加在表上,一张表只能存在一个同一类型的表锁。
比如InnoDB是一个支持多粒度锁的存储引擎,它的锁机制是基于聚簇索引实现的,当SQL执行时,如果能在聚簇索引命中数据,则加的是行锁,如无法命中聚簇索引的数据则加的是表锁,比如:
select * from user for update;这条SQL就无法命中聚簇索引,此时自然加的就是表级别的排他锁,但是这个表级锁,并不是真正意义上的表锁,是一个“伪表锁”,但作用是相同的,锁了整张表.
而反观MyISAM引擎,由于它并不支持聚簇索引,所以无法再以InnoDB的这种形式去对表上锁,因此如若要在MyISAM引擎中使用表锁,又需要使用额外的语法,如下:
-- MyISAM引擎中获取读锁(具备读-读可共享特性)
LOCK TABLES `table_name` READ;
-- MyISAM引擎中获取写锁(具备写-读、写-写排他特性)
LOCK TABLES `table_name` WRITE;
-- 查看目前库中创建过的表锁(in_use>0表示目前正在使用的表锁)
SHOW OPEN TABLES WHERE in_use > 0;
-- 释放已获取到的锁
UNLOCK TABLES举例理解MyISAM的表锁:
①,读读:不阻塞

②,读写:阻塞

③,写读:阻塞

④,写写:阻塞

MyISAM引擎中,获取了锁还需要自己手动释放锁,否则会造成死锁现象出现,因为如果不手动释放锁,就算事务结束也不会自动释放,除非当前的数据库连接中断时才会释放。如下:

3.1 元数据锁
Meta Data Lock元数据锁,也被简称为MDL锁,这是基于表的元数据加锁,想要搞懂元数据锁,首先要知道元数据是什么?
所有存储引擎的表都会存在一个.frm文件,这个文件中主要存储表的结构,而MDL锁就是基于.frm文件中的元数据加锁的。
对于这种锁是在MySQL5.5版本后再开始支持的,一般来说咱们用不上,因此也无需手动获取锁,这个锁主要是用于:更改表结构时使用,比如你要向一张表创建/删除一个索引、修改一个字段的名称/数据类型、增加/删除一个表字段等这类情况。
毕竟当你的表结构正在发生更改,假设此时有其他事务来对表做CRUD操作,自然就会出现问题,比如我刚删了一个表字段,结果另一个事务中又按原本的表结构插入了一条数据,这显然会存在风险,因此MDL锁在加锁后,整张表不允许其他事务做任何操作。
3.2 意向锁
InnoDB引擎是一种支持多粒度锁的引擎,而意向锁则是为了实现InnoDB支持多粒度,兼容行锁、表锁而设计的锁。
怎么理解这句话呢?先来看一个例子:
假设一张表中有千万条数据,现在事务T1对ID=800W的这条数据加了一个行锁,此时来了一个事务T2,想要获取这张表的表级别写锁,经过前面的一系列讲解,大家应该知道写锁必须为排他锁,也就是在同一时刻内,只允许当前事务操作,如果表中存在其他事务已经获取了锁,目前事务就无法满足“独占性”,因此不能获取锁。
那思考一下,由于T1是对ID=800W的数据加了行锁,那T2获取表锁时,是不是得先判断一下表中是否存在其他事务在操作?但因为InnoDB中有行锁的概念,所以表中任何一行数据上都有可能存在事务加锁操作,为了能精准的知道答案,MySQL就得将整张表的1000W条数据全部遍历一次,然后逐条查看是否有锁存在,那这个效率自然会非常的低。
有人可能会说,慢就慢点怎么了,能接受!但实际上不仅仅存在这个问题,还有另外一个致命问题,比如现在MySQL已经判断到了第500W行数据,发现前面的数据上都没有锁存在,正在继续往下遍历,万一又来了一个事务在扫描过的数据行上加了个锁怎么办?比如在第100W条数据上加了一个行锁。那难道又重新扫描一遍嘛?这就陷入了死循环,行锁和表锁之间出现了兼容问题。
也正是由于行锁和表锁之间存在兼容性问题,所以意向锁它来了!意向锁实际上也是一种特殊的表锁,意向锁其实是一种“挂牌告知”的思想,好比日常生活中的出租车,一般都会有一个牌子,表示它目前是“空车”还是“载客”状态,而意向锁也是这个思想。
比如T1要对ID=800W的数据加一个行级别的读锁,就会先添加一个表级别的意向共享锁,如果T1要加行级别的写锁,亦是同理。
这个时候T2要获取的表级别的锁,就不需要判断每一条数据有没有加锁了。
3.3 自增锁
自增锁,这个是专门为了提升自增ID的并发插入性能而设计的,通常情况下咱们在建表时,都会对一张表的主键设置自增特性,如下:
CREATE TABLE `table_name` (
`xx_id` NOT NULL AUTO_INCREMENT,
.....
) ENGINE = InnoDB; 当对一个字段设置AUTO_INCREMENT自增后,意味着后续插入数据时无需为其赋值,系统会自动赋上顺序自增的值。但想一想,比如目前表中最大的ID=88,如果两个并发事务一起对表执行插入语句,由于是并发执行的原因,所以有可能会导致插入两条ID=89的数据。因此这里必须要加上一个排他锁,确保并发插入时的安全性,但也由于锁的原因,插入的效率也就因此降低了,毕竟将所有写操作串行化了。
为了改善插入数据时的性能,自增锁诞生了,自增锁也是一种特殊的表锁,但它仅为具备AUTO_INCREMENT自增字段的表服务,同时自增锁也分成了不同的级别,可以通过innodb_autoinc_lock_mode参数控制。
innodb_autoinc_lock_mode = 0:传统模式。innodb_autoinc_lock_mode = 1:连续模式(MySQL8.0以前的默认模式)。innodb_autoinc_lock_mode = 2:交错模式(MySQL8.0之后的默认模式)。
简单了解上述三种插入模式后,再用一句话来概述自增锁的作用:自增锁主要负责维护并发事务下自增列的顺序,也就是说,每当一个事务想向表中插入数据时,都要先获取自增锁先分配一个自增的顺序值,但不同模式下的自增锁也会有些许不同。
3.4 全局锁
全局锁其实是一种尤为特殊的表锁,其实将它称之为库锁也许更合适,因为全局锁是基于整个数据库来加锁的,加上全局锁之后,整个数据库只能允许读,不允许做任何写操作,一般全局锁是在对整库做数据备份时使用。
-- 获取全局锁的命令
FLUSH TABLES WITH READ LOCK;
-- 释放全局锁的命令
UNLOCK TABLES;从上述的命令也可以看出,为何将其归纳到表锁范围,因为获取锁以及释放锁的命令都是表锁的命令。
四,行锁
在MySQL诸多的存储引擎中,仅有InnoDB引擎支持行锁,这是由于什么原因导致的呢?因为InnoDB支持聚簇索引,InnoDB中如果能够命中索引数据,就会加行锁,无法命中则会加表锁。InnoDB会将表数据存储在聚簇索引中,每条行数据都会存储在树中的叶子节点上,因此行数据是“分开的”,所以可以对每一条数据上锁,但其他引擎大部分都不支持聚簇索引,表数据都是一起存储在一块的,所以只能基于整个表数据上锁,这也是为什么其他引擎不支持行锁的原因。
4.1 记录锁
Record Lock记录锁,实际上就是行锁,一行表数据、一条表记录本身就是同一个含义,因此行锁也被称为记录锁。使用方式如下:
-- 获取行级别的 共享锁
select * from tablename where userId = 1 lock in share mode;
-- 获取行级别的 排他锁
select * from tablename where userId = 1 for update;想要使用InnoDB的行锁就是这样写的,如果你的SQL能命中索引数据,那也就自然加的就是行锁,反之则是表锁.
4..2 间隙锁
间隙锁是对行锁的一种补充,主要是用来解决幻读问题的,但想要理解它,咱们首先来理解啥叫间隙:
SELECT * FROM user;
+---------+-----------+
| user_id | user_name |
+---------+-----------+
| 1 | 小1 |
| 2 | 小2 |
| 3 | 小3 |
| 4 | 小4 |
| 9 | 小9 |
+---------+-----------+上述这张表最后两条数据,ID字段之间从4跳到了9,那么4~9两者之间的范围则被称为“间隙”,而间隙锁则主要锁定的是这块范围。
那为何又说间隙锁是用来解决幻读问题的呢?
因为幻读的概念是:一个事务在执行时,另一个事务插入了一条数据,从而导致第一个事务操作完成之后发现结果与预想的不一致,跟产生了幻觉一样。
举例子,现在要将ID>3的用户名重置为小x,因此事务T1先查到了ID>3的4、9两条数据并上锁了,然后开始更改用户名,但此时事务T2过来又插入了一条ID=6、userName=小6的数据并提交,等T1修改完了4、9两条数据后,此时再次查询ID>3的数据时,结果发现了ID=6的这条数据并未被重置用户名。
在上述这个例子中,T2因为新增并提交了事务,所以T1再次查询时也能看到ID=6的这条数据,就跟产生了幻觉似的,对于这种新增数据,专业的叫法称之为幻影数据。
为了防止出现安全问题,所以T1在操作之前会对目标数据加锁,但在T1事务执行时,这条幻影数据还不存在,因此就会出现一个新的问题:不知道把锁加在哪儿,毕竟想要对ID=6的数据加锁,就是加了个寂寞。
那难道不加锁了吗?肯定得加锁,但怎么加呢?普通的行锁就已经无法解决这个问题了,总不能加表锁吧,那也太影响性能了,所以间隙锁应运而生!间隙锁的功能与它的名字一样,主要是对间隙区域加锁,举个例子:
select * from user where user_id = 6 lock in share mode; 这条加锁的SQL看起来似乎不是那么合理对吧?毕竟ID=6的数据在表中还没有呀,咋加锁呢?其实这个就是间隙锁,此时会锁定{4~9}之间、但不包含4、9的区域,因为间隙锁是遵循左右开区间的原则
简单说一下结论:当对一个不存在的数据加锁后,默认就是锁定前后两条数据之间的区间,当其他事务再尝试向该区间插入数据时,就会陷入阻塞,只有当持有间隙锁的事务结束后,才能继续执行插入操作。
间隙锁加在不同的位置,锁定的范围也并不相同,如果加在两条数据之间,那么锁定的区域就是两条数据之间的间隙。如果加在上表ID=1的数据上,锁定的区域就是{-∞ ~ 1},即无穷小到1之间的区域。如果加在ID=9之后,锁定的区域就是{9 ~ +∞},即9之后到无穷大的区域。
4..3 临键锁
临键锁是间隙锁的Plus版本,或者可以说成是一种由记录锁+间隙锁组成的锁:
- 记录锁:锁定的范围是表中具体的一条行数据。
- 间隙锁:锁定的范围是左闭右开的区间,并不包含最后一条真实数据。
而临键锁则是两者的结合体,加锁后,即锁定左闭右开的区间,也会锁定当前行数据。
举个例子:
select * from user where user_id = 9 lock in share mode; 事务T1基于表中ID=9的这条数据加锁的,此时来看结果,除开锁定了4~9这个区间外,对于ID=9这条数据也锁定了,在事务T2中尝试对ID=9的数据修改时,也会让事务陷入阻塞。
临键锁的注意点:当原本持有锁的
T1事务结束后,T2会执行插入操作,这时锁会被T2获取,当你开启一个新的事务T3,再次尝试获取相同的临键锁时,是无法获取的,只能等T2结束后才能获取,因为临建锁包含了记录锁,虽然间隙锁可以同时由多个事务持有,但排他类型的记录锁只允许一个事务持有。
4.4 插入意向锁
插入意向锁,听起来似乎跟前面的表级别意向锁有些类似,但实际上插入意向锁是一种间隙锁,这种锁是一种隐式锁,也就是咱们无法手动的获取这种锁。通常在MySQL中插入数据时,是并不会产生锁的,但在插入前会先简单的判断一下,当前事务要插入的位置有没有存在间隙锁或临键锁,如果存在的话,当前插入数据的事务则需阻塞等待,直到拥有临键锁的事务提交
当持有原本持有临建锁的事务提交后,当前事务即可以获取插入意向锁,然后执行插入操作,当此时如若又来一个新的事务,也要在该区间中插入数据,那新的事务会阻塞吗?答案是不会,可以直接执行插入操作,为什么?
简单来说就是:能够真正执行的插入语句,绝对是通过了唯一检测的,因此插入时可以让多事务并发执行,同时如果设置了自增ID,也会获取自增锁确保安全性,所以当多个事务要向一个区间插入数据时,插入意向锁是不会排斥其他事务的,从这种角度而言,插入意向锁也是一种共享锁。
五,页面锁
页面锁是Berkeley DB存储引擎支持的一种锁粒度,当然,由于BDB引擎被Oracle收购的原因,因此MySQL5.1以后不再直接性的支持该引擎。