面试进阶-数据库中需要理解的锁

时间:2022-07-22
本文章向大家介绍面试进阶-数据库中需要理解的锁,主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

城边编程 phplog

上一篇文章介绍了数据库中锁的起源,今天将介绍数据库中常用的锁。还是以MySQL为例,MySQL中有表锁、行锁、共享锁、互斥锁、意向锁、间隙锁、记录锁、Next-Key锁、插入意向锁、AUTO-INC锁、隐式锁。看完本篇文章,再多的锁都难不倒你。

两个重要的知识点

1. 读锁不是乐观锁

世界上只有两种锁,悲观锁和乐观锁。以上MySQL中的锁都是悲观锁,都会在线程中对资源加锁。一个线程对数据加读锁后,其他线程也能读取数据,但无法写入和更新数据,所以读锁不是乐观锁(有加锁过程的都不是乐观锁)。

2. 乐观锁不是真正的『锁』

乐观锁不会给资源加锁,他通过CAS加自旋的方式在多线程中对资源进行读写操作。我们经常听到无锁版队列、无锁版链表、无锁版数据结构和算法等,底层都是使用乐观锁实现。

共享锁与互斥锁

共享锁就是读锁,一个线程对数据加共享锁后,其他线程也能读取数据,但无法写入和更新数据。

互斥锁就是写锁,一个线程对数据加互斥锁后,其他线程不能读取、写入、更新数据。

表锁和行锁中会使用共享锁与互斥锁来实现数据隔离。

表锁

表锁分为读锁和写锁(就是共享锁和互斥锁)。表锁是对整张表做操作,读锁能让其他线程同时读取数据,但无法写入和更新。写锁让其他线程无法读取、写入、更新数据。语句如下:

//表锁支持read锁和write锁
lock table php_monitor write;
SELECT SLEEP(10);
unlock table;

常见的 DDL 语句(如 ALTER、CREATE 等)会加表锁(写锁),虽然MySQL 5.6之后支持在线修改(增、删、改、查均不会锁表),但在该表被访问时执行DDL操作还是会加表锁,阻塞对表的任何操作。

另外ALTER、CREATE等DDL语句会隐式的对当前事务进行一次『COMMIT』操作(隐式提交),所以无法回滚。

行锁

行锁是指对某一行数据加锁,MySQL隔离级别的核心是基于行锁实现,几十年磨一刀,设计无比精美。我将从最基本的SQL语句分析,例如执行如下两条更新操作:

//id为主键索引,name为二级索引
update user set age = 18 where id = 9;
update user set age = 31 where name = 'Layne';

第一条 SQL 使用主键索引来查询,则只需要在 id = 9 这个主键索引上加写锁;第二条 SQL 使用二级索引来查询,首先在 name = Layne 这个索引上加写锁,然后还需要在 id = 9 这个主键索引上加写锁。

为什么二级索引要加两把锁?因为InnoDB 是聚簇索引,也就是 B+ 树的叶节点既存储了主键索引也存储了数据行。而 InnoDB 的二级索引的叶节点存储的则是主键值,所以通过二级索引查询数据时,需要拿对应的主键去聚簇索引中再次进行查询才能拿到数据行。

举个开发中的实际例子。如果要范围更新数据,不规范的做法如下:

update user set age = 18 where name = "Lay%";

这会导致数据库加大量的锁,更规范的做法如下:

// 1. 导出数据
select id from user where name = "Lay%";
//2. 批量运行
update user set age = 18 where id = 1;
update user set age = 18 where id = 2;
…………
update user set age = 18 where id = 8;

锁的数量会减少一半。

行锁包括共享锁、互斥锁、意向锁、间隙锁、记录锁、Next-Key锁、插入意向锁、AUTO-INC锁。共享锁和互斥锁就是读写锁,这里不做讲解。

1. 记录锁 - 记录锁是最简单的行锁,上边描述 InnoDB 加锁原理中的锁就是记录锁,只锁住 id = 9 或者 name = ‘Layne’ 这一条记录。更新操作必须要根据索引进行操作,没有索引时,不仅会消耗大量的锁资源,增加数据库的开销,还会极大的降低了数据库的并发性能。

2. 间隙锁 - 还是最开始的例子,如果 id = 9 这条记录不存在,会在 id = 9 前后两个索引之间加上间隙锁。间隙锁加在索引上(没有索引就没间隙锁),唯一的作用就是防止其他事务插入记录造成幻读。间隙锁会锁定一个范围,但不包括记录本身。

3. Next-Key锁 - 记录锁与间隙锁的结合,锁定一个范围并且锁定记录本身。用来替代记录锁+间隙锁,减少锁的数量。

4. 意向锁 - 表锁和行锁虽然锁定范围不同,但是会相互冲突。当要加表锁时,需要遍历该表的所有记录是否加有行锁,这种遍历检查的方式非常低效。为此MySQL引入了意向锁来检测表锁和行锁的冲突。意向锁是表级锁,分为读意向锁和写意向锁。当事务要在一行数据上加上读锁或写锁时,首先要在表上加上意向锁。这样判断表中是否有行锁只要检查表上是否有意向锁。

5. 插入意向锁 - 插入意向锁是一种特殊的间隙锁,表示插入的意向,只有在 INSERT 的时候才会有这个锁。间隙锁唯一的作用就是防止其他事务插入记录造成幻读,正是由于在执行 INSERT 语句时需要加插入意向锁,而插入意向锁和间隙锁冲突,从而阻止了插入操作的执行。

6. AUTO-INC锁 - 插入操作会根据自增长的计数器值加1赋予自增长列。这个实现方式称作为AUTO-INC 锁。这种锁采用一种特殊的表锁机制,为了提高插入的性能,锁不是在一个事务完成后才释放,而是在完成对自增长值插入的SQL语句后立即释放,这也导致自增ID无法回滚。

最后

行锁是MySQL隔离级别的核心,抓住行锁就抓住了主要矛盾。在执行SQL时可以通过行锁的数量来评估执行效率。工作中最常见的优化方法就是通过减少记录锁的数量来实现SQL优化,比如范围执行 UPDATE 和 DELETE 语句时可以先 SELECT 再通过主键id批量处理(参见文中二级索引UPDATE语句的优化)。下一篇将介绍增、删、改、查语句中涉及到的锁。