为什么需要引入锁
锁是计算机协调多个进程或线程并发访问某一资源的一种机制,在并发事务下保证数据的正确和唯一性。
图片
锁在 MySQL 中是非常重要的一部分,对 MySQL 的数据访问并发有着举足轻重的影响
MySQL中的锁是在服务器层或存储引擎层实现的,不同的存储引擎的锁机制也有较大的区别。
MySQL锁的实现
很多人都一样,在刚开始学习MySQL中锁的时候,网上一查出来一堆,什么表锁、行锁、读锁、写锁、悲观锁、乐观锁等等等,直接整个人就懵了。
本文我们将以锁粒度的角度去看MySQL锁的分类情况
没事,先看看小许归纳的锁知识大纲,先对锁的位置和锁归属的存储引擎有个前置了解!
图片
全局锁
全局锁就是对整个数据库实例加锁,MySQL有个全局读锁的命令如下:
flush tables with read lock(FTWRL)
执行后,整个数据库就处于只读状态(不能写入) 了,这个时候其他线程执行数据更新语句(数据的增删改),数据定义语句(建表、修改表结构等)等,都会被阻塞。
解锁命令:
unlock tables
使用场景举例:
主要应用于做全库逻辑备份,原理也很简单在全局锁期间数据或表结构不会被更新,备份后文件的数据与预期也就一样了。
当时加上全局锁,意味着整个数据库都是只读状态,如果备份时间过长就导致其他
Mysql中数据备份使用的命令是mysqldump命令
当使用参数-single-transaction的时候,导出数据之前就会启动一个事务,来确保拿到一致性视图,而由于MVCC的支持,这个过程中数据是可以正常更新的,因为读取的数据在更新前已确认。
页锁
页级锁是 MySQL 中比较独特的一种锁定级别,主要应用于 BDB 存储引擎,我们实际中基本上用的是InnoDB引擎,这里对页锁就不多展开了。
表锁
MyISAM和InnoDB都支持表级锁,但是InnoDB默认的是行级锁。
表锁下面又分了以下四种
图片
表锁
顾名思义,就是直接对表进行加锁,可以使用下面命令:
//加读锁
lock tables table_name read;
//加写锁
lock tables table_name write;
// 释放当前会话的所有表锁
unlock tables
如果加的是写锁,当对表进行写操作时也会被阻塞,直到写锁被释放。
不过尽量避免在使用 InnoDB 引擎的表使用表锁,因为表锁的颗粒度太大,会影响并发性能。
元数据锁
MySQL5.5引入了元数据锁(meta data lock - MDL),它不需要显式使用,在访问一个表的时候会被自动加上。
- • 对表数据进行 CRUD 操作时,加 MDL 读锁
- • 对表结构变更操作的时候,加 MDL 写锁
既然是自动加锁,那释放也是自动的!
事务执行期间,MDL 是一直持有的, 在事务提交后MDL才会释放。
意向锁(Intention Lock)
意向锁主要是在对数据表的行记录加共享锁(S锁)、独占锁(X锁)之前,需要先在表级别加上一个意向锁。
在InnoDB引擎中,当执行查询操作,需要先对表加上「意向共享锁」,然后对该记录加【共享锁】
意向锁有两种类型:
意向共享锁(IS锁):一个事务给一个数据行加共享锁时,必须先获得表的意向共享锁
意向独占锁(IX锁): 一个事务给一个数据行加独占锁时,必须先获得表的意向独占锁
为什么需要先加意向锁?
意向锁的目的是更加快速的判断数据表表里是否有记录被加锁。
比如我们要加【独占表锁】,先在表级别加了【意向独占锁】,那么在加【独占锁】时,直接查该表是否有意向独占锁,如果有就表示表记录存在独占锁,这样就不用去遍历表记录去查看行记录是否存在独占锁了。
加锁命令如下:
//加上意向共享锁,然后对读取的记录加共享锁
select ... lock in share mode;
//先表上加上意向独占锁,然后对读取的记录加独占锁
select ... for update;
AUTO-INC锁
字面意思是用来控制自动自增的锁?
是的,一般来说我们会在表中设置一个字段声明 AUTO_INCREMENT 的自增ID字段。
AUTO-INC锁在自增字段起了个什么作用呢?
当使用INSERT语句插入一条新记录时,MySQL会自动为自增字段加锁,防止其他并发的插入操作同时获取相同的自增值。
其他事务要等待,直到执行完插入语句之后才会释放锁。
这就保证了数据表的 AUTO_INCREMENT 字段的值是连续递增。
好吧,原来这个AUTO_INC锁的作用是这样的,以前我还一直不知道呢!
👉 AUTO-INC锁有什么问题?
大批量数据在一条语句中插入时(INSERT SELECT ),会带来一些性能上的影响,从而阻塞其他事务的插入操作!
🚩 MySQL是如何进行AUTO-INC锁性能优化的?
MYSQL 5.1.22版本开始,InnoDB存储引擎使用一种轻量级互斥锁(Mutex)来控制自增列增长
通过参数innodb_autoinc_lock_mode来控制 可以设定3个值分别是0,1,2
- • 0:traditional 每次insert都采用 AUTO-INC 锁,语句执行结束后才释放锁,但并发能力较弱
- • 1:consecutive 对于SIMPLE INSERT,使用轻量级互斥锁,对于BULK INSERT,使用AUTO-inc locking
- • 2:interleaved 采用轻量级锁,申请自增主键后就释放锁,但可能会造成insert分配的id顺序不一致
🚩 一个事务中存在多个insert语句,auto-inc锁是如何申请的?
自增锁跟事务无关,即使多个insert语句在同一个十五中,每个insert还是都会申请罪行的自增锁。
图片
行锁
顾名思义,行锁就是给数据库表中每行数据加锁,行锁是加在索引上的
比如某个表中id字段是主键,如果给id=2这条记录加锁,那这把锁是加在主键索引(聚簇索引)上的
行锁使用分类
我们讲表锁的时候说到了意向锁,在对数据表的行记录加共享锁(S锁)、独占锁(X锁)之前,需要先在表级别加上一个意向锁 。
InnoDB 行级锁按照使用方式分为:共享锁(S锁)、排它锁(X锁)
图片
读锁会阻塞写(X),但是不会堵塞读(S),而写锁则会把读(S)和写(X)都堵塞
对于普通 select 语句,innodb 不会加任何锁。如果想在select操作的时候加上 S锁 或者 X锁,需要我们手动加锁。
//查询记录加共享锁
select ... lock in share mode;
//查询记录加独占锁
select ... for update;
InnoDB 在RR(MySQL默认隔离级别) ,对于 update、delete 和 insert 语句, 会自动给涉及的数据集加排它锁(X)
InnoDB支持3种行锁的算法,分别是:
- • Record Lock: 单个行记录上的锁
• Gap Lock: 间隙锁,锁定一个范围,但不包含记录本身
• Next-Key Lock: Gap Lock与Record Lock的结合,锁定一个范围,并且锁定记录本身
我们在分析行锁三种算法是要结合存在共享锁(S)和排他锁(X)场景,我们接着看这三种
记录锁 Record Lock
Record Lock 称为记录锁,锁住的是一条记录
SELECT * FROM `demo` WHERE `id`= 23 FOR UPDATE;
上面SQL在 id = 23 的记录上加上记录锁(X锁),这样其他事务就无法插入,更新,删除 id=23 这一行。
下面SQL是对主键索引 与 唯一索引 对数据行进行 UPDATE 操作时,也会对该行数据加记录锁:
UPDATE demo SET name = 'xiaoxu' WHERE id = 23;
记录锁是锁住记录,锁住索引记录,而不是真正的数据记录。
🚩 表中没有建索引怎么办?
即使该表上没有任何索引,那么innodb会在后台创建一个隐藏的聚集主键索引,那么锁住的就是这个隐藏的聚集主键索引。
间隙锁 GAP Lock
间隙锁 是 InnoDB 在 RR(可重复读) 隔离级别 下为了解决幻读问题时引入的锁机制。
Tips:使用间隙锁GAP Lock锁住的是一个区间,而不仅仅是这个区间中的每一条数据
SELECT * FROM demo WHERE id > 23 and id < 25 FOR UPDATE
上面语句对id范围(23, 25)的数据行加间隙锁锁,此时就无法插入id= 24的数据
临键锁 Next-Key Lock
Next-key Lock 临键锁是记录锁和间隙锁的组合,锁的范围是左开右闭区间的数据(即在某条记录以及这条记录前面间隙上的锁)。
InnoDB是使用Next-Key Lock来解决幻读问题的,在数据行上的非唯一索引列上都会存在一把临键锁。
注意:临键锁只与 非唯一索引列 有关,在 唯一索引列(包括主键列)上不存在临键锁。
图片
上面表结构中age字段为普通索引
-- 事务A 更新age=24的记录
UPDATE demo SET name = Vladimir WHERE age = 24;
-- 事务B 执行插入
INSERT INTO demo VALUES(100, 26, 'xiaoxu');
事务 A 在对 age 为 24 的列进行 UPDATE 操作的同时,也获取了 (24, 26] 这个区间内的临键锁,所以此时事务B会被阻塞。
问题
临键锁 Next-Key Lock如何降级?
细心的朋友会发现开头的题纲中有一个降级的指向,那么是在什么情况下发生降级的呢?
图片
在能使用记录锁或者间隙锁就能避免幻读现象的场景下, next-key lock 就会退化成记录锁或间隙锁。
有以下场景:
唯一索引等值查询:
1.当查询的记录是存在的,next-key lock 会退化成【记录锁】 2.当查询的记录是不存在的,next-key lock 会退化成【间隙锁】
非唯一索引等值查询:
1.当查询的记录存在时,除了会加 next-key lock 外,还额外加间隙锁,也就是会加两把锁。
2.当查询的记录不存在时,只会加 next-key lock,然后会退化为间隙锁,也就是只会加一把锁。