@adamhand
2019-02-18T15:54:40.000000Z
字数 2896
阅读 1332
根据加锁的范围,MySQL 里面的锁大致可以分成全局锁、表级锁和行锁三类。下面看一下全局锁和表级锁。
全局锁就是对整个数据库实例加锁。MySQL 提供了一个加全局读锁的方法,命令是 Flush tables with read lock (FTWRL)
。执行这个命令之后,整个就库处于只读状态。
全局锁的典型使用场景是,做全库逻辑备份。也就是把整库每个表都 select 出来存成文本。但是使用这种方法时会有问题:
下面看一个反例,如果备份不加锁,在备份过程允许写入,会出现什么问题。
假设现在要维护“极客时间”的购买系统,关注的是用户账户余额表和用户课程表。
现在发起一个逻辑备份。假设备份期间,有一个用户,他购买了一门课程,业务逻辑里就要扣掉他的余额,然后往已购课程里面加上一门课。
如果时间顺序上是先备份账户余额表 (u_account)
,然后用户购买,然后备份用户课程表 (u_course)
,会怎么样呢?可以看一下这个图:
可以看到,这个备份结果里,用户 A 的数据状态是“账户余额没扣,但是用户课程表里面已经多了一门课”。
也就是说,不加锁的话,备份系统备份的得到的库不是一个逻辑时间点,这个视图是逻辑不一致的。
加锁会出问题,不加锁又拿不到一致性视图。那么,有没有一种方法,可以既不加锁,又能够拿到一致性视图呢?那就是在可重复读隔离级别下开启一个事务。
官方自带的逻辑备份工具是 mysqldump
。当 mysqldump
使用参数–single-transaction
的时候,导数据之前就会启动一个事务,来确保拿到一致性视图。而由于 MVCC
的支持,这个过程中数据是可以正常更新的。当然,数据库比如支持“可重复度”这个隔离级别,向MyISAM这样的引擎,就不得不使用FTWRL了。
既然FTWRL方法是将整个库设置为只读状态,那么可不可以使用set global readonly=true
的方式来代替呢?最好不要,原因有两个:
readonly
的值会被用来做其他逻辑,比如用来判断一个库是主库还是备库(在存在主从复制的库时,备库一般只用来读,所以会将其设置为只读状态)。因此,修改 global
变量的方式影响面更大,不建议你使用。FTWRL
命令之后由于客户端发生异常断开,那么 MySQL
会自动释放这个全局锁,整个库回到可以正常更新的状态。而将整个库设置为 readonly
之后,如果客户端发生异常,则数据库就会一直保持 readonly
状态,这样会导致整个库长时间处于不可写状态,风险较高。在数据库中,业务的更新不只是增删改数据(DML
)还有可能是加字段等修改表结构的操作(DDL
)`。即使没有被全局锁住,加字段也不是就能一帆风顺的,因为还有表级锁。
注意:
DML(data manipulation language
)数据操纵语言:就是我们最经常用到的 SELECT、UPDATE、INSERT、DELETE
)。主要用来对数据库的数据进行一些操作)。DDL(data definition language)
数据库定义语言:其实就是我们在创建表的时候用到的一些sql
,比如说:CREATE、ALTER、DROP
等。DDL
主要是用在定义或改变表的结构。DCL(Data Control Language)
数据库控制语言:是用来设置或更改数据库用户或角色权限的语句,包括(grant,deny,revoke
等)语句。MySQL 里面表级别的锁有两种:一种是表锁,一种是元数据锁(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 都不允许,自然也不能访问其他表。
另一类表级的锁是 MDL(metadata lock)。MDL主要解决在访问数据的时候对表做DDL操作从而导致访问数据出错。MDL 不需要显式使用,在访问一个表的时候会被自动加上。
在 MySQL 5.5 版本中引入了 MDL,当对一个表做增删改查操作的时候,加 MDL 读锁;当要对表做结构变更操作的时候,加 MDL 写锁。
但是,需要注意MDL的一个机制:事务中的 MDL 锁,在语句执行开始时申请,但是语句结束后并不会马上释放,而会等到整个事务提交后再释放。
给一个表加字段,或者修改字段,或者加索引,需要扫描全表的数据。在对大表操作的时候,需要特别小心,以免对线上服务造成影响。而实际上,即使是小表,操作不慎也会出问题。
下面看一个例子,假设表 t 是一个小表(基于mysql5.6):
上图中, session A 先启动,这时候会对表 t 加一个 MDL 读锁。由于 session B 需要的也是 MDL 读锁,因此可以正常执行。
之后 session C 会被 blocked,是因为 session A 的 MDL 读锁还没有释放,而 session C 需要 MDL 写锁,因此只能被阻塞。<并且之后所有要在表 t 上新申请 MDL 读锁的请求也会被 session C 阻塞。
如果某个表上的查询语句频繁,而且客户端有重试机制,也就是说超时后会再起一个新 session 再请求的话,这个库的线程很快就会爆满。
information_schema
库的 innodb_trx
表中,可以查到当前执行中的事务。如果要做 DDL 变更的表刚好有长事务在执行,要考虑先暂停 DDL,或者 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 ...