You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

175 lines
12 KiB
Markdown

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# 06 | 全局锁和表锁 :给表加个字段怎么有这么多阻碍?
今天我要跟你聊聊MySQL的锁。数据库锁设计的初衷是处理并发问题。作为多用户共享的资源当出现并发访问的时候数据库需要合理地控制资源的访问规则。而锁就是用来实现这些访问规则的重要数据结构。
**根据加锁的范围MySQL里面的锁大致可以分成全局锁、表级锁和行锁三类**。今天这篇文章,我会和你分享全局锁和表级锁。而关于行锁的内容,我会留着在下一篇文章中再和你详细介绍。
这里需要说明的是,锁的设计比较复杂,这两篇文章不会涉及锁的具体实现细节,主要介绍的是碰到锁时的现象和其背后的原理。
# 全局锁
顾名思义全局锁就是对整个数据库实例加锁。MySQL提供了一个加全局读锁的方法命令是 Flush tables with read lock (FTWRL)。当你需要让整个库处于只读状态的时候,可以使用这个命令,之后其他线程的以下语句会被阻塞:数据更新语句(数据的增删改)、数据定义语句(包括建表、修改表结构等)和更新类事务的提交语句。
**全局锁的典型使用场景是,做全库逻辑备份。**也就是把整库每个表都select出来存成文本。
以前有一种做法是通过FTWRL确保不会有其他线程对数据库做更新然后对整个库做备份。注意在备份过程中整个库完全处于只读状态。
但是让整库都只读,听上去就很危险:
* 如果你在主库上备份,那么在备份期间都不能执行更新,业务基本上就得停摆;
* 如果你在从库上备份那么备份期间从库不能执行主库同步过来的binlog会导致主从延迟。
看来加全局锁不太好。但是细想一下,备份为什么要加锁呢?我们来看一下不加锁会有什么问题。
假设你现在要维护“极客时间”的购买系统,关注的是用户账户余额表和用户课程表。
现在发起一个逻辑备份。假设备份期间,有一个用户,他购买了一门课程,业务逻辑里就要扣掉他的余额,然后往已购课程里面加上一门课。
如果时间顺序上是先备份账户余额表(u\_account),然后用户购买,然后备份用户课程表(u\_course),会怎么样呢?你可以看一下这个图:
![](https://static001.geekbang.org/resource/image/cb/cd/cbfd4a0bbb1210792064bcea4e49b0cd.png)
图1 业务和备份状态图
可以看到这个备份结果里用户A的数据状态是“账户余额没扣但是用户课程表里面已经多了一门课”。如果后面用这个备份来恢复数据的话用户A就发现自己赚了。
作为用户可别觉得这样可真好啊,你可以试想一下:如果备份表的顺序反过来,先备份用户课程表再备份账户余额表,又可能会出现什么结果?
也就是说,不加锁的话,备份系统备份的得到的库不是一个逻辑时间点,这个视图是逻辑不一致的。
说到视图你肯定想起来了,我们在前面讲事务隔离的时候,其实是有一个方法能够拿到一致性视图的,对吧?
是的,就是在可重复读隔离级别下开启一个事务。
> 备注如果你对事务隔离级别的概念不是很清晰的话可以再回顾一下第3篇文章[《事务隔离:为什么你改了我还看不见?》](https://time.geekbang.org/column/article/68963)中的相关内容。
官方自带的逻辑备份工具是mysqldump。当mysqldump使用参数single-transaction的时候导数据之前就会启动一个事务来确保拿到一致性视图。而由于MVCC的支持这个过程中数据是可以正常更新的。
你一定在疑惑有了这个功能为什么还需要FTWRL呢**一致性读是好,但前提是引擎要支持这个隔离级别。**比如对于MyISAM这种不支持事务的引擎如果备份过程中有更新总是只能取到最新的数据那么就破坏了备份的一致性。这时我们就需要使用FTWRL命令了。
所以,**single-transaction方法只适用于所有的表使用事务引擎的库。**如果有的表使用了不支持事务的引擎那么备份就只能通过FTWRL方法。这往往是DBA要求业务开发人员使用InnoDB替代MyISAM的原因之一。
你也许会问,**既然要全库只读为什么不使用set global readonly=true的方式呢**确实readonly方式也可以让全库进入只读状态但我还是会建议你用FTWRL方式主要有两个原因
* 一是在有些系统中readonly的值会被用来做其他逻辑比如用来判断一个库是主库还是备库。因此修改global变量的方式影响面更大我不建议你使用。
* 二是在异常处理机制上有差异。如果执行FTWRL命令之后由于客户端发生异常断开那么MySQL会自动释放这个全局锁整个库回到可以正常更新的状态。而将整个库设置为readonly之后如果客户端发生异常则数据库就会一直保持readonly状态这样会导致整个库长时间处于不可写状态风险较高。
业务的更新不只是增删改数据DML)还有可能是加字段等修改表结构的操作DDL。不论是哪种方法一个库被全局锁上以后你要对里面任何一个表做加字段操作都是会被锁住的。
但是,即使没有被全局锁住,加字段也不是就能一帆风顺的,因为你还会碰到接下来我们要介绍的表级锁。
# 表级锁
MySQL里面表级别的锁有两种一种是表锁一种是元数据锁meta data lockMDL)。
**表锁的语法是 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命令来控制并发毕竟锁住整个表的影响面还是太大。
**另一类表级的锁是MDLmetadata lock)。**MDL不需要显式使用在访问一个表的时候会被自动加上。MDL的作用是保证读写的正确性。你可以想象一下如果一个查询正在遍历一个表中的数据而执行期间另一个线程对这个表结构做变更删了一列那么查询线程拿到的结果跟表结构对不上肯定是不行的。
因此在MySQL 5.5版本中引入了MDL当对一个表做增删改查操作的时候加MDL读锁当要对表做结构变更操作的时候加MDL写锁。
* 读锁之间不互斥,因此你可以有多个线程同时对一张表增删改查。
* 读写锁之间、写锁之间是互斥的,用来保证变更表结构操作的安全性。因此,如果有两个线程要同时给一个表加字段,其中一个要等另一个执行完才能开始执行。
虽然MDL锁是系统默认会加的但却是你不能忽略的一个机制。比如下面这个例子我经常看到有人掉到这个坑里给一个小表加个字段导致整个库挂了。
你肯定知道给一个表加字段或者修改字段或者加索引需要扫描全表的数据。在对大表操作的时候你肯定会特别小心以免对线上服务造成影响。而实际上即使是小表操作不慎也会出问题。我们来看一下下面的操作序列假设表t是一个小表。
> 备注这里的实验环境是MySQL 5.6。
![](https://static001.geekbang.org/resource/image/7c/ce/7cf6a3bf90d72d1f0fc156ececdfb0ce.jpg)
我们可以看到session A先启动这时候会对表t加一个MDL读锁。由于session B需要的也是MDL读锁因此可以正常执行。
之后session C会被blocked是因为session A的MDL读锁还没有释放而session C需要MDL写锁因此只能被阻塞。
如果只有session C自己被阻塞还没什么关系但是之后所有要在表t上新申请MDL读锁的请求也会被session C阻塞。前面我们说了所有对表的增删改查操作都需要先申请MDL读锁就都被锁住等于这个表现在完全不可读写了。
如果某个表上的查询语句频繁而且客户端有重试机制也就是说超时后会再起一个新session再请求的话这个库的线程很快就会爆满。
你现在应该知道了事务中的MDL锁在语句执行开始时申请但是语句结束后并不会马上释放而会等到整个事务提交后再释放。
基于上面的分析,我们来讨论一个问题,**如何安全地给小表加字段?**
首先我们要解决长事务事务不提交就会一直占着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 ...
```
# 小结
今天我跟你介绍了MySQL的全局锁和表级锁。
全局锁主要用在逻辑备份过程中。对于全部是InnoDB引擎的库我建议你选择使用single-transaction参数对应用会更友好。
表锁一般是在数据库引擎不支持行锁的时候才会被用到的。如果你发现你的应用程序里有lock tables这样的语句你需要追查一下比较可能的情况是
* 要么是你的系统现在还在用MyISAM这类不支持事务的引擎那要安排升级换引擎
* 要么是你的引擎升级了但是代码还没升级。我见过这样的情况最后业务开发就是把lock tables 和 unlock tables 改成 begin 和 commit问题就解决了。
MDL会直到事务提交才释放在做表结构变更的时候你一定要小心不要导致锁住线上查询和更新。
最后我给你留一个问题吧。备份一般都会在备库上执行你在用single-transaction方法做逻辑备份的过程中如果主库上的一个小表做了一个DDL比如给一个表上加了一列。这时候从备库上会看到什么现象呢
你可以把你的思考和观点写在留言区里,我会在下一篇文章的末尾和你讨论这个问题。感谢你的收听,也欢迎你把这篇文章分享给更多的朋友一起阅读。
说明:这篇文章没有介绍到物理备份,物理备份会有一篇单独的文章。
# 上期问题时间
上期的问题是关于对联合主键索引和InnoDB索引组织表的理解。
我直接贴@老杨同志 的回复略作修改如下(我修改的部分用橙色标出):
表记录
a--|b--|c--|d--
1 2 3 d
1 3 2 d
1 4 3 d
2 1 3 d
2 2 2 d
2 3 4 d
主键 ab的聚簇索引组织顺序相当于 order by a,b 也就是先按a排序再按b排序c无序。
索引 ca 的组织是先按c排序再按a排序同时记录主键
c--|a--|主键部分b\-- 注意这里不是ab而是只有b
2 1 3
2 2 2
3 1 2
3 1 4
3 2 1
4 2 3
这个跟索引c的数据是一模一样的。
索引 cb 的组织是先按c排序在按b排序同时记录主键
c--|b--|主键部分a\-- (同上)
2 2 2
2 3 1
3 1 2
3 2 1
3 4 1
4 3 2
所以结论是ca可以去掉cb需要保留。
评论区留言点赞:
> @浪里白条 帮大家总结了复习要点;
> @约书亚 的问题里提到了MRR优化
> @HwangZHen 留言言简意赅。