MySQL中的锁

122 阅读7分钟

MySQL中的锁

在MySQL中,数据库锁设计的初衷是处理并发问题。作为多用户共享的资源,当出现并发访问的时候,数据库需要合理地控制资源的访问规则。而锁就是用来实现这些访问规则的重要数据结构。那么在MySQL中究竟有些什么样的锁机制来保证并发时的数据正常呢?

MySQL中的锁大致可以分为三个类型:全局锁、表锁、行锁。

1.全局锁

1.全局锁设置以及其设置之后的状态

全局锁就是对整个数据库加锁,让整个库仅仅处于只读(readOnly)状态。

MySQL可以通过以下命令添加全局锁:

flush tables with read lock(FTWRL)

当该命令被执行后,整个库的数据更新语句(增删改语句)、数据的定义语句(DML)等会对数据库当前数据状态发生改变的语句都会被阻塞。

image-20220112163026173.png 图1.1 FRWRL添加全局锁后变为只读状态

2.设置全局锁的用途

全局锁设置之后让整个库只处于可读状态,这意味着绝大多数业务都无法正常执行。那么设置全局锁的目的何在?其实全局锁设置的目的就是为了做全库的逻辑备份。我们都知道,在做数据库备份时,无论是关系型数据库还是缓存型数据库,我们唯一要确保的就是备份的数据在某个逻辑时间点是视图一致的。倘若在备份的过程中,我们允许并且确实对数据库中的数据进行了更改操作,那么整个库备份下来,所得到的结果并非是数据库在某个时间点的一致性视图。

但是这种借助于全局锁来进行数据备份的情况在我们现实中很少碰到。通常我们进行数据库的逻辑备份时,是采用MySQL为我们提供的mysqldump工具来进行备份:

mysqldump --opt --single-transaction --master-data=2 --default-character-set=utf8 -h<host> -u<user> -p<password> -A > backup.sql

当mysqldump使用参数–single-transaction的时候,导数据之前就会启动一个事务,来确保拿到一致性视图。而由于MVCC的支持,这个过程中数据是可以正常更新的,并不会阻塞业务的执行。

那么我们为什么需要全局锁来进行逻辑备份呢?我们不能忽略了一个前提:mysqldump --opt --single-transaction是好,但最根本是数据库的存储引擎要支持事务并且支持可重复读这个隔离级别才行。比如,对于MyISAM这种不支持事务的引擎,如果备份过程中有更新,总是只能取到最新的数据,那么就破坏了备份的一致性。这时,我们就需要使用FTWRL命令了。

3.FTWRL与readonly的区别

FTWRLset global readonly = true都能让整个数据库进入只读状态,都能保证数据在备份的过程中不受更改。那么为什么要使用FTWRL来进行全库的逻辑备份呢?

  • 一是,在有些系统中,readonly的值会被用来做其他逻辑,比如用来判断一个库是主库还是备库。因此,修改global变量可能影响到其他业务的正常执行
  • 二是,在异常处理机制上有差异。如果执行FTWRL命令之后由于客户端发生异常断开,那么MySQL会自动释放这个全局锁,整个库回到可以正常更新的状态。而将整个库设置为readonly之后,如果客户端发生异常,则数据库就会一直保持readonly状态,这样会导致整个库长时间处于不可写状态,风险较高。

2.表级锁

MySQL中表级锁有两种:一种就是表锁,另一种是元数据锁(MDL,metadata lock)。

1. 表锁

表锁一般是在数据库引擎不支持行锁的时候才会被用到的。给数据库加表锁的语句如下:

lock tables tablename read/write

例如在某个连接线程中执行lock tables test read, user write表示只允许当前线程读t1,读写t2

image-20220112170935538.png

图2.1 给test表添加read表锁后当前线程无法执行

可见表锁不仅限制了别的线程的读写外,也限定了本线程接下来的操作对象。在没有更细粒度的锁出现时,表锁是最常见的并发处理方式。但锁住整个表的影响面过大,这也就导致了更细粒度的锁--->行锁的出现。

2.MDL锁

1.为什么要有MDL锁

MDL锁是为了解决在对数据库进行DML(对数据进行增删改查)操作与对数据库进行DDL(对表结构进行修改)操作时的并发冲突而引入的。想象一下,你在查询遍历一个表的数据时,另一个线程在执行期间干掉了数据库中的某一列。导致你查出来的东西跟表内容不同,这肯定是不行的。

2.MDL锁如何控制DDL与DML之间的并发冲突

MDL锁在对一个表做增删改查操作的时候,加MDL读锁

当要对表做结构变更操作的时候,加MDL写锁

  • 读锁之间并不互斥。多个线程可以同时对一张表进行增删改查操作
  • 读、写锁,写锁之间是互斥的。所以多个线程不能同时对一张表进行DDL操作,同时在一个线程进行增删改操作时,另一个线程不能对这个表进行DDL操作

MDL锁是MySQL自动为我们添加的。在语句执行开始时申请,但是语句结束后并不会马上释放,而会等到整个事务提交后再释放

image-20220112174945374.png

图2.2 一个线程给user表MDL读锁

image-20220112175116158.png

图2.3 另一个线程给user表修改结构加MDL读锁被阻塞

3.修改表结构的时机

我们从上面已经知道,修改表结构会自动添加写锁,增删改查操作会自动添加读锁。倘若我们在修改某个表结构的同时,有一个长事务正在对这个表执行增删改查操作。这就导致了我们修改表结构的语句会被阻塞。此时,如果有新的线程要对该表进行增删改查操作,那么这些线程也会被阻塞。这就导致了在那个长事务以及表结构修改语句执行完之前,整个表都处于不可读写状态。

image-20220112180701984.png

图2.4 在长事务存在情况下对表结构进行修改进而阻塞其他增删改查操作

如上图,首先开启一个长事务(语句1),即给这个表加MDL读锁,保持事务不结束(即不释放MDL读锁)。此时另一个线程对表结构进行修改(语句2),即给表加MDL写锁。这时语句2被阻塞。之后的新的线程对该表进行增删改查操作时也被阻塞住,整个表都处于不可读写状态。

所以我们在对表结构进行修改时,要确保当前表没有正在执行的长事务。如果有kill掉或者等待其执行完之后再执行DDL语句。

#查询正在运行中的事务
SELECT t.trx_mysql_thread_id                        AS connection_id
      ,t.trx_id                                     AS trx_id          
      ,t.trx_state                                  AS trx_state        
      ,t.trx_started                                AS trx_started     
      ,TIMESTAMPDIFF(SECOND,t.trx_started, now())   AS "trx_run_time(s)"  
      ,t.trx_requested_lock_id                      AS trx_requested_lock_id
      ,t.trx_operation_state                        AS trx_operation_state
      ,t.trx_tables_in_use                          AS trx_tables_in_use
      ,t.trx_tables_locked                          AS trx_tables_locked
      ,t.trx_rows_locked                            AS trx_rows_locked
      ,t.trx_isolation_level                        AS trx_isolation_level
      ,t.trx_is_read_only                           AS trx_is_read_only
      ,t.trx_autocommit_non_locking                 AS trx_autocommit_non_locking
      ,e.event_name                                 AS event_name
      ,e.timer_wait / 1000000000000                 AS timer_wait
      ,e.sql_text 
FROM   information_schema.innodb_trx t, 
       performance_schema.events_statements_current e, 
       performance_schema.threads c 
WHERE  t.trx_mysql_thread_id = c.processlist_id 
   AND e.thread_id = c.thread_id \G;

image-20220112183220251.png 图2.5 通过上述SQL脚本查询当前存在的事务执行过的上一条语句

我们就可以通过当前存在的事务执行过的上一条语句判断我们要修改结构的表是否有正在执行的长事务。但这并不是这个长事务中所有用到的表,仅仅是上一条语句所用到的表。暂时不知道如何查询mysql一个事务中涉及到的所有表。 不过如果确定当前表存在长事务,为了避免上述所说的因为修改表结构而导致的整个表都处于不可读写状态。可以kill掉当前表正在执行的长事务或者等待其执行完之后再执行DDL语句。另一种方法是给alter语句增加超时等待机制,如果指定时间未得到写锁就先放弃,避免其他线程获取MDL读锁失败。之后重试命令重复这个过程直至修改表结构成功。

写的有点多了。有关行锁的内容,留到下一篇博客吧!