前言:整理归纳,仅供个人温习之用,请支持正版极客时间
-
根据加锁的范围,MySQL 里面的锁大致可以分成 全局锁、表级锁和行锁三类
1、全局锁(对整个数据库实例加锁)
*MySQL 提供了一个加全局读锁的方法,命令是 Flush tables with read lock (FTWRL)。当你需要让整个库处于只读状态的时候,可以使用这个命令,之后其他线程的以下语句会被阻塞:数据更新语句(数据的增删改)、数据定义语句(包括建表、修改表结构等)和更新类事务的提交语句。
*使用场景:全库逻辑备份
*风险:1、如果在主库备份,在备份期间不能更新,业务停摆;2、如果在从库备份,备份期间不能执行主库同步的binlog,导致主从延迟
*备份不加锁的话,备份系统备份的得到的库不是一个逻辑时间点,这个视图是逻辑不一致的。
*怎么拿到一致性视图:在可重复读隔离级别下开启一个事务。
官方自带的逻辑备份工具是 mysqldump。当 mysqldump 使用参数–single-transaction 的时候,导数据之前就会启动一个事务,来确保拿到一致性视图。而由于 MVCC 的支持,这个过程中数据是可以正常更新的。
*为什么还需要FTWRL:一致性读是好,但前提是引擎要支持这个隔离级别。single-transaction 方法只适用于所有的表使用事务引擎的库
*既然要全库只读,为什么不使用 set global readonly=true 的方式呢?
readonly 方式也可以让全库进入只读状态,但还是建议你用 FTWRL 方式,主要有两个原因:
-
在有些系统中,readonly 的值会被用来做其他逻辑,比如用来判断一个库是主库还是备库。因此,修改 global 变量的方式影响面更大,不建议使用
-
在异常处理机制上有差异。如果执行 FTWRL 命令之后由于客户端发生异常断开,那么 MySQL 会自动释放这个全局锁,整个库回到可以正常更新的状态。而将整个库设置为 readonly 之后,如果客户端发生异常,则数据库就会一直保持 readonly 状态,这样会导致整个库长时间处于不可写状态,风险较高。
-
read_only=1只读模式,不会影响slave同步复制的功能
-
read_only=1只读模式,限定的是普通用户进行数据修改的操作,但不会限定具有super权限的用户的数据修改操作,除非设置super_read_only=on
2、表级锁(表锁、元数据锁)
*表锁的语法是 lock tables … read/write。可以用 unlock tables 主动释放锁,也可以在客户端断开的时候自动释放。
Ps:lock tables 语法除了会限制别的线程的读写外,也限定了本线程接下来的操作对象。
在还没有出现更细粒度的锁的时候,表锁是最常用的处理并发的方式。InnoDB引擎支持行锁,一般不使用 lock tables 命令来控制并发,毕竟锁住整个表的影响面还是太大。
*在 MySQL 5.5 版本中引入了 MDL(元数据锁:保证读写的正确性),当对一个表做增删改查操作的时候,加 MDL 读锁;当要对表做结构变更操作的时候,加 MDL 写锁。
-
读锁之间不互斥,可以有多个线程同时对一张表增删改查
-
读写锁之间、写锁之间是互斥的,用来保证变更表结构操作的安全性。如果有两个线程要同时给一个表加字段,其中一个要等另一个执行完才能开始执行。
*案例:给一个小表加个字段,导致整个库挂了?
session A 先启动,这时候会对表 t 加一个 MDL 读锁。由于 session B 需要的也是 MDL 读锁,因此可以正常执行。之后 session C 会被 blocked,是因为 session A 的 MDL 读锁还没有释放,而 session C 需要 MDL 写锁,因此只能被阻塞,之后所有要在表 t 上新申请 MDL 读锁的请求也会被 session C 阻塞(队列)。所有对表的增删改查操作都需要先申请 MDL 读锁,就都被锁住,等于这个表现在完全不可读写了。如果某个表上的查询语句频繁,而且客户端有重试机制,也就是说超时后会再起一个新 session 再请求的话,这个库的线程很快就会爆满。
*如何安全地给小表加字段?
首先要解决长事务,事务不提交,就会一直占着 MDL 锁。在 MySQL 的 information_schema 库的 innodb_trx 表中,可以查到当前执行中的事务。如果要做 DDL 变更的表刚好有长事务在执行,要考虑先暂停 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 ...
3、总结
*全局锁主要用在逻辑备份过程中。对于全部是 InnoDB 引擎的库,建议你选择使用–single-transaction 参数,对应用会更友好。
*
表锁一般是在数据库引擎不支持行锁的时候才会被用到的。如果你发现你的应用程序里有 lock tables 这样的语句,比较可能的情况是:
-
系统现在还在用 MyISAM 这类不支持事务的引擎,要安排升级换引擎
-
引擎升级了,但是代码还没升级。解决方法:把 lock tables 和 unlock tables 改成 begin 和 commit
*MDL 会直到事务提交才释放,在做表结构变更的时候,一定要小心不要导致锁住线上查询和更新。
*问题:备份一般都会在备库上执行,你在用–single-transaction 方法做逻辑备份的过程中,如果主库上的一个小表做了一个 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)。
DDL 从主库传过来的时间按照效果不同,我打了四个时刻。题目设定为小表,我们假定到达后,如果开始执行,则很快能够执行完成。
参考答案:
-
如果在 Q4 语句执行之前到达,现象:没有影响,备份拿到的是 DDL 后的表结构
-
如果在“时刻 2”到达,则表结构被改过,Q5 执行的时候,报 Table definition has changed, please retry transaction,现象:mysqldump 终止
-
如果在“时刻 2”和“时刻 3”之间到达,mysqldump 占着 t1 的 MDL 读锁,binlog 被阻塞,现象:主从延迟,直到 Q6 执行完成
-
从“时刻 4”开始,mysqldump 释放了 MDL 读锁,现象:没有影响,备份拿到的是 DDL 前的表结构
*Online DDL:
-
拿MDL写锁
-
降级成MDL读锁
-
真正做DDL
-
升级成MDL写锁
-
释放MDL锁
1、2、4、5如果没有锁冲突,执行时间非常短。第3步占用了DDL绝大部分时间,这期间这个表可以正常读写数据,是因此称为“online ”