目录
[行锁(Record Lock)](#行锁(Record Lock))
[间隙锁(Gap Lock)](#间隙锁(Gap Lock))
[临键锁(Next-Key Lock)](#临键锁(Next-Key Lock))
[插入意向锁(Insert Intention Lock)](#插入意向锁(Insert Intention Lock))
锁是计算机协调多个进程或线程并发访问某一资源的机制。在 MySQL 里,根据加锁的范围,可以分为全局锁、表级锁和行级锁三类。
全局锁
全局锁就是对整个数据库实例加锁,加锁后整个实例就只处于只读状态,后需的DML语句(增删改),DDL语句(修改表字段等等),已经更行操作的事务提交语句都会被阻塞。
开启全局锁命令如下:
bash
flush tables with read lock;
如果要释放全局锁,则执行以下命令
bash
unlock tables;
当然,会话断开的话,全局锁也是会释放的。
全局锁的应用场景
做全库的逻辑备份,对所有的表进行锁定,从而保证数据的完整性。这样可以在备份数据库期间,不会因为数据或表结构的更新,而出现备份文件的数据与预期的不一样。
加全局锁的缺点
如果在主库上进行备份,那么在备份期间都不能执行更新,业务基本上就得停摆;
那有什么办法可以解决?
备份数据库的工具是 mysqldump,在使用 mysqldump 时加上 --single-transaction
参数的时候,就会在备份数据库之前先开启事务。这种方法只适用于支持「可重复读隔离级别的事务」的存储引擎。
在innodb中我们可以在备份时加上参数 -- single- transaction
参数来完成不加锁的一致性数据备份。
bash
mysqldump --single-transaction -uroot -p test >test.sql
InnoDB 存储引擎默认的事务隔离级别正是可重复读,因此可以采用这种方式来备份数据库。
表级锁
表级锁的分类
MySQL 里面表级别的锁有这几种:
- 表锁(表共享读锁(read lock), 表独占写锁(write lock) );
- 元数据锁(MDL);
- 意向锁;
- AUTO-INC 锁;
表锁
表锁有两个:共享读锁(read lock)和 独占写锁(write lock)。
bash
--加锁,只可以对一个表进行加锁
lock talbes 表名 read/write;
--释放锁, 客户端断开连接也会自动释放锁
unlock talbes;
举个例子:只对表A加锁了,却访问表B,这时会显示出错。尝试在一条语句中对多个表加锁,回复是成功,但是使用select查看时候出错。
bash
mysql> show tables;
+----------------+
| Tables_in_test |
+----------------+
| has_key |
| no_key |
| user |
+----------------+
3 rows in set (0.00 sec)
mysql> lock tables user read;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from no_key;
ERROR 1100 (HY000): Table 'no_key' was not locked with LOCK TABLES
mysql> unlock tables;
Query OK, 0 rows affected (0.00 sec)
--尝试一次对多张表加锁, 回复是成功,但是在select时候显示错误
mysql> lock tables user no_key read;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from user;
ERROR 1100 (HY000): Table 'user' was not locked with LOCK TABLES
mysql> select * from no_key;
ERROR 1100 (HY000): Table 'no_key' was not locked with LOCK TABLES
mysql> unlock tables;
Query OK, 0 rows affected (0.00 sec)
mysql> lock tables user,no_key read;
ERROR 1064 (42000): You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near ',no_key read' at line 1
注意:
1.当前会话加了共享读锁后,其他会话不能进行增删改等操作的DDL/DML语句。而当前会话也不允许执行增删改等操作的DDL/DML语句。
2.当前会话加了独占写锁后,当前会话是可以进行查看和修改数据的。而其他会话不可读不可写。
备注:若不明白DDL语句是什么意思,可以查看该文章MySQL的SQL语句
不过应尽量避免在使用 InnoDB 引擎的表使用表锁,因为表锁的颗粒度过大,会影响并发性能。
InnoDB 是实现了颗粒度更细的行级锁, 也是有这个原因,innodb才是MySQL的默认引擎**。**
元数据锁(MDL)
试想在这个场景,客户A在修改表A第一个字段的值,而客户B此时在删除表A结构的第一个字段,那这时候就会出问题了。
那要如何解决这个问题,MySQL5.5中引入了MDL,英文全称是meta data lock。
首先要注意的是,该锁是系统自动控制的,无需显示使用的,在访问一张表的时候会自动加上的。这个特性是在MySQL5.5中引入的。
- 当对一张表进行增删改查的时候,加MDL读锁(共享锁)
- 当对表结构进行变更操作的时候,加MDL写锁(也称为排他锁,独占锁)
metadata lock是表级锁,是在server层加的,适用于所有存储引擎,其主要作用是维护表元数据的数据一致性。是为了避免DML语句和DDL冲突,保证读写的正确性。
MDL 是不需要显示调用,那它是在什么时候释放的?
MDL 是在事务提交后才会释放,这意味着事务执行期间,MDL 是一直持有的。
当开启事务,查询表,另一个会话进行删该表的字段操作时候,操作3会被阻塞,一直等到操作4提交事务后才成功。
换另一种场景,开启事务,进行删除字段操作,而另一会话之后也开启事务,进行查询表操作,而这个查询却没有被阻塞。这是因为这种DDL语句,就是对表,对库有创建,删除,修改的操作语句,都是在语句前就默认自动提交事务的了,不会等到commit才提交事务的。
DDL语句会进行隐式提交,是不能再rollback的,所以大家在操作中一定要注意。
备注:关于不能rollback的语句,可以查看文章MySQL中有事务无法回滚的语句?
还有另一种情况:
3个会话,会话1开启事务,之后查询,会话2想进行DDL语句,会被堵住。会话3也进行查询,也被堵住。
这是因为申请 MDL 锁的操作会形成一个队列,队列中写锁获取优先级高于读锁,一旦出现 MDL 写锁等待,会阻塞后续该表的所有 CRUD 操作。
意向锁
想象一种情况:在进行修改表的某行的时候,是对某行加行锁,那这时其他会话想加表锁,而加表锁前,需要判断该表的行是否被锁住。这时意向锁就派上用场了,就直接查看该表是否有意向锁即可。
简单来说,为了避免DML在执行时,加的行锁与表锁的冲突,在InnoDB中引入了意向锁,使得表锁不用检查每行数据是否加锁,使用意向锁来减少表锁的检查,也是为了快速判断表里是否有记录被加锁。
也就是说,当执行插入、更新、删除操作,需要先对表加上「意向独占锁」,这个是mysql自动添加的。
select 也是可以对记录加共享锁和独占锁的 ,写select语句需要额外添加些关键词。
意向锁有两种:
- 意向共享锁(lS) : 由语句select ... lock in share mode添加,与表锁read兼容,与表锁排它锁write互斥。
- 意向排他锁(IX)∶由insert、update、delete、select ... for update添加,与表锁共享锁read及排它锁write互斥。
意向共享锁和意向独占锁是表级锁,不会和行级的共享锁和独占锁发生冲突, 意向锁之间也不会互斥,意向锁只会和表锁(表共享锁,排它锁)互斥。
AUTO-INC锁
我们可以在表中设定一个自增的主键字段,在后续的插入中,就可有不需要指定主键的值,让数据库自动为主键赋值。这是如何实现的呢?而在并发的情况中,又如何不出现相同的的ID问题?这就需要用到AUTO-INC 锁。
起初,AUTO-INC锁 在插入数据的时候加入,此时其他的事务都无法插入,那么就能维护主键的唯一性和自增,也就是说这把表级锁,在执行插入语句的时候加锁,在执行结束后释放锁,而不是在事务结束后释放锁。
但是,大量的数据进行插入的时候,如果每次插入都需要加一把锁,那么插入的性能将会大大降低,因此,在MySQL5.1.22版本开始,它的实现换了一种方式:
使用轻量级的锁,在插入数据时,给这个字段赋值一个自增的值,然后就释放这个轻量级的锁,不需要等到整个插入语句执行结束再释放锁,它省去了执行的那段时间,只需要申领一个自增ID就释放锁。
在MySQL中,可以通过innodb_autoinc_lock_mode这个系统变量区设置自增的策略
- 当 innodb_autoinc_lock_mode = 0,就采用 AUTO-INC 锁,语句执行结束后才释放锁;
- 当 innodb_autoinc_lock_mode = 2,就采用轻量级锁,申请自增主键后就释放锁,并不需要等语句执行后才释放。
- 当 innodb_autoinc_lock_mode = 1:
- 普通 insert 语句,自增锁在申请之后就马上释放;
- 类似 insert ... select... 这样的批量插入数据的语句,自增锁还是要等语句结束后才被释放;
该参数值为 2 是性能最高的,但是当搭配 binlog 的日志格式是 statement 一起使用的时候,在「主从复制的场景」中会发生数据不一致的问题。
行级锁
InnoDB 引擎是支持行级锁的,而 MyISAM 引擎并不支持行级锁。
InnoDB的数据是基于索引组织的,行锁是通过索引上的索引项加锁来实现的,而不是通过记录来加锁的。
行级锁的分类
- 行锁(Record Lock): 锁定单个行记录的锁,防止其他事务对此进行删除和更新操作。在RC和RR隔离级别都支持。
- 间隙锁(Gap Lock):锁定索引记录间隙(不含该记录),确保索引记录间隙不变,防止其他事务在这个间隙进行insert,产生幻读。在RR隔离级别中支持。
- 临键锁(Next-Key Lock):行锁和间隙锁的组合。同时锁住该记录,并锁住数据前面的间隙Gap。在RR隔离级别支持。
行锁(Record Lock)
也称为记录锁,锁住的是一条记录。
innodb实现了两种类型行锁:共享锁(S 锁)和排它锁(X 锁)。
注意:innnodb的行锁是针对索引加的锁,要是不通过索引条件检索数据,那么innodb将对表中的所有记录加锁,此时升级为表锁。
间隙锁(Gap Lock)
目的是为了解决可重复读隔离级别下幻读的现象。
幻读现象:一个场景,在一个事务A中,先查看数据,有id为1,5,9的数据,没有id=3的这个记录。而后其他事务B添加了id=3这个记录。事务A再次查看,却查到了id=3这条数据。这是幻读。
如何解决,可以在id为(1,5)这个范围内加上锁,这个就是间隙锁。
间隙锁也是分为排它锁和互斥锁,但他们之间都是兼容共存 的。间隙锁的唯一目的是防止其他事务插入间隙,所以 两个事务的间隙锁(锁住同一范围)之间是相互兼容的,不会产生冲突。
有同学肯定会好奇:那在什么情况下会使用间隙锁?是innodb自动添加的,还是需要用户写语句时候额外添加关键字的呢?
这个就稍微复杂点,后序会出一篇文章来详细讲解。临键锁也是如此。
临键锁(Next-Key Lock)
是 Record Lock + Gap Lock 的组合,锁定一个范围,并且也锁定记录本身,比如范围(2,10],是左开右闭区间。而间隙锁就是左开右开区间。
当我们使用了范围查询 ,不仅命中了 Record 记录 ,还包含了 Gap 间隙,在这种情况下我们使用的就是临键锁 ,它也是 MySQL 里面默认的行锁算法。
临键锁 即能保护该记录,又能阻止其他事务将新纪录插入到被保护记录前面的间隙中。
next-key lock 是包含间隙锁+记录锁的**,而记录锁是有互斥情况的。** 所以如果一个事务获取了 X 型(即是排他锁)的 next-key lock, 那么另外一个事务在获取相同范围的 X 型的 next-key lock 时,是会被阻塞的。
插入意向锁(Insert Intention Lock)
插入意向锁是一种特殊的间隙锁,表示插入的意向,只有在 INSERT 的时候才会有这个锁。
但是它不是锁定间隙,而是等待某个间隙。比如上面间隙锁图例子:想要插入 id = 4 的数据 事务A,但由于被间隙锁(1,5)给阻塞了,所以事务 A 会生成一个插入意向锁,表明等待这个间隙锁的释放。
注意:这个锁虽然也叫意向锁,但是和前面的表级意向锁是两个完全不同的概念。
并且插入意向锁之间不会阻塞,因为它们的目的也是只等待这个间隙被释放,所以插入意向锁之间没有冲突。
**插入意向锁只会和间隙锁或 Next-key 锁冲突。**间隙锁唯一的作用就是防止其他事务插入记录造成幻读,而正是由于在执行 INSERT 语句时需要加插入意向锁,而插入意向锁和间隙锁冲突,从而阻止了插入操作的执行。
**插入意向锁的作用?作用很鸡肋,好像是没什么作用。**它的目的不在于锁定资源防止别人访问,更像是为了遵循 MySQL 的锁代码实现而为之。
锁其实就是内存里面的一个结构,每个事务为某个记录或者间隙上锁就是创建一个锁对象来争抢资源。如果某个事务没有抢到资源,那也会生成一个锁对象,只是状态是等待的,而当拥有资源的事务释放锁之后,就会寻找正在等待当前资源的锁结构,然后选一个让它获得资源并唤醒对应的事务使之得以执行。