暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

浅谈MySQL三种锁:全局锁、表锁和行锁

1229

【此为"一森咖记"公众号——第25篇文章】

【前言】


锁是服务器在协调多个进程/线程并发访问某一资源的访问机制。

数据是一种用户共享的资源,保证数据并发访问的一致性、有效性是关系型数据库的本质问题,锁冲突同时也是也是影响数据库并发访问性能重要度量指标。要想学好关系型数据库,锁机制是DBA一个绕不开的话题。

 

今天就讲一讲MySQL的三种锁。根据加锁的范围,MySQL里面的锁大致可以分成全局锁,表级锁,行锁;本文主要讲述MySQL的这三种锁的原理和用途。

一、 全局锁

 

MySQL 提供了一种加全局读锁的方法,命令是Flush tables with read lock (FTWRL)。全局锁就是对整个数据库进行加锁。当你需要让整个库处于只读状态的时候,可以使用这个命令,之后的其他线程的以下语句会被阻塞:数据DML的更新操作(数据的增删改)、数据DDL的定义语句(包括建表、修改表结构等)和事务更新类的提交语句。

 

1.1 全局锁的应用场景

 

全局锁的一个非常典型场景为:

        1、 使用mysqldump做全库逻辑备份;

        2、 主库在做完全库逻辑备后在从库上做主从配置。使用mysqldump做初始化工作。

 

以前的常规做法是使用Flush tables with read lock(FTWRL )锁全库,然后对整个库进行逻辑备份,命令如下:

    mysql> Flush tables with read lock;
    [root@ethanDB ~]# mysqldump -u root -p --all-databases > /mysql/backup/mysql_`date '+%m-%d-%Y'`.sql


    这种场景下会带来一定的局限性:

        1. 如在主库mysqldump操作,备份期间数据库不能执行增删改,影响业务正常使用,在备份时间长的情况下,此操作业务不可容忍;

        2. 如在从库mysqldump操作,那么备份期间从库不能执行主库同步过来的binlog进行同步更新,导致主从延迟。可以在从库上执行如下命令,参考Seconds_Behind_Master,多刷几遍命令可以看到参数一直在增大。


       mysql> show slave status \G

       

      注意:上述场景为--single-transaction参数的逻辑备。如果想不影响业务正常使用,则可使用如下命令:

        [root@ethanDB ~]#/mysqlsoft/mysql/bin/mysqldump -uroot -p \
        --all-databases --single-transaction \
        --socket=/mysql/3307/data/mysqld.sock  \
        --master-data=2  >/mysql/backup/mysql_`date '+%m-%d-%Y'`.sql

        --single-transaction的参数意义,可以使用命令进行查看

          [root@ethanDB ~] ~]# mysqldump --help
          --single-transaction
          Creates a consistent snapshot by dumping all tables in a
          single transaction. Works ONLY for tables stored in
          storage engines which support multiversioning (currently
          only InnoDB does); the dump is NOT guaranteed to be
          consistent for other storage engines. While a
          --single-transaction dump is in process, to ensure a
          valid dump file (correct table contents and binary log
          position), no other connection should use the following
          statements: ALTER TABLE, DROP TABLE, RENAME TABLE,
          TRUNCATE TABLE, as consistent snapshot is not isolated
          from them. Option automatically turns off --lock-tables.

           

          那么,加全局锁如此不好,备份为什么要加锁呢?

           

          这是因为数据一致性的问题,如不加全局锁的话,备份发起时库的数据一致性和恢复后的数据库一致性不在一个逻辑时间点,会导致备份数据库和恢复数据库的数据逻辑不一致的。


          为什么需要全局读锁(FTWRL)

           

          官方自带的逻辑备份工具是 mysqldump。当 mysqldump 使用参数--single-transaction的时候,导数据之前就会启动一个事务,来确保拿到一致性快照视图。由于 MVCC 的支持,备份库期间数据是可以正常更新的。


          MVCC是个嘛?


          MVCCMulti-Version Concurrency Control | 多版本并发控制) InnoDB通过为每一行记录添加两个额外的隐藏的值来实现MVCC,这两个值一个记录这行数据何时被创建,另外一个记录这行数据何时过期(或者被删除)。但是InnoDB并不存储这些事件发生时的实际时间,相反它只存储这些事件发生时的系统版本号(LSN)。这是一个随着事务的创建而不断增长的数字。每个事务在事务开始时会记录它自己的系统版本号。每个查询必须去检查每行数据的版本号与事务的版本号是否相同


          MVCC对普通的SELECT不加锁,直接利用MVCC读取指版本的值,避免了对数据重复加锁的过程。Undo log是Innodb MVCC重要组成部分,InnoDB的MVCC就是基于Undo log实现的。InnoDB支持MVCC多版本,其中RC和RR隔离级别是利用consistent read view方式支持的,即在某个时刻对事物系统打快照记下所有活跃读写事务ID,之后读操作根据事务ID与快照中的事务ID进行比较,判断可见性。

           

          综上,MVCC的实现,通过保存数据在某个时间点的快照来实现的。意味着一个事务无论运行多长时间,在同一个事务里能够看到数据一致的视图。根据事务开始的时间不同,同时也意味着在同一个时刻不同事务看到的相同表里的数据可能是不同的。


          为什么还需要 FTWRL ?

           

          single-transaction 方法只适用于使用Innodb引擎的库。如果有的表使用了不支持事务的引擎,那么备份就只能通过 FTWRL 方法。比如,对于 MyISAM 这种不支持事务的引擎,如果备份过程中有更新,总是只能取到最新的数据,那么就破坏了备份的一致性。所以,就需要使用FTWRL 命令保持非Innodb的一致性读。

           

          这也是 DBA 要求业务开发人员使用 InnoDB 替代 MyISAM 的原因之一。


          二、表级锁

           

          MySQL表级锁有两种:一是表锁,一是元数据锁


          2.1 表锁 

          表锁的语法是 lock tables … read/write,示例如下:

            mysql > lock tables 表名 read;

            #该表可以读,不能ddl 和 dml 中增删改,只能读取表数据 

              mysql > lock tables 表名 write;

              # 既不能读,也不能写 

               

              表锁与 FTWRL 类似,可用 unlock tables 主动释放锁,客户端断开时也会自动释放。


              注意:

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

               

              在还没有出现更细粒度的锁的时候,表锁是最常用的处理并发的方式。而对于 InnoDB 这种支持行锁的引擎,一般不使用 lock tables 命令来控制并发,毕竟锁住整个表的影响面还是太大了。


              2.2 MDL 锁

               

              另一类表级锁是 MDL(metadata lock)。MySQL 5.5 版本中引入了 MDL,MDL 不需要显式使用,当对一个表做增删改查操作和对表做结构变更操作时候,数据库会自动加上MDL。

               

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

               

              虽然 MDL 锁是系统默认会加的,但却不能忽略的一个机制。 比如在给一个小表添加个字段,缺导致整个数据库长时间业务不可用。 为什么? 以下为一个简单场景解释:

                Session1:select * from ethan_table;
                Session2:select * from ethan_table;
                Session3:alter tables ethan_table add(col_add varchar);
                Session4:select * from ethan_table;
                Session2:insert into ethan_table values(...);

                 

                在读取ethan_table时,Session1对表做了一个 MDL读锁,到了session2也对ethan_table加了一个 MDL读锁;这时session1和session2均可以正常操作;之后 session3 需要MDL 写锁,会被 blocked,是因为 session A 的 MDL 读锁还没有释放,因此只能被阻塞。如果只有 session 3 自己被阻塞还好,但是之后所有要在表 ethan_table 上新申请 MDL 读锁的请求也会被 session C3阻塞。而所有对表的增删改查操作均需先申请MDL 读锁,因此后续的此类操作都被锁住,业务上来看就感觉此时这个库完全不可读写/不可用了。

                 

                事务中的 MDL 锁,在语句执行开始时申请,但是语句结束后并不会马上释放,而会等到整个事务提交后再释放。若ethan_table表上的查询频繁,且客户端有重试机制,超时后会再起一个新session 请求,一段时间后数据库库的线程很快爆满,新发起的连接不能成功,业务表象是数据库完全不可用。


                划重点:

                行锁都有锁超时设定。但MDL锁没有超时限制,只要事务没提交就会一直锁。


                所以,在对大表操作时,大家都会特别小心,但更新小表时,如发生此类现象,也要学会分析。

                 

                那怎么分析?怎么解决MDL锁?

                 

                出现MDL时,就需要提交或者回滚事务。首先要找到这个事务,怎么找?

                 通过information_schema.innodb_trx 查看事务的执行时间 

                 

                # 查看事务超过60s的事务

                  mysql>select * from information_schema.innodb_trx \
                  where TIME_TO_SEC(timediff(now(),trx_started))>60 \
                  \G;

                  trx_started 表示什么时候执行的这个事务

                   

                  使用show full processlist; 查看这个线程id,通过其中host 字段 ,到底是谁连接了数据库,进去commit/rollback 。如不是localhost 环境,而是应用程序连接,可联系业务进行commit/rollback/kill。

                   

                   如何正确的给小表加字段?


                  如有库中有长事务,且不提交,则会一直占着 MDL 锁。使用MySQL 的information_schema 库的 innodb_trx 表可查到当前执行中的事务。如做 DDL 操作的表有长事务在执行,首先考虑暂停 DDL,或 kill 掉这个长事务。这也是为什么需要在低峰期做ddl 变更的原因。


                  三、InnoDB行锁


                  InnoDB行锁是通过给索引上的索引项加锁来实现的,这一点MySQL与Oracle不同,后者是通过在数据块中对相应数据行加锁来实现的。InnoDB这种行锁实现特点意味着:只有通过索引条件检索数据,InnoDB才使用行级锁,否则,InnoDB将使用表锁。


                  InnoDB的锁定机制和Oracle数据库有不少相似之处。InnoDB的行级锁定同样分为两种类型,共享锁和排他锁,而在锁定机制的实现过程中为了让行级锁定和表级锁定共存,InnoDB也同样使用了意向锁(表级锁定)的概念,也就有了意向共享锁和意向排他锁这两种。这里本文只介绍概念,后续将进行相应实验验证。


                  什么是共享锁和排他锁?


                  当一个事务需要给自己需要的某个资源加锁的时候,如果遇到一个共享锁正锁定着自己需要的资源的时候,自己可以再加一个共享锁,不过不能加排他锁。但是,如果遇到自己需要锁定的资源已经被一个排他锁占有之后,则只能等待该锁定释放资源之后自己才能获取锁定资源并添加自己的锁定。而意向锁的作用就是当一个事务在需要获取资源锁定的时候,如果遇到自己需要的资源已经被排他锁占用的时候,该事务可以需要锁定行的表上面添加一个合适的意向锁。如果自己需要一个共享锁,那么就在表上面添加一个意向共享锁。而如果自己需要的是某行(或者某些行)上面添加一个排他锁的话,则先在表上面添加一个意向排他锁。意向共享锁可以同时并存多个,但是意向排他锁同时只能有一个存在。所以,可以说InnoDB的锁定模式实际上可以分为四种:共享锁(S),排他锁(X),意向共享锁(IS)和意向排他锁(IX),我们可以通过以下表格来总结上面这四种所的共存逻辑关系:

                   


                  X

                  IX

                  S

                  IS

                  X

                  Conflict

                  Conflict

                  Conflict

                  Conflict

                  IX

                  Conflict

                  Compatible

                  Conflict

                  Compatible

                  S

                  Conflict

                  Conflict

                  Compatible

                  Compatible

                   

                  以下是一些注意事项:


                  1. 在不通过索引条件查询的时候,InnoDB确实使用的是表锁,而不是行锁。

                  2. MySQL的行锁是针对索引加的锁,不是针对记录加的锁,所以虽然是访问不同行的记录,但如果是使用相同的索引键,会出现锁冲突。

                  3. 当表有多个索引的时候,不同的事务可以使用不同的索引锁定不同的行,另外,不论是使用主键索引、唯一索引或普通索引,InnoDB都会使用行锁来对数据加锁。

                  4. 表创建了索引字段,但是否使用索引来检索数据由MySQL通过判断不同执行计划的代价来决定的,了解Oracle CBO机制读者可能更容易理解;如果MySQL认为全表扫描效率更高,比如对一些很小的表,它就不会使用索引,这种情况下InnoDB将使用表锁,而非行锁。

                   

                  【结语】

                  1. 本文简单介绍了MySQL的全局锁,表级锁,行锁三种锁;有些地方需要后续试验做验证;如有不清楚的地方,欢迎大家探讨;

                  2. InnoDB行锁是通过给索引上的索引项加锁来实现的,这一点MySQL与Oracle不同,后者是通过在数据块中对相应数据行加锁来实现。InnoDB这种行锁实现特点意味着:只有通过索引条件检索数据,InnoDB才使用行级锁,否则,InnoDB将使用表锁。

                  3. MySQL除了共享锁-排他锁,还有意向共享锁(IS)和意向排他锁(IX);意向锁是InnoDB自动加的,不需用户干预。对于UPDATE、DELETE和INSERT语句,InnoDB会自动给涉及数据集加排他锁(X);对于普通SELECT语句,InnoDB不会加任何锁。

                   

                  感觉写完今天这篇文章,脑袋有点晕。



                  【参考】

                  https://dev.mysql.com/doc/refman/5.6/en/innodb-locking.html#innodb-shared-exclusive-locks

                  【参考】

                  https://juejin.im/post/5ce8eee45188253114078f2a

                  【参考】

                  https://www.cnblogs.com/keme/p/11065025.html

                  【参考】

                  https://www.jianshu.com/p/7d050498d9da

                  【参考】

                  https://blog.csdn.net/laladebon/article/details/82347596


                  如果大家觉得此文有帮助,欢迎关注个人微信公众号;

                  长按识别二维码或公众号搜索“一森咖记”


                  文章转载自戏说数据那点事,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

                  评论