1. 概述
这一章主要讲到了全局锁和表锁,介绍了碰到锁时的现象和其背后的原理。
2. 总结
2.1 MySQL 里面的锁大致可以分成全局锁、表级锁和行锁三类
2.2 全局锁
全局锁就是对整个数据库实例加锁。MySQL 提供了一个加全局读锁的方法,命令是 Flush tables with read lock (FTWRL)。当你需要让整个库处于只读状态的时候,可以使用这个命令,之后其他线程的以下语句会被阻塞:数据更新语句(数据的增删改)、数据定义语句(包括建表、修改表结构等)和更新类事务的提交语句。
2.3 全局锁的使用场景
- 全局锁的典型使用场景是,做全库逻辑备份。为了是在做备份时得到的库是在同一个时间点上,这个视图是逻辑一致的。
- 做全库逻辑备份,还可以使用之前提到的事务隔离级别,也就是可重复读。有了这个功能,为什么还需要 FTWRL 呢?一致性读是好,但前提是引擎要支持这个隔离级别。比如,对于 MyISAM 这种不支持事务的引擎,如果备份过程中有更新,总是只能取到最新的数据,那么就破坏了备份的一致性。这时,我们就需要使用 FTWRL 命令了。所以对于全部是 InnoDB 引擎的库,建议你选择使用–single-transaction 参数,对应用会更友好。
- 既然要全库只读,为什么不使用 set global readonly=true 的方式,是因为 readonly 的值会被用来做其他逻辑,比如用来判断一个库是主库还是备库。还有就是如果将整个库设置为 readonly 之后,如果客户端发生异常,则数据库就会一直保持 readonly 状态,这样会导致整个库长时间处于不可写状态,风险较高。
2.4 MySQL 里面表级别的锁有两种:一种是表锁,一种是元数据锁(meta data lock,MDL)
2.5 表锁
- 表锁的语法是 lock tables … read/write。与 FTWRL 类似,可以用 unlock tables 主动释放锁,也可以在客户端断开的时候自动释放。需要注意,lock tables 语法除了会限制别的线程的读写外,也限定了本线程接下来的操作对象。
- 举个例子, 如果在某个线程 A 中执行 lock tables t1 read, t2 write; 这个语句,则其他线程写 t1、读写 t2 的语句都会被阻塞。同时,线程 A 在执行 unlock tables 之前,也只能执行读 t1、读写 t2 的操作。连写 t1 都不允许,自然也不能访问其他表。
- 表锁一般是在数据库引擎不支持行锁的时候才会被用到的。如果你发现你的应用程序里有 lock tables 这样的语句,你需要追查一下,比较可能的情况是:要么是你的系统现在还在用 MyISAM 这类不支持事务的引擎,那要安排升级换引擎;要么是你的引擎升级了,但是代码还没升级。把 lock tables 和 unlock tables 改成 begin 和 commit,问题就解决了。
2.6 元数据锁(meta data lock,MDL)
- MDL 不需要显式使用,在访问一个表的时候会被自动加上。
- MySQL 5.5 版本中引入了 MDL,当对一个表做增删改查操作DML的时候,加 MDL 读锁;当要对表做结构变更操作DDL的时候,加 MDL 写锁。 读锁之间不互斥,因此你可以有多个线程同时对一张表增删改查。 读写锁之间、写锁之间是互斥的,用来保证变更表结构操作的安全性。 因此,如果有两个线程要同时给一个表加字段,其中一个要等另一个执行完才能开始执行。 如果对线上一个频繁DML操作的表做DDL如添加字段等操作,可能会导致死锁,使数据库连接资源被消耗完,导致数据库宕机。安全的解决方式是对表做DDL如添加字段时,设置执行语句的超时时间,写锁超时自动释放,不影响读锁。
- MDL 会直到事务提交才释放,在做表结构变更的时候,你一定要小心不要导致锁住线上查询和更新。
2.7 如何安全地给小表加字段
- 解决长事务,事务不提交,就会一直占着 MDL 锁。在 MySQL 的 information_schema 库的 innodb_trx 表中,你可以查到当前执行中的事务。如果你要做 DDL 变更的表刚好有长事务在执行,要考虑先暂停 DDL,或者 kill 掉这个长事务。
- 热点表,虽然数据量不大,但是上面的请求很频繁。这时候 kill 可能未必管用,因为新的请求马上就来了。比较理想的机制是,在 alter table 语句里面设定等待时间,如果在这个指定的等待时间里面能够拿到 MDL 写锁最好,拿不到也不要阻塞后面的业务语句,先放弃。再通过重试命令重复这个过程。
2.8 思考题:当备库用 –single-transaction 做逻辑备份的时候,如果从主库的 binlog 传来一个 DDL 语句会怎么样
Q1:SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
Q2:START TRANSACTION WITH CONSISTENT SNAPSHOT;
/* other tables */
Q3:SAVEPOINT sp;
/* 时刻 1 */
Q4:show create table `t1`;
/* 时刻 2 */
Q5:SELECT * FROM `t1`;
/* 时刻 3 */
Q6:ROLLBACK TO SAVEPOINT sp;
/* 时刻 4 */
/* other tables */
- 在备份开始的时候,为了确保 RR(可重复读)隔离级别,再设置一次 RR 隔离级别 (Q1);
- 启动事务,这里用 WITH CONSISTENT SNAPSHOT 确保这个语句执行完就可以得到一个一致性视图 (Q2);
- 设置一个保存点,这个很重要 (Q3);
- show create 是为了拿到表结构 (Q4);
- 然后正式导数据 (Q5);
- 回滚到 SAVEPOINT sp,在这里的作用是释放 t1 的 MDL 锁 (Q6)。
参考答案如下:
- 如果在 Q4 语句执行之前到达,现象:没有影响,备份拿到的是 DDL 后的表结构。
- 如果在“时刻 2”到达,则表结构被改过,Q5 执行的时候,报 Table definition has changed, please retry transaction,现象:mysqldump 终止。
- 如果在“时刻 2”和“时刻 3”之间到达,mysqldump 占着 t1 的 MDL 读锁,binlog 被阻塞,现象:主从延迟,直到 Q6 执行完成。
- 从“时刻 4”开始,mysqldump 释放了 MDL 读锁,现象:没有影响,备份拿到的是 DDL 前的表结构。