MySQL系列:全局锁、表锁

作者: 阿琦 | 2021-10-05 | 阅读

   

1前言

注意:目前整个 MySQL 系列都是 8.0.X 版本

不过我会兼容 5.7、5.8版本 ,差异化的地方会有标出来(比如官方文档)

比如 上一篇: MySQL系列:索引(下)

这篇索引(下) 是从那天的下午14 左右一直淦到晚上 23点多... .

2目录

  • 全局锁
  • 表锁

3继续

锁,设计锁是为了处理并发问题。多用户共享资源出现并发时,就需要合理地控制资源的访问规则。而锁,就是用来实现这些访问规则的重要数据结构。

根据 MySQL 加锁的范围,可以大致分为全局锁表锁行锁三类。

4全局锁

全局锁就是字面意思,对整个数据库实例加锁。MySQL 自身有个一个加全局锁的方法 Flush tables with read lock(FTWRL)。如果需要整个库处于只读状态,那么就可以使用这个命令,之后其它线程的以下语句都会被阻塞:

  • 数据库CRUD 语句
  • 数据定义语句(建表、修改表结构等)
  • 更新类事务的提交语句

全局锁的典型使用场景是,做全库逻辑备份 ,换句话说就是把整库每个表都 select 出来存成文本。

之前有一种做法通过 FLUSH TABLES WITH READ LOCK简称(FTWRL),该命令主要用于备份工具获取一致性备份(数据与binlog位点匹配)。由于FTWRL总共需要持有两把全局的MDL锁,并且还需要关闭所有表对象,因此这个命令的杀伤性很大,执行命令时容易导致库hang住。如果是主库,则业务无法正常访问;如果是备库,则会导致SQL线程卡住,主备延迟。

FTWRL主要包括3个步骤:

  1. 上全局读锁(lock_global_read_lock)
  2. 清理表缓存(close_cached_tables)
  3. 上全局COMMIT锁(make_global_read_lock_block_commit)

详情可参考 博客园 天士梦 的: https://www.cnblogs.com/cchust/p/4603599.html#:~:text=FLUSH TABLES WITH,READ LOCK简称 (FTWRL),该命令主要用于备份工具获取一致性备份 (数据与binlog位点匹配)。

但让整库都只读,第一想法那项目肯定都要死掉了:

  • 如果你在主库上备份,那么备份期间都不能执行更新,业务基本上就得停罢;
  • 如果你再从库上备份,那么备份期间从库不能执行主库同步过来的 binlog,会导致主从延迟。

那么有个问题,全局加锁问题这么多,如果不加锁会发生什么。

假设你现在要维护“极客时间”的购买系统,关注的是用户账号余额表和用户课程表。

说到这里插一句:《Mysql比知比会》《高性能mysql》《MySQL技术内幕:InnoDB存储引擎》《mysql实战45讲》《mysql是怎样运行的从根上了解》 这 6 本通透的话,那么你就超凡入圣了。

还有一本被吃了。

害

目前我也就看了几遍 《mysql实战45讲》,和一遍《高性能mysql》,整个系列也都是以45 讲为基础,精读不写点总结啥的总感觉不是很通透。

继续,现在发起一个逻辑备份,假设备份期间有个用户他购买了一门课程,业务逻辑里就要扣掉他的余额,然后往已购课程列表里面加上这门课。

如果时间顺序上是先备份账号余额表(u_account),然后用户购买,再备份用户课程表(u_course),会发生什么?

可以看到这个备份结果里,用户 A 的数据状态是“账号余额没扣,但是用户课程表里面已经加上了这门课”。如果后面用这个备份来恢复数据的话,用户 A 就会发现,自己赚了。

那么如果备份表的顺序反过来,先备份用户课程表再备份账号余额表,也就是说不加锁的话,备份系统备份得到的库不是一个逻辑时间点,这个视图是逻辑不一致的。

MySQL 系列:事务隔离 有介绍在可重复读隔离级别下开启一个事物拿到一致性视图。

官方自带的逻辑备份工具是 MySQLdump。当MySQLdump使用参数–single-transaction的时候,导数据之前就会启动一个事务,来确保拿到一致性视图。而由于 MVCC 的支持,这个过程中数据是可以正常更新的。

官网MySQLdump -- https://dev.mysql.com/doc/refman/8.0/en/mysqldump-sql-format.html

那么有了这个为什么还需要 FTWRL ?一致性读是好,但前提是引擎要支持这个隔离级别。

比如:对于MyISAM这种不支持事物的引擎,如果备份过程中有更新,总是只能取到最新的数据,那么就破坏了备份的一致性。这时,就需要使用 FTWRL 命令了。

–single-transaction方法只适用于所有的表使用事务引擎的库。 如果有的表使用了不支持事务的引擎,那么备份只能通过FTWRL方法。也是MyISAM 被 InnoDB 取代的原因之一。

全库只读,还有一种使用 set global readonly=true 的方式,但还是建议使用FTWRL方式:

  1. 在有些系统中,readonly 的值会被用来做其他逻辑,比如 用来判断一个库是主库还是备库。因此,修改 global 变量的方式影响面会更大,一般不建议使用。
  2. 在异常处理机制上有差异。 如果执行 FTWRL命令后由于客户端发生异常断开,那么 MySQL 会自动释放这个全局锁,整个库会回到可以正常更新的状态。 - 而如果将整个库设置为 readonly 之后,如果客户端发生异常,则数据库就会一直保持 readonly 状态,这样会导致整个库长时间处于不可写状态,风险非常高。

业务的更新不只是增删改数据(DML),还有可能是加字段等修改表结构的操作(DDL)。

无论哪种方法,一个库被全局锁上后,对任何一个表做加字段操作,都是会被锁住的。

即使没有被全局锁住,加字段也不是一帆风顺的,还会碰到表级锁。

5表级锁

MySQL 表级锁有两种:

  1. 表锁
  2. 元数据锁(meta data lock) MDL

表锁的语法是 lock tables... read/write。 与 FTWRL 类似,可以用 unlock tables 主动释放锁,也可以在客户端断开的时候自动释放。需要注意的是 lock tables 语法除了会限制别的线程的读写外,也限定了本线程接下来的操作对象。

假设,某线程 A 中执行 lock tables t1 read,t2 write; 这个语句,则其他线程写 t1、读写 t2 的语句都会被阻塞。同时,线程 A 在执行 unlock tables 之前,也只能执行读 t1、读写 t2 的操作。写 t1 都是不允许的,自然也不能访问其他表。

在还没有出现更细粒度的锁的时候,表锁是最常用的处理并发的方式。而对于 InnoDB 这种支持行锁的引擎,一般不使用lock tables命令来控制并发,毕竟锁住整个表的影响面还是太大。

另一类表级锁是 MDL(metadata lock). MDL 不需要显示使用,在访问一个表的时候会被自动加上。MDL 的作用是保证读写的正确性。

如果一个查询正在遍历一个表中的数据,而执行期间另一个线程对这个表结构做变更,删了一列,那么查询线程拿到的结果肯定跟表结构对不上。

因此,在 MySQL5.5 版本中引入了 MDL,当对一个表做增删改查时,加 MDL 读锁;当要对表结构变更操作的时候,加 MDL 写锁。

  • 读锁之间不互斥,所以你可以有多个线程同时对一张表增删改查。
  • 读写锁之间、写锁之间是互斥、排他的,用来保证变更表结构操作的安全性。因此,如果有两个线程要同时给一个表加字段,其中一个要等另一个执行完才能开始执行。
默认开启
默认开启

给表加字段、修改字段、加索引,都需要扫描全表的数据。一不小心就会出问题:

备注:这里的实验环境是 MySQL 5.6
备注:这里的实验环境是 MySQL 5.6

可以看到 sessionA 先启动,这时会对表 t 加一个 MDL 读锁。由于sessionB 需要的也是 MDL 读锁,也能正常执行,上面说过,读锁之间不互斥。

之后 sessionC 会被 blocked,是因为 sessionA 的 MDL 读锁还没有释放,而 sessionC 需要 MDL 写锁,因此会阻塞。

如果只有 sessionC 自己被阻塞倒是问题不大,关键是再这之后所有要在表 t 上新申请 MDL 读锁的请求也会被 sessionC 阻塞。

所有对表的增删改查操作都需要先申请 MDL 读锁,如果都被锁住了,等于这个表 现在完全不可读写了。

如果某一个表上的查询语句频繁,而且客户端有重试机制,也就是说超时后会再起一个新session 再请求的话,这个库的线程很快就会饱满。

事务中的 MDL 锁,在语句执行开始时申请,但是语句结束后并不会马上释放,而是等到整个事务提交后再释放。

那么有个问题 -- 长事务,事务不提交,就会一直占着 MDL 锁。在 MySQL 的 information_schema 库的 innodb_trx 表中 可以查到当前执行中的事务。如果你要做 DLL 变更的表刚好有长事务在执行,要先考虑先暂停 DDL,或者 kill(干掉)长事务。

如果要变更的表是一个热点表,虽然数据量不大,但是请求频繁,又必须得加这个字段,那么《一键删库跑路》了解一波。

这时 kill 可能未必管用,因为新的请求马上就来了。比较理想的机制是,在 alter table 语句里面设定等待时间,如果在这个指定的等待时间里面能够拿到 MDL 写锁最好,拿不到也不要阻塞后面的业务语句,先放弃。之后开发人员或者 DBA 再通过重试命令重复这个过程。

MariaDB 已经合并了 AliSQL 的这个功能,所以这两个开源分支目前都支持 DDL NOWAIT/WAIT n 这个语法。

ALTER TABLE tbl_name NOWAIT add column ...
ALTER TABLE tbl_name WAIT N add column ... 

Github开源地址:https://github.com/alibaba/AliSQL


版权声明:本文由 阿琦 在 2021年10月05日发表。本文采用CC BY-NC-SA 4.0许可协议,非商业转载请注明出处,不得用于商业目的。
文章题目及链接:《MySQL系列:全局锁、表锁》




  相关文章:


留言区:

TOP