根据加锁的范围,MySQL里面的锁大致可以分成全局锁、表级锁和行锁三类,本文先讨论前两种。
全局锁
全局锁是对整个数据库实例加锁,MySQL提供的加全局读锁的命令是Flush tables with read lock
(下面简称FTWRL)。当需要让整个库处于只读状态时,可以使用这个命令,之后其他线程的以下语句会被阻塞:数据的增删改、数据定义语句(表的建立与修改等)、更新类事务的提交语句。
全局锁的典型使用场景是全库逻辑备份,即把库中每个表都select出来存成文本。
让整个库都处于只读,有些弊端:
-
若在主库上备份,那么在备份期间都不能执行更新,业务会停滞;
-
若在从库上备份,那么备份期间从库不能执行主库同步过来的binlog,导致主从延迟。
那么既然备份加锁有这么多弊端,是否可以不加锁呢?
答案是不行的,考虑有用户余额表和订单表两张表,当用户发起一次购买,需要在用户余额表扣减余额,在订单表里增加一笔订单。若顺序是先备份用户余额表,然后用户购买,再备份订单表,不难发现会出现问题,顺序反之亦然。
官方自带的逻辑备份工具是mysqldump,当mysqldump使用参数single-transaction
,在导数据之前就会启动一个事务,拿到一致性的视图。由于MVCC的支持,这个备份过程中数据可以正常更新。
那既然官方有这个工具,为什么需要FTWRL呢?答案是有些引擎并不支持可重复读的隔离级别,这时候必须使用FTWRL命令。因此,只有所有表都使用事务引擎的库 才能使用single-transaction
。
还有一种想法是,为了让全库只读,使用set global readonly=true
的方式。但还是建议使用FTWRL,原因如下:
-
在有些系统中,readonly的值会被用作其他逻辑,比如判断库是主库还是从库。
-
在异常处理机制上,如果执行FTWRL命令之后由于客户端发生异常断开,那么MySQL会自动释放这个全局锁,整个库回到可以正常更新的状态。而将整个库设置为readonly之后,如果客户端发生异常,则数据库会一直保持该状态,导致整个库长时间处于不可写状态,风险较高。
表级锁
这里介绍两种表级别的锁,一种是表锁,一种是元数据锁。
表锁的语法是lock tables ... read/write
,可以用unlock tables
主动释放锁,也可以在客户端断开时自动释放锁。表锁除了限制其他线程的读写,也限制本线程接下来的操作。
在没有出现更细粒度的锁的时候,表锁是最常用的处理并发的方式。对于InnoDB这种支持行锁的引擎,一般不使用lock tables
来控制并发。
元数据锁(MDL)不需要显式使用,在访问一个表的时候会被自动加上。其作用是保证读写的正确性。MDL在MySQL 5.5版本引入,当对一个表做增删改查时,会加MDL读锁;当要对表做结构变更操作时,会加MDL写锁。
-
读锁不互斥,因此可以有多个线程同时对一张表增删改查。
-
读写锁、写锁之间互斥,用来保证变更表结构操作的正确性。
要注意的是,事务中的MDL锁,在语句执行开始时申请,但语句结束后并不会马上释放 ,而会等到整个事务提交后释放。考虑下面这样一个例子:
首先事务启动,然后session A会对表t加一个MDL读锁,由于session B需要的也是读锁,第二条查询语句也能正常执行。但之后session C会被阻塞,因为前面的读锁还没释放,而session C需要写锁。当session C阻塞,之后只需要读锁的请求也都会被阻塞,等同于这张表此时完全不可读写。
如果某张表上查询语句频繁,而客户端有重试机制,即超时后再起session请求,那么在上述情况下库的线程很快就会爆满。
基于以上分析,考虑一个场景,假设要对一张小表加字段,这张表数据量不大,但是请求很频繁,该怎么做?
此时因为请求频繁,不能简单kill事务。比较好的方法是,在alter table
语句里设置等待时间,如果在等待时间里能拿到MDL写锁就进行修改,拿不到也不阻塞后面的业务语句,先放弃修改,之后开发人员人工介入。
目前已有相应的语句:
sql
ALTER TABLE tbl_name NOWAIT add column ...
ALTER TABLE tbl_name WAIT N add column ...
参考资料:极客时间专栏《MySQL实战45讲》https://time.geekbang.org/column/intro/100020801?tab=catalog