前言
不管是数据库,还是很多后端编程语言,都存在锁的机制,锁的存在有效解决了并发情况下对共同资源的抢占,保证了数据的稳定性和一致性,在mysql中,锁是如何工作的呢?其底层的工作原理是怎样的呢?本篇将详细介绍下mysql锁的机制。
mysql锁介绍
在数据库中,除传统的计算资源(CPU、RAM、I/O)的争用以外,数据也是一种供许多用户共享的资源。如何保证数据并发访问的一致性、有效性是所有数据库必须解决的一个问题,锁冲突也是影响数据库并发访问性能的一个重要因素。从这个角度来说,锁对数据库而言显得尤其重要,也更加复杂。
MySQL中,按照锁的粒度,可分为以下三类:
- 全局锁:锁定某个数据库的所有表;
- 表级锁:每次操作锁住整张表;
- 行级锁:每次操作锁住对应的行数据;
当然,关于锁的分类,还可以有其他的不同的维度,下面列出了一个更为详细的划分
从操作类型上来看,mysql的锁可以直接划分为:读锁和写锁;
- 读锁 :也称共享锁 、英文用 S 表示。针对同一份数据,多个事务的读操作可以同时进行而不会互相影响,相互不阻塞的;
- 写锁 :也称为 排他锁 、英文用X表示。当前写操作没有完成前,它会阻断其他写锁和读锁,这样就能确保在给定的时间里,只有一个事务能执行写入,并防止其他用户读取正在写入的同一资源;
需要注意的是对于 InnoDB 引擎来说,读锁和写锁可以加在表上,也可以加在行上
mysql 中锁的来源
文章开头提到,mysql的锁存在的根源是为了保证数据的一致性,需要对并发操作进行控制 ,因此产生了锁,也就是锁,对mysql数据库数据的并发访问的存在,这个访问可以是读或者写,或者是读写并存;
我们知道,对mysql的每一次操作,都是在事务中进行的,如果将mysql的锁从并发事务角度来看,访问相同数据的情况大致可划分为3种:
读 - 读
读-读情况,即并发事务相继读取相同的记录 。读取操作本身不会对记录有任何影响,并不会引起什么问题,所以允许这种情况的发生;
写 - 写
写-写 情况,即并发事务相继对相同记录做出改动。在这种情况下可能会造成脏写问题。任何一种隔离级别都不允许这种问题发生。所以在多个未提交事务相继对一条记录做改动时,需要让它们 排队执行 ,这个排队的过程其实是通过锁来实现的;
锁,其实可以设想为内存中的一个逻辑结构,和具体的操作事务有关系,在事务执行前本来是没有锁的,也就是说一开始是没有 锁结构 和记录进行关联的,如下图所示;
但是当一个事务想对这条记录做修改,首先会检查内存中有没有与这条记录关联的锁结构 ,如果没有,就会在内存中生成一个锁结构 与之关联。比如事务T1 要对这条记录做改动,就需要生成一个锁结构与之关联,大致发生的过程如下:
- trx代表操作这条记录的事务ID;
- is_waiting 代表是否被某个事务持有了锁,false加锁失败;
这时候,一个事务也要操作这条记录,将会出现下面的情形,如果获取锁成功,is_waiting状态将变为false,此时如果另一个事务也要对这条记录枷锁,如果获取锁失败,is_waiting变为true;
一旦获取锁成功,这条记录在内存中将会出现下面这样一个锁结构的记录
针对上述情况做简单小结:
- 不加锁,意思就是不需要在内存中生成对应的锁结构,可以直接执行操作;
- 获取锁成功(加锁成功),就是在内存中生成了对应的 锁结构 ,而且锁结构的 is_waiting 属性为 false ,也就是事务可以继续执行操作;
- 获取锁失败(加锁失败),即没有获取到锁,即在内存中生成了对应的锁结构 ,不过锁结构的 is_waiting 属性为true ,也就是事务需要等待,不可以继续执行操作;
以上了解了关于锁的基本概念之后,下面来结合实际操作对mysql常用的几种类型的锁进行深入的使用吧。
一、全局锁
全局锁顾名思义就是对整个数据库加锁,加锁后整个数据库就处于只读状态,后续DML中的insert操作,DDL语句,已经更新操作的事务提交语句都将被阻塞;
全局锁的典型使用场景就是在做全库的逻辑备份时,需要对当前库下的所有表进行锁定,从而获取一致性视图,确保数据的完整性;
为什么全库逻辑备份,就需要加全就锁呢?试想,在对某个表进行数据备份时,还有新的数据在不断的写入,这种备份出来的数据最后可能会存在数据不一致的问题的,这就可以借助mysql的全局锁来解决;
对数据库进行进行逻辑备份之前,先对整个数据库加全局锁,一旦加了全局锁,其他DDL、DML操作将全部处于阻塞状态,但可以执行DQL语句,也就是处于只读状态,而数据备份就是查询操作。那么数据在进行逻辑备份的过程中,数据库中的数据就是不会发生变化的,这就保证了数据的一致性和完整性;
下面来演示下使用全局锁进行数据备份的操作
全局锁备份数据操作过程
1、加全局锁
flush tables with read lock ;
2、数据备份
mysqldump -uroot –p123456 bank1 > /root/bank1.sql
3、备份完毕释放锁
unlock tables ;
操作过程,打开一个会话,使用bank1这个数据库,并加全局锁,此时在启动另一个会话窗口,执行update,看看会发生什么情况
此时发现右边的会话窗口阻塞在那里了,当数据备份完成,执行解锁命令,可以看到右边的update语句就执行成功;
全局锁小结
- 数据库中加全局锁,是一个比较重的操作,一般要慎用;
- 如果在主库上备份(主从架构),那么在备份期间都不能执行数据更新,业务基本上就处于停摆状态;
- 如果在从库上备份,那么在备份期间从库不能执行主库同步过来的二进制日志(binlog),会导致主从延迟;
在InnoDB引擎中,可以在备份时加上参数 --single-transaction 参数来完成不加锁的一致性数据备份。
mysqldump --single-transaction -uroot –p123456 bank1 > /root/bank1.sql
二、表级锁
表级锁,每次操作时锁住整张表。锁定粒度大,发生锁冲突的概率最高,并发度最低。应用在MyISAM、InnoDB、BDB等存储引擎中。
1、表锁
对于表级锁,细化下去的话,主要分为下面几类:
- 表锁;
- 元数据锁;
- 意向锁;
表锁从操作效果上来看,可以分为2类,分别为:表共享读锁,和表独占写锁;
表锁语法
- 加锁:lock tables 【表名】... read/write;
- 释放锁:unlock tables ;
表锁演示过程
1、左侧为客户端一,对t_user表加读锁,不影响右侧客户端读,但会阻塞右侧客户端写;
2、左侧为客户端一,对t_user表加写锁,会阻塞右侧客户端的读和写;
关于表的读写锁,总结下就是:
- 读锁不会阻塞其他客户端的读,但会阻塞写;
- 写锁既会阻塞其他客户端的读,也会阻塞其他客户端的写;
2、元数据锁
元数据锁,简写为MDL(meta data lock);
元数据,可简单理解为就是一张表的表结构(建表语句)。 也就是说,某一张表涉及到未提交的事务时,是不能够修改这张表的表结构的;
MDL加锁过程属于引擎的系统行为,在访问表的时候会自动加上。MDL锁主要作用是维护表元数据的数据一致性,在表上有活动事务的时候,不可以对元数据进行写入操作。目的是为了避免DML与DDL冲突,以保证读写数据的正确性;
在MySQL5.5中引入了MDL,在对一张表进行增删改查的时候,加MDL读锁(共享);当对表结构进行变更操作的时候,加MDL写锁(排他锁);
在常见的sql操作中,涉及到的元数据锁类型锁总结如下:
SQL | 锁类型 | 说明 |
lock tables xxx read /write | SHARED_READ_ONLY / SHARED_NO_READ_WRITE |
|
select 、select ... lock in share mode |
SHARED_READ | 与SHARED_READ、 SHARED_WRITE兼容,与 EXCLUSIVE互斥 |
insert 、update、 delete、select ... for update |
SHARED_WRITE | 与SHARED_READ、 SHARED_WRITE兼容,与 EXCLUSIVE互斥 |
alter table ... | EXCLUSIVE | 与其他的MDL都互斥 |
可以通过下面的SQL,来查看数据库中的元数据锁的情况:
select object_type,object_schema,object_name,lock_type,lock_duration from
performance_schema.metadata_locks ;
元数据锁演示过程
执行SELECT、INSERT、UPDATE、DELETE等语句时,添加的是元数据共享锁,是兼容的;
1、左侧窗口开启事务,执行查询(开启元数据共享锁),右侧窗口开启另一个事务,执行修改操作,可以正常执行;
2、左侧窗口执行SELECT语句时,添加的是元数据共享锁,会阻塞元数据排他锁,它们之间是互斥的;
3、意向锁
InnoDB 支持多粒度锁,即它允许行级锁与表级锁 共存,而意向锁就是其中的一种 表锁 。比如为了避免DML在执行时,加的行锁与表锁的冲突,在InnoDB中引入意向锁,使得表锁不用检查每行数据是否加锁,使用意向锁来减少表锁的检查,一定程度上提升执行效率。
意向锁分为两种:
1)意向共享锁(intention shared lock, IS):事务有意向对表中的某些行加共享锁(S锁);
-- 事务要获取某些行的 S 锁,必须先获得表的 IS 锁。
SELECT column FROM table ... LOCK IN SHARE MODE;
2)意向排他锁(intention exclusive lock, IX):事务有意向对表中的某些行加排他锁(X锁);
-- 事务要获取某些行的 X 锁,必须先获得表的 IX 锁。
SELECT column FROM table ... FOR UPDATE;
注意:
意向锁是由存储引擎 自己维护 ,用户无法手动操作意向锁,在为数据行加共享 / 排他锁之前,InooDB 会先获取该数据行 所在数据表的对应意向锁 。
具体来说,当第一个客户端开启一个事务,执行DML语句时,会对涉及到的数据行加一个行锁;
这时候,客户端二,想对这张表加表锁时,先会检查当前表是否有对应的行锁,如果没有,则添加表锁,此时就会从第一行数据开始扫描,检查到最后一行数据,如果使用这种方式,效率会很低;
而有了意向锁之后,客户端一,在执行DML操作时,会对涉及的行加行锁,同时也会给该表加上意向锁;
对于其他客户端来说,在对这张表加表锁的时候,会根据该表上所加的意向锁来判定是否可以成功加表锁,就不用逐行判断行锁情况了。
一旦事务提交了,意向共享锁、意向排他锁,都会自动释放
可通过下面的sql,查看意向锁及行锁的加锁情况:
select object_schema,object_name,index_name,lock_type,lock_mode,lock_data
from performance_schema.data_locks;
意向锁操作演示
1、意向共享锁与表读锁兼容,左侧会话窗口给当前id为1的记录加了一个行锁同时也加了意向锁,右侧的窗口再次给这个表添加读锁时加锁成功;
2、意向排他锁与表读锁、写锁都是互斥的,左侧会话窗口修改id为1的数据时给表添加行锁同时加了意向排他锁,其他事务再锁表的时候出现阻塞;
更多的场景有兴趣的同学可以自行操作体验下,下面对意向锁做过简单的小结
- InnoDB 支持 多粒度锁 ,特定场景下,行级锁可以与表级锁共存;
- 意向锁之间互不排斥,但除了 IS 与 S 兼容外,意向锁会与共享锁 / 排他锁 互斥 ;
- IX,IS是表级锁,不会和行级的X,S锁发生冲突,只会和表级的X,S发生冲突;
- 意向锁在保证并发性的前提下,实现了行锁和表锁共存且满足事务隔离性的要求;
三、行锁
顾名思义,行锁表示操作表的具体数据行时涉及到的锁,InnoDB实现了下面两种类型的行锁:
- 共享锁(S):允许一个事务去读一行,阻止其他事务获得相同数据集的排它锁;
- 排他锁(X):允许获取排他锁的事务更新数据,阻止其他事务获得相同数据集的共享锁和排他锁;
共享锁与排他锁之间的兼容情况如下:
当前锁与请求锁类型 | S(共享锁) | X(排他锁) |
S(共享锁) | 兼容 | 冲突 |
X(排他锁) | 冲突 | 冲突 |
简而言之:
- 共享锁与共享锁兼容,与排他锁冲突;
- 排他锁与共享锁以及排他锁都冲突;
常见的SQL语句,在执行时,所加的行锁如下:
SQL语句 | 行锁类型 | 说明 |
INSERT/UPDATE/DELETE ... | 排他锁 | 自动加锁 |
SELECT | 不加锁 | |
SELECT ... LOCK IN SHARE MODE |
共享锁 | 需要手动在SELECT之后加LOCK IN SHARE MODE |
SELECT ... FOR UPDATE | 排他锁 | 需要手动在SELECT之后加FOR UPDATE |
行级锁,每次操作锁住对应的行数据,锁定粒度最小,发生锁冲突的概率最低,并发度最高。应用在InnoDB存储引擎中;
InnoDB数据基于索引进行组织,行锁是通过对索引上的索引项加锁来实现的,而不是对记录加的锁。对于行级锁,主要分为以下三类:
1)行锁(Record Lock):锁定单个行记录的锁,防止其他事务对此行进行update和delete。在RC、RR隔离级别下都支持;
2)间隙锁(Gap Lock):锁定索引记录间隙(不含该记录),确保索引记录间隙不变,防止其他事务在这个间隙进行insert,产生幻读。在RR隔离级别下都支持;
3)临键锁(Next-Key Lock):行锁和间隙锁组合,同时锁住数据,并锁住数据前面的间隙Gap。在RR隔离级别下支持;
操作演示
1、执行select语句时,不会加锁
2、select...lock in share mode,加共享锁,共享锁与共享锁之间兼容
3、共享锁与排他锁之间互斥
4、排他锁与排他锁之间互斥
注意,以上的操作是基于mysql默认情况下,即事务隔离级别为REPEATABLE READ,在这个事务隔离级别下,InnoDB使用 next-key 锁进行搜索和索引扫描,以防止幻读;
- 针对唯一索引进行检索时,对已存在的记录进行等值匹配时,将会自动优化为行锁;
- InnoDB行锁是针对于索引加锁,不通过索引条件检索数据,那么InnoDB将对表中的所有记录加锁,此时 就会升级为表锁;
5、无索引行锁升级为表锁
在左侧客户端中,开启事务,并执行update语句,更新name为“曹操”的数据,也就是id为3的记录,再在右侧客户端更新id为11的记录,却会处于阻塞状态,就是因为此时,客户端一,根据name字段进行更新时,name字段是没有索引的,如果没有索引,此时行锁会升级为表锁(因为行锁是对索引项加的锁,而name没有索引);
间隙锁与临键锁
mysql默认情况下,即事务隔离级别为REPEATABLE READ,在这种隔离级别下,InnoDB使用 next-key 锁进行搜索和索引扫描,以防止幻读,工作原理如下:
-
索引上的等值查询 ( 唯一索引 ) ,给不存在的记录加锁时 , 优化为间隙锁 ;
-
索引上的等值查询(非唯一普通索引),向右遍历时最后一个值不满足查询需求时,next-keylock退化为间隙锁;
-
索引上的范围查询(唯一索引)--会访问到不满足条件的第一个值为止;
注意:间隙锁目的是防止其他事务插入间隙。间隙锁可以共存,一个事务采用的间隙锁不会阻止另一个事务在同一间隙上采用间隙锁;
间隙锁
MySQL 在REPEATABLE READ 隔离级别下是可以解决幻读问题的,解决方案有两种,可以使用MVCC方案解决,也可以采用加锁方案解决。但在使用加锁方案解决时有个大问题,就是事务在第一次执行读取操作时,那些幻影记录尚不存在,我们无法给这些 幻影记录 加上 记录锁 。InnoDB提出了一种称之为Gap Locks 的锁,官方的类型名称为: LOCK_GAP ,我们可以简称为 gap锁 。比如,把id值为8的那条记录加一个gap锁的示意图如下:
图中id值为8的记录加了gap锁,意味着不允许别的事务在id值为8的记录前边的间隙插入新记录,其实就是id列的值(3,8)这个区间的新记录是不允许立即插入的;
比如,有另外一个事务再想插入一条id值为4的新记录,它定位到该条新记录的下一条记录的id值为8,而这条记录上又有一个gap锁,所以就会阻塞插入,操作,直到拥有这个gap锁的事务提交了之后,id列的值在区间(3, 8)中的新记录才可以被插入;
注意:gap 锁的提出仅仅是为了防止插入幻影记录而提出的
临间锁
如果我们既想锁住某条记录,又想阻止其他事务在该记录前边的间隙插入新记录 ,该怎么办呢?于是InnoDB就提出了一种称之为 Next-Key Locks 的锁,官方的类型名称为: LOCK_ORDINARY ,也可以简称为next-key锁 。Next-Key Locks是在存储引擎 innodb 、事务级别在可重复读的情况下使用的数据库锁,
innodb默认的锁就是Next-Key locks。
操作演示
1、索引上的等值查询(唯一索引),给不存在的记录加锁时, 优化为间隙锁;
左侧开启一个事务对,对3和11之间的一条不存在的数据执行update操作,右侧执行insert操作
由于11之前的数据被锁住了,所以无法插入id为9的数据;
2、索引上的等值查询(非唯一普通索引),向右遍历时最后一个值不满足查询需求时,next-keylock 退化为间隙锁;
如下图所示,如果要查询的字段,比如age加了一个普通索引(非唯一索引),如果要根据这个二级索引查询值为18的数据,并加上共享锁,只锁定18的这行数据是不行的,由于是非唯一索引,这个结构中可能有多个18的存在,所以,在加锁时会继续往后找,找到一个不满足条件的值(即下图中的29这条数据)。此时会对18加临键锁,并对29之前的间隙加锁;
首先给age字段加索引
create index index_age on t_user(age);
左侧的会话窗口中对age=35的数据加锁,由于为临键锁,所以这之前的数据都会被锁住,右侧会话插入一条id落在1~11范围内的数据时会阻塞;
3、索引上的范围查询(唯一索引),会访问到不满足条件的第一个值为止
对id >=9 的数据添加共享锁,于是id大于11的数据将会形成间隙锁,右侧会话执行insert的时候将会被阻塞;