06 | 全局锁和表锁

148 阅读6分钟

根据加锁的范围,MySQL里面的锁大致可以分为全局锁、表级锁、行锁。

全局锁

  • 全局锁是对整个数据库实例进行加锁。

  • MySQL加全局锁的方法:执行命令(Flush tables with read lock(FTWRL))。

  • 当你需要整个库处于只读状态时,可以用这个命令,之后其他的线程的以下语句会被阻塞:数据库更新(增删改)、数据定义语句(建表、修改表结构)和更新事务的提交语句。

  • 全局锁使用场景全库逻辑备份。(将整库的每个表都select出来存成文本备份)

官方自带的逻辑备份工具是 mysqldump。当 mysqldump 使用参数–single-transaction的时候,导数据之前就会启动一个事务,来确保拿到一致性视图。而由于 MVCC 的支持,这个过程中数据是可以正常更新的,其实是可以达到同样的备份效果,有了这个功能,为什么还需要 FTWRL呢?一致性读是好,但前提是引擎要支持这个隔离级别。比如,对于 MyISAM 这种不支持事务的引擎,如果备份过程中有更新,总是只能取到最新的数据,那么就破坏了备份的一致性。这时,我们就需要使用FTWRL 命令了。

既然要全库只读,为什么不使用set global readonly=true的方式呢?

readonly 方式也可以让全库进入只读状态,但我还是会建议你用 FTWRL 方式,主要有两个原因:

    1. 在有些系统中,readonly 的值会用来判断一个库是主库还是备库。因此,修改 global 变量的方式影响面更大,不建议使用。
    1. 在异常处理机制上有差异,如果执行FTWRL命令之后客户端发生异常断开,那么MySQL会自动释放全局锁,整个库可以回到可以正常更新的状态。若将整个库设置为readonly之后,如果客户端发生异常,则数据库会一直保持readonly状态,这样可能会导致整个库长时间处于不可写的状态,风险高。

表级锁

MySQL表级锁分为2种:表锁、元数据锁(meta data lock,MDL)。

表级锁:表锁

  • 表锁语法:lock tables ... read/write。
  • 释放锁(与FTWRL类似):
    • 用unlock tables 主动释放锁,
    • 或者客户端断开的时候自动释放。

注意:lock tables语法除了会限制别的线程读写,也限定了本线程接下来的操作对象。

线程A执行 lock tables t1 read, t2 write; 会产生如下效果:

  • 其他线程写t1、读写t2 的语句都会被阻塞。(t1可读不可写,t2不可读&写)
  • 同时,线程 A 在执行 unlock tables 之前,也只能执行读t1、读写t2 的操作。连写t1都不允许,自然也不能访问其他表。
A线程执行命令A线程本身B线程
lock tables t1 read可读,不可写可读,不可写
lock tables t1 write可读写不可读写

为什么这么设计read和write表级锁?

  • 读锁的设计考虑
    • 提高并发效率:当表被一个事务加上读锁时,当前事务仍然可以继续读操作,但不能写,其他线程依然可读,可以提高并发读的效率。
  • 写锁的的设计考虑
    • 独占性:当表被一个事务加上写锁后,其他事务读写会被阻塞,只有当前事务可读写,确保在写入数据时独占资源

对于 InnoDB 这种支持行锁的引擎,一般不使用 lock tables 命令来控制并发,毕竟锁住整个表的影响面还是太大。

表级锁:元数据锁(MDL)

  • MDL不需要显式使用,在访问一个表的时候会被自动加上。
  • MDL 的作用是,保证读写的正确性。比如,一个查询正在遍历一个表中的数据,执行期间另一个线程对这个表结构做变更,删了一列,那么查询线程拿到的结果跟表结构对不上,肯定是不行的。

在 MySQL 5.5 版本中引入了 MDL,

  1. 当对一个表做增删改查操作的时候,加 MDL读锁
  2. 当要对表做结构变更操作的时候,加 MDL写锁
  • 读锁之间不互斥,因此允许多个线程同时对一张表增删改查。
  • 读写锁之间、写锁之间是互斥的,用来保证变更表结构操作的安全性。因此,如果有两个线程要同时给一个表加字段,其中一个要等另一个执行完才能开始执行。

表锁例子:给一个小表加个字段,导致整个库挂了场景

image.png

  1. 我们可以看到 session A 先启动,这时候会对表 t 加一个 MDL 读锁。
  2. session B 需要的也是 MDL 读锁,因此可以正常执行。
  3. session C 会被阻塞,是因为 session A的MDL读锁还没有释放,而 session C需要 MDL写锁(读写锁之间是互斥的),因此只能被阻塞。
  4. 如果只有 session C 自己被阻塞还没什么关系,但是之后所有要在表 t 上新申请 MDL 读锁的请求也会被 session C 阻塞。前面我们说了,所有对表的增删改查操作都需要先申请MDL读锁,就都被锁住,等于这个表现在完全不可读写了。

如果某个表上的查询语句(如session A)频繁,而且客户端有重试机制,也就是说超时后会再起一个新session 再请求的话,这个库的线程很快就会爆满。

表锁例子:如何安全地给小表加字段?

首先考虑:首先我们要解决长事务,事务不提交,就会一直占着 MDL 锁。在 MySQL的information_schema 库的 innodb_trx 表中,你可以查到当前执行中的事务。如果你要做DDL变更的表刚好有长事务在执行,要考虑先暂停DDL,或者 kill 掉这个长事务。

但考虑一下这个场景,如果你要变更的表是一个热点表,虽然数据量不大,但是上面的请求很频繁,而你不得不加个字段,此时kill未必管用,因为新的请求马上就过来了。

比较理想的机制是,在 alter table语句里面设定等待时间,如果在这个指定的等待时间里面能够拿到 MDL 写锁最好,拿不到也不要阻塞后面的业务语句,先放弃。之后开发人员或者 DBA 再通过重试命令重复这个过程。