天天看点

如何选用适合的MySQL锁?从全局锁、表级锁、行级锁深入了解

作者:Java灵风

本篇速览

锁是计算机协调多个进程或线程并发访问某一资源的机制。在数据库中,除了传统的计算资源(CPU、RAM、I/O)的争用以外,数据也是一种供许多用户共享的资源,如何保证数据并发访问的一致性、有效性是所有数据库必须解决的一个问题,锁冲突也是影响数据库并发访问性能的一个重要因素。

在Java中我们就提到过锁的概念,锁存在的意义就是帮助我们解决并发问题,当多个线程并发访问的时候,锁是帮助我们解决问题的一个很好的方法,但是类似于Java中的 synchronize关键字锁的对象,可以是一个对象,也可以是一个方法,选择合适的对象以及锁能够帮助我们提高数据库的并发访问性能,而我们如何使用锁,又如何使用合适的锁呢?本文下面将从这三种锁来深入了解MySQL的锁机制:

  • 1️⃣ 全局锁:锁定数据库中的所有表
  • 2️⃣ 表级锁:每次操作锁住整张表
  • 3️⃣ 行级锁:每次操作锁住对应的行数据

1️⃣ 全局锁

全局锁就是对于整个数据库实例加锁,在加锁之后,整个数据库就处于只读的状态,后续的DML的写语句,DDL语句,已经更新的事务提交语句都会被阻塞。

看到全局锁的功能,可能会觉得,锁的粒度这么大,有什么用呢?

它最典型的使用场景时做全库的逻辑备份,对所有的表进行锁定,从而获取一致性视图,保证数据的完整性。

上面的场景听起来很有道理,但是我们不妨走入实际场景,想想为什么备份要加全局锁呢?又有哪些原因呢?

假设我有两张表order,stock,也就是订单表和库存表,现在假设没有全局锁,我开始导出数据。在导出stock的过程中,order表新增的一条记录,理论上讲,stock应该减少一条数据,但是stock的数据已经导出了,因此最后导出的数据就会产生脏数据,stock的数据与order的数据对应不上。

如果还是这个场景,我们如何去解决呢,如何加全局锁,加了全局锁又会有什么样的效果呢?

首先谈谈如何去加全局锁,也就是全局锁的语法:FLUSH tables with read lock

接下来可以执行数据备份,借用MySQL中提供给我们的工具 mysqldump 进行数据备份:

mysqldump -uroot -p1234 databaseName > fileName.sql

数据备份结束后,就可以进行解锁:unlock tables

诚然,全局锁能帮助我们解决导出数据库的时候的脏数据,但是使用全局锁也存在一定的问题:

数据库中加全局锁,锁的粒度很大,是一个比较重的操作:

  1. 在加锁期间,其他客户端在执行写入操作的时候都会阻塞,业务基本上就处停摆状态
  2. 如果数据库不是单机,而是主从结构,甚至做了读写分离,我们在做写入的时候不会阻塞,因为我们可以从从库中做备份,而数据写入主库,但是存在的问题就是,在备份期间从库不能执行主库同步过来的二进制日志,就会产生主从延迟

经典“白学”: 在InnoDB存储引擎中,我们为了解决这一个问题,可以在备份中加上一个参数 --single-transaction 参数来完成不加锁的一致性数据备份。为什么可以这样呢,是因为在InnoDB存储引擎的底层,它实际使用的是快照读实现。

2️⃣ 表级锁

表级锁,每次操作锁住整张表,锁的粒度也比较大,容易发生锁冲突的概率最高,并发度最低。

对于表级锁,可以分为这三类:表锁、元数据锁、意向锁。接下来,我们分别来了解这三种表级锁,了解它们的具体使用场景和使用方法:

⛏ 表锁

对于表锁,我们又可以分为两类:表共享读锁(read lock)、表独占写锁(write lock)。我们有时候简称他们为:读锁、写锁

在了解他们的区别、应用之前,我们首先来了解一下加锁和解锁的语法,方便我们后续的使用:

  1. 加锁:lock tables 表名…… read/write
  2. 解锁:unlock tables 或者 断开MySQL连接,锁会自动释放

然后再谈谈读锁和写锁的锁的范围。其实在最开始看到读锁和写锁的时候,我的第一反应是JUC里面的读写分离的锁,但是在具体了解到MySQL的读锁和写锁后,发现他们的差别很大:

读锁

如果对表加了读锁,那么其他的客户端是能继续读的,但是不能写,与JUC的读写分离锁的区分的点就在于,对于加锁的线程也不能写,只能读:

如图,右侧的客户端加了读锁,左侧的客户端能进行查询,但是做不了插入,插入的语句会被阻塞,会在解锁的时候执行

如何选用适合的MySQL锁?从全局锁、表级锁、行级锁深入了解

可以用这个图来表示加了读锁时候的状态:(其中绿色表示能够执行,红色表示会阻塞)

如何选用适合的MySQL锁?从全局锁、表级锁、行级锁深入了解

不论是加锁的线程,还是其他的线程,都只能查询,不能修改,当然,在解锁后,修改的语句会自动执行。

⚫ 写锁

如果对表加了写锁,其实我们可以理解为一个线程锁,只有这个加锁的线程能操作这个表,包括查询和修改,其他的线程既不能查询也不能修改,都会被阻塞。

可以用这个图来表示加了写锁时候的状态:(其中绿色表示能够执行,红色表示会阻塞)

如何选用适合的MySQL锁?从全局锁、表级锁、行级锁深入了解

⚒ 元数据锁(meta data lock)

元数据锁的加锁过程是系统自动控制的,不需要显式使用,在访问一张表的时候就会自动加上。元数据锁的主要作用就是维护表元数据的数据一致性,在表上有活动事务的时候,不可以对元数据进行写入操作。为的就是避免DML和DDL冲突,保证读写的正确性。

这里提到了元数据,那么什么是元数据呢?

元数据:简单说,元数据可以简单理解为表结构。所以其实元数据锁是MySQL底层维护的一把防止表的结构发生改变的锁。

在MySQL5.5中引入了MDL(元数据锁):

  • 当对一张表进行增删改的时候,会给表加一个MDL读锁(共享)
  • 当对表结构进行变更的时候,加MDL写锁(排他)

我们先来看看各种SQL语句对应的元数据锁:

SQL语句 元数据锁类型 说明
lock tables tableName read/write SHARED_READ_ONLY / SHARED_NO_READ_WRITE
select, select…… lock in share mode(共享锁,后续会提到) SHARED_READ 与SHARED_WRITE兼容,与EXCLUSIVE互斥
insert, update, delete, select……for update SHARED_WRITE 与SHARED_READ兼容,与EXCLUSIVE互斥
alter tables EXCLUSIVE 与其他的MDL都互斥

其实我们得到的信息大概就是,MySQL给增删改查的SQL加了元数据锁,其中,修改表结构的锁与其他的增删改查的锁互斥,也就是说:元数据锁就是为了保证表的完整性和数据完整性,一致性。防止在修改数据的过程中出现表结构被修改的情况。

听完上面的描述,可能对于元数据锁的作用有了一定了解,但是还是感觉有点模糊,我们不妨举个例子,如图:

如何选用适合的MySQL锁?从全局锁、表级锁、行级锁深入了解

在一个线程我们开启一个事务,然后在事务中进行查询,此时就加上了元数据锁SHARED_READ锁

此时在另外一个线程中去修改表结构,也就是要加上EXLUSIVE这个锁,但是两个锁互斥,就导致了,修改表结构阻塞。

如果想要更深一步了解元数据锁,除了看到它的效果(上述的修改表结构的SQL语句被阻塞),还要去看看SQL语句对应的锁,我们可以借用这条SQL来查看元数据锁:

SELECT object_type, object_schema, object_name, lock_type, lock_duration, from performance_schema.metadata_locks;

意向锁

我们先不想什么是意向锁,我们先看一个场景:

对于下面这张表

线程A开启事务,根据主键对数据进行更改,此时会给该行加上一个行锁

而此时线程B要给表加上一个表锁,由于表锁可能会与行锁进行冲突,所以线程B要全表扫描,是否存在行锁,如果存在行锁就阻塞。

很明显,全表扫描行锁的效率很低,因此就引入了我们的意向锁。

如何选用适合的MySQL锁?从全局锁、表级锁、行级锁深入了解
那意向锁是怎么做到提高效率的呢?

DML语句和for update会给表加上意向锁,同时会给索引添加行锁,表的意向锁与表锁是互斥的(从大体上来讲,特殊情况我们后续讲),因为二者互斥,表锁就无法加进去,这样就不用全表扫描去找行锁了。

刚刚有提到特殊情况,特殊情况是怎么回事呢?

意向锁分为两种:

  • 意向共享锁:与表锁读锁兼容,与表锁写锁互斥
  • 意向排他锁:与表锁的写锁和读锁都互斥

也就是说,我们的说法可以理解为:

  • 普通的DML语句和for update语句会给表加上意向排它锁,与表锁互斥,这样在加表锁的时候就会阻塞
  • 而共享锁SELECT …… lock in share mode会给表加上意向共享锁,与表锁的读锁兼容,此时是可以给表加上读锁的

其实可以说,意向锁有点像表的一种状态,一共是三种状态:不能加表锁,只能加读锁,能加表锁,而SQL语句就是改变意向锁状态的关键

3️⃣ 行级锁

行级锁,每次操作锁住对应的行数据,锁定粒度最小,发生锁冲突的概率最低,并发度最高。只应用在InnoDB存储引擎中,这一点我们在讲存储引擎中有提到。

InnoDB的数据是基于索引组织的,行锁是通过对索引上的索引项加锁来实现的,而不是对记录加的锁。对于行级锁,主要分为以下三类:

  1. 行锁:锁定单个行记录的锁,防止其他事务对此进行update和delete。在RC、RR隔离级别下都支持。
  2. 间隙锁:锁定索引间隙,确保索引记录的间隙不变,反之其他事务对这个间隙插入,产生幻读。在RR隔离级别下支持。这里提到了间隙,那间隙是什么了,就是下图中两个数据之间的间隙,那个锁的图标就是间隙锁。
  3. 临键锁:行锁和间隙锁的组合,同时锁住数据和数据前面的间隙,也就是行锁和间隙锁的组合,在RR隔离级别下支持。
如何选用适合的MySQL锁?从全局锁、表级锁、行级锁深入了解

行锁

InnoDB实现了两种类型的行锁:

  1. 共享锁:允许一个事务去读一行,阻止其他事务获得相同数据集的排它锁。简单来说就是,共享锁与共享锁兼容,但是共享锁与排它锁互斥。
  2. 排它锁:允许获取排它锁的事务更新数据,也就是说事务如果拿到了这行的排它锁,它就可以更新数据,阻止其他事务获得相同数据集的共享锁和排它锁,也就是说其他事务不能拿到这行数据的共享锁和排它锁。
  3. 综上:共享锁和共享锁兼容,但是排它锁与其他锁都不兼容
那么在常见的增删查改的SQL语句中,它们加的是何种类型的行锁呢?
SQL 行锁类型 说明
INSERT 排它锁 自动加锁
UPDATE 排它锁 自动加锁
DELETE 排它锁 自动加锁
SELECT 不加锁
SELECT…… lock in share mode 共享锁 需要手动在SELECT之后加上LOCK IN SHARE MODE
SELECT…… FOR UPDATE 排它锁 需要手动在SELECT之后加上FOR UPDATE

默认情况下,InnoDB存储引擎在RR事务隔离级别下运行,InnoDB会使用next-key锁进行搜索和索引扫描,以防止幻读。

  1. 针对唯一索引进行检索的时候,对于已存在的记录进行等值匹配时,将自动优化为行锁
  2. InnoDB的行锁是针对索引加的锁,不通过索引条件检索数据的话,InnoDB会对表中所有数据加锁,此时就会升级为表锁

间隙锁 & 临键锁

如果说行锁的目的是防止两个事务对同一条记录做更改,那间隙锁又有什么作用呢?我们以下面这个场景来了解一下间隙锁的使用:

如图我们拥有一张表,ID分别为1,3,8,11,19,25。

现在,我们开启一个事务,修改一条不存在的ID的数据,比如UPDATE STU SET age = 10 where id = 5

那么此时,InnoDB就会在3和8之间加上间隙锁,那么此时在另外一个线程插入数据:

INSERT INTO stu values(7,'Ruby',1)

就会因为间隙锁而阻塞

如何选用适合的MySQL锁?从全局锁、表级锁、行级锁深入了解

因此我们可以得到:索引上的等值查询(唯一索引),给不存在的记录加锁的时候,优化为间隙锁。

总结

本篇分别介绍了三种锁:全局锁、表级锁、行级锁,三种锁的粒度不同,使用的场景也不同。

  1. 首先说说全局锁,全局锁的粒度最大,它的使用场景是做全库的逻辑备份,防止产生脏数据,但是它也有它的弊端,因为全局锁的粒度太大了,尽管做了主从和读写分离也会存在一定的问题,当然InnoDB也给我们提供了备份的方法。
  2. 再说说表级锁,表级锁有三种分别为表锁、元数据锁、意向锁。表锁要弄清楚读锁和写锁的区别,元数据锁是为了维护表结构元数据的数据一致性,防止表结构被篡改(在写数据的时候),意向锁可以理解为表的一种状态,是否能够使用表锁的状态,它的底层由InnoDB维护,会随着SQL语句的变化而变化。
  3. 然后就是行级锁,行级锁也有三种,分别为:行锁、间隙锁、临键锁。行锁存在的目的是为了,保证一条数据的读写的正确性,有点类似于表级锁的元数据锁,而间隙锁和临键锁存在的目的就是为了防止数据在更新和插入的时候间隙不被修改,而导致幻读的问题。

原文:https://juejin.cn/post/7170707711208718344