重新整理 mysql 基础篇—————表锁和全局锁[六]
前言
锁从大的方面可以分为:
1.全局锁
2.表锁
3.行锁
正文
全局锁
全局锁就是对整个数据加上读锁。
在mysql 中,加入全局锁的命令就是: Flush tables with read lock(FTWRL)
这个时候会让整个数据库处于只读状态,之后其他线程的数据更新、数据定义语句和更新类事务提交语句将会被堵塞。
那么有一个问题了,为什么不设置set global readonly=true。
一是,在有些系统中,readonly的值会被用来做其他逻辑,比如用来判断一个库是主库还是备库。因此,修改global变量的方式影响面更大,我不建议你使用。
二是,在异常处理机制上有差异。如果执行FTWRL命令之后由于客户端发生异常断开,那么MySQL会自动释放这个全局锁,整个库回到可以正常更新的状态。
而将整个库设置为readonly之后,如果客户端发生异常,则数据库就会一直保持readonly状态,这样会导致整个库长时间处于不可写状态,风险较高。
业务的更新不只是增删改数据(DML),还有可能是加字段等修改表结构的操作(DDL)。
不论是哪种方法,一个库被全局锁上以后,你要对里面任何一个表做加字段操作,都是会被锁住的。
但是,即使没有被全局锁住,加字段也不是就能一帆风顺的,因为你还会碰到接下来我们要介绍的表级锁。
全局锁的典型使用场景是,做全库逻辑备份。
为什么做全库的备份要加全局锁?又为什么建议从库备份呢?
先介绍为什么要在从库进行备份,在从库进行备份就很简单了。
1.如果在主库备份,那么在备份期间都不能执行更新,业务基本停止了。
2.在从库备份,主从会导致延迟。
这其他都不好的,第二个好点,但是呢,会出现比如用户更新了,但是人家一查询,发现没有更新,这就要打客服了。
那么有没有更好的办法呢? 可以使用热备库,有一台机器一般专门用来做热备的服务器,这样备份的时候就在热备服务器,这样就不影响线上了。
那么为什么备份的时候要加全局锁呢?
比如说,用户购买业务,里面有一张用户表,用户表里面有一个金额字段。同样,还有一张购买的商品表,如果用户购买了商品将会在这里面。
业务逻辑是先扣除用户的金额,然后再给用户加入一个商品。
为什么是先扣除金额呢?因为是这样的,我们一般会写成事务,但是呢,如果系统发送不可挽回的崩溃了,用户金额少了,商品不见了,那么用户会报告。如果是商品在,金额没少,那么这个时候是没人会报告的。
加入现在不锁表,那么出现这样的情况。
现在如果先备份用户表,这个时候是在用户表备份完了之后,然后用户开始购买成功,并且商品表里面加入了该数据,这个时候呢,备份商品表,然后你就发现一个问题,备份里面用户金额没有扣除,然后商品表多了商品。
反过来先备份商品,然后备份用户,结果就是反过来的了。
那么问题来了是不是一定要有一个热备的服务器(或者数据库)才能不影响线上用户啊?
在可重复读隔离级别下开启一个事务。
官方自带的逻辑备份工具是mysqldump。当mysqldump使用参数–single-transaction的时候,导数据之前就会启动一个事务,来确保拿到一致性视图。而由于MVCC的支持,这个过程中数据是可以正常更新的。
故而,需要在整个库的每个表示在可重复读隔离级别的情况下。
表锁
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不需要显式使用,在访问一个表的时候会被自动加上。
MDL的作用是,保证读写的正确性。你可以想象一下,如果一个查询正在遍历一个表中的数据,而执行期间另一个线程对这个表结构做变更,删了一列,那么查询线程拿到的结果跟表结构对不上,肯定是不行的。
因此,在MySQL 5.5版本中引入了MDL,当对一个表做增删改查操作的时候,加MDL读锁;当要对表做结构变更操作的时候,加MDL写锁。
读锁之间不互斥,因此你可以有多个线程同时对一张表增删改查。
读写锁之间、写锁之间是互斥的,用来保证变更表结构操作的安全性。因此,如果有两个线程要同时给一个表加字段,其中一个要等另一个执行完才能开始执行。
虽然MDL锁是系统默认会加的,但是MDL也有一个大坑。
可以看到session A先启动,这时候会对表t加一个MDL读锁。由于session B需要的也是MDL读锁,因此可以正常执行。
之后session C会被blocked,是因为session A的MDL读锁还没有释放,而session C需要MDL写锁,因此只能被阻塞。
如果只有session C自己被阻塞还没什么关系,但是之后所有要在表t上新申请MDL读锁的请求也会被session C阻塞。
前面我们说了,所有对表的增删改查操作都需要先申请MDL读锁,就都被锁住,等于这个表现在完全不可读写了。
如果某个表上的查询语句频繁,而且客户端有重试机制,也就是说超时后会再起一个新session再请求的话,这个库的线程很快就会爆满。
如果是短事务,大表依然可能出现这种情况。因为增加一个字段,时间比较长,那么可能其他语句肯定都被锁住了。
那么该怎么办呢?
在上面中,造成这个的原因是因为session A 是长事务,一直占用MDL锁。
如果发现这种情况,可以先杀死这个事务,也就是杀死这个会话。
然后如果发现sessionA 这种事务比较频繁,那么可能还是不适用。除此之外,如果表大的话,增加一个字段耗费时间比较长的话,依然会有问题。
那么可以设置另外一个东西,那就是MDL写锁限定时间,如下:
MariaDB 中可以使用:
ALTER TABLE tbl_name NOWAIT add column ...
ALTER TABLE tbl_name WAIT N add column ...
结
下一节,行锁。