Mysql - 争取一篇讲清楚Mysql中的锁(全局锁、表级锁、行级锁)及加锁规则、死锁问题

目录

全局锁(Flush tables with read lock (FTWRL))

表级锁(表锁、MDL读锁写锁)

行级锁(行锁、临键锁、间隙锁)

死锁和死锁检测


    Mysql中的锁比较多并且加锁规则复杂,争取这一篇博客可以分析清楚,锁分为全局锁和表级锁是整个Mysql实例和所有的存储引擎都可以使用的。行级锁是InnoDB引擎特有的,而InnoDB最大的区别于其他引擎的特点就是事务,事务与锁、MVCC、Redo Log、Undo Log、Binlog都有关系,所以InnoDB为了实现事务代价还是比较大的。因为抛开事务谈锁,本身没有意义,所以简单阐明事务与锁的关系。

    事务有四个特点原子性、一致性、持久性、和隔离性(读未提交、读已提交、可重复读、串行化),并且事务的特性并不是Mysql或者InnoDB特有的,事务隔离级别本身就是对数据一致性的妥协,事务隔离级别越高,数据库并发性能越低,一致性越高。Mysql基于binglog和redo log的二阶段提交方式保证了一定持久化;基于事务的回滚机制保证了操作的原子性(要不都提交,要不都回滚),而回滚本身依赖于回滚段的数据(底层就是重做日志,undo log);一致性基于多版本并发控制(MVCC)实现,而MVCC底层就是基于Undo Log 和 数据行的版本号实现。为了实现各种事务隔离级别,使用了不同级别的锁,为了实现可重复读,使用了表锁、临建锁、间隙锁(更多加锁细节需要配合事务博客:后面写)。

全局锁(Flush tables with read lock (FTWRL))

    全局锁的使用场景就是给全库作逻辑备份,让数据库可以恢复到最近一段时间(比如一个月)之内的某一时刻,这对数数据安全,对应系统本身是非常重要的。实现方式就是 Flush tables with read lock,简称 FTWRL,让整个库处于只读状态。这个很好理解,可以让数据恢复到最近一段时间的某一时刻,就是基于最近的全量数据备份 + 备份开始的binlog操作。如果在进行整库备份时,发生了修改操作(比如讲某一行的字段从1改成2),数据备份的是原来的数据(备份的是值为1的操作),binlog从备份完成开始记录也没有了该操作记录(即没有1改成2的binlog),那么进行数据恢复时就会漏掉一部分操作。所以使用了全局锁时,所有写相关的操作都会处于阻塞等待状态,还是比较危险的。阻塞的操作包括:

  •     数据的修改语句DML:insert、delete、update语句
  •     数据的定义语句DDL:alter table、create table、drop table等
  •     所有事务的相关操作:开启事务、提交事务、回滚事务
  •     从库不能同步主库同步的binlog,增加主从同步延迟

    FTWRL操作会导致那么多的操作处于阻塞状态,目的就是为了拿到一致性视图(全程拿到某一时刻的快照),代价未免太大了,那么有没有其他的方案呢?

1、将数据库这种为从库一样的只读状态,set global readonly=true,这样也不能解决上面的阻塞问题,并且还会增加额外的问题(比如有点操作会拿是否数据库只读判断是否从库,客户端处理异常的方式也不同,总之不行)

2、利用InnoDB的默认事务隔离级别可重复度获取一致性视图,的确是可行的,前提是所有的数据库表都使用了InnoDB引擎,所以这也是有些公司要求数据库表必须使用InnoDB引擎的原因之一

    官方自带的逻辑备份工具是 mysqldump。当 mysqldump 使用参数–single-transaction 的时候,导数据之前就会启动一个事务,来确保拿到一致性视图,至于一致性视图的本质就是开始事务时会有一个对应的 read-view,即当时的版本号,每次获取数据MVCC

(多版本并发控制)的数据时,都是回去的Undo Log的对应版本数据,这样就每次都获取到了快照时刻 read-view的状态数据。

 

表级锁(表锁、MDL读锁写锁)

表锁

    表级锁包括表锁和元数据锁(Meta Data Lock,简称MDL),基于历史原因在没有InnoDB引擎前表锁就是最小粒度的锁,索引相关引擎都会直接使用表锁,表锁本身是互斥锁。表锁的添加方式是:lock tables ... read/write,而解锁为加锁线程执行:unlock tables 或者当客户端发生异常时也会自动解锁。

    

    MyISAM引擎在执行 update等语句时会直接加表锁;

    InnoDB在开启事务,并且隔离级别为串行化(serializable)时也会加表锁;

    InnoDB的行级锁退化为表锁的情况。比如:update t set column_1 += 1 where column_1 = 55; 如果column_1 没有索引,那么就是加的表锁

原数据锁

     MySQL 5.5 版本中引入了元数据锁(MDL),原数据锁(MDL)分为读锁和写锁,可以理解成类似Java中的ReentrantReadWriteLock中的ReadLock、WriteLock,读锁与读锁共享,读锁和写锁、写锁和写锁互斥。对数据库insert、update、select、delete都会先获取 MDL读锁;对数据库表结构变更操作都会先获取MDL写锁(alter 增加列,删除列等);并且MDL读锁、写锁都是自动加的,在操作完成后释放。

    需要注意只要有MDL写锁介入,本身就会阻塞获取锁的操作,经常会遇到线上给一个表加字段,或者加索引导致整个库挂掉【前提是并发量比较高】。我们之前项目上就遇到过COE,因为加索引导致整个系统类似瘫痪。场景如下:

   1)、开始事务,需要执行一大堆逻辑(长事务),而其中一步就是 对表A的检查查询操作,此时会自动获取MDL读锁

   2)、修改表结构比如增加一列,那么会扫描整张表的数据,会自动加MDL写锁,此时上一步获取的MDL读锁还没有释放,MDL读写锁是互斥的则需要一致等待上面的MDL锁在提交事务后释放。

   3)、高并发场景下,其他线程并发查询该表,默认会获取 MDL读锁本身就会阻塞(与上一步的MDL写锁互斥), 客户端有重试机制,也就是说超时后会再起一个新 session 再请求的话,这个库的线程很快就会爆满。严重的话会导致整个库类似瘫痪或者库挂掉。

分析完成了MDL读写锁导致的严重后果,怎么进行避免呢?

    1)、像MariaDB一样已经支持在为表添加列时可以设置超时,ALTER TABLE tbl_name NOWAIT add column ...  或者 ALTER TABLE tbl_name WAIT N add column ... 

    2)、查询长事务的线程,kill掉。select * from information_schema.innodb_trx where TIME_TO_SEC(timediff(now(),trx_started))>60; 

 

行级锁(行锁、临键锁、间隙锁)

    行锁就是锁在主键索引的B+树上,就是一条记录,行锁本身就是互斥锁。并且在InnoDB引擎事务中,行锁是在需要的时候加上的(即下面的行锁加锁规则,查询的过程中查询到了才加锁),但是并不是不需要了就马上释放,而是要等到事务结束(提交 or 回滚)时才释放。这个就是两阶段锁协议。基于两阶段锁协议,如果你的事务中需要锁多个行,要把最可能造成锁冲突、最可能影响并发度的锁尽量往后放,因为他们离事务结束更近,MDL和行锁的锁持有时间最短。临键锁(Next-key Lock)是一个前开后闭的区间,间隙锁(gap Lock)是一个前开后开的区间。

    比如当前数据库主键聚簇索引中有:1,2,5,8四条数据,那么分别拥有的锁有:

    行锁: 1,2,5,8   这个比较好理解

    临键锁: (-∞, 1] 、(1, 2]、(2, 5]、(5, 8]、(8, +∞]

    间隙锁:(-∞, 1) 、(1, 2)、(2, 5)、(5, 8)、(8, +∞)

    可以理解如下:

        临键锁 = 间隙锁 + 行锁

        (1,2]    =  (1,2)    +   2

    理解行级锁一定是理解事务的基础上,因为行级锁就是为了牺牲一致性,为了事务的隔离级别作的妥协,否则 InnoDB的串行化事务隔离级别与 MyISAM的修改操作 都会使用表锁了。 事务隔离级别对应可能存在的问题如下:

    特别是为了解决不可重复读的问题,这里需要简明阐述什么是不可重复读:

    1、表中有主键为: 1,4,5,6 的四条数据

    2、启动事务,线程 A 第一次执行 select  count(*) from table where id < 8, 获取到结果为 4 条数据;

    3、线程 B 执行了一条insert语句,主键为 2

    4、线程 A 再次执行  select  count(*) from table where id < 8, 获取到结果为 5 条数据; // 线程A说我是不是产生了幻觉

  怎么避免这种情况呢? 使用行锁,行锁是作用在 主键索引的B+树上的,此时 行2 还不存在,怎么防止或者让 insert id 为2的线程B阻塞呢? 此时就需要使用临键锁(或间隙锁)将中间的间隙都锁住(即为什么锁是区间的)。其实对于不止新增操作,删除操作也一样的,也可能照成幻读的问题。Mysql使用锁机制+MVCC(视图 + 版本并发 + UndoLog) 解决了不可重复读隔离级别下,如上的查询问题,因为其走的是快照读。但是如果线程A中的是当前读操作(update、delete等),则还是会有幻读的问题,则需要通过串行化隔离级别(使用表锁互斥执行解决)。具体的加锁规则如下,具体规则说明在事务中进行分析:

1、加锁的基本规则是next lock【临键锁,前开后闭】; 查询的过程中查到的对象才会加锁;

2、索引上的等值查询,给唯一索引加锁时,next lock退化为行锁;索引上的等值查询,向右遍历时且最后一个值不满足等值条件的时候,next-key lock退化为gap lock;

3、唯一索引上的范围查询,会查询到不满足条件的第一个值为止

 

死锁和死锁检测

    当并发系统中不同线程出现循环资源依赖的时候,涉及的线程都在等待别的线程释放资源时,就会导致这几个线程都进入无限循环等待状态,称为死锁。

    Mysql设计了各种级别的锁,目的就是为了适应各种数据库操作,在Mysql多线程高并发情况下都可以正确的执行。但是锁的应用场景必定会产生死锁的问题,这其实与Java并发编程的死锁问题是一样的,可以参见:并发编程基础 - 死锁Coffman总结的发生死锁需要同时满足四个必要的条件,以及破坏死锁在这里任然是有效的。1)、破坏占有且等待  2)、破坏不可抢占  3)、破坏循环等待

    回到数据库我们看看死锁是怎么产生的,比如:

   并且Mysql中产生死锁的情况总结如下(从左到右看,是想要获取锁的类型,去碰到了别的线程已经持有锁的类型):

    怎么防止死锁,或者发生了死锁应该怎么办呢?

1)、Mysql提供了配置参数, innerdb_lock_wait_timeout 控制获取锁的等待超时时间

    参数是默认超时时间是 50s,默认时间本身太长。但是这个与分布式锁的超时时间一样的原理和问题,如果设置时间太短可能会把正常业务执行的误伤了,客户端可能会收到比较多异常。但是如果设置超时时间太长,可能会照成大量的线程等待,处于阻塞状态,客户端表现为执行时间过长,间接的影响用户体验。

2)、开启死锁检测,Mysql配置参数 innerdb_deadlock_detect 设置为 on

    当开启死锁检测后,发现死锁则主动回滚死锁链中的某一个事务,利用了上面的 破坏循环等待,解开死锁。一般会建议使用死锁检测代替上面的设置超时时间。但是死锁检查的复杂度是 O(n), 那边并发度是1000的时候同时修改同一行数据【这个并发度算不高的,执行了类似秒杀的操作】,
则进行死锁检查操作就是100万的量级,最终发现没有死锁,但是照成了CPU的消耗。

    真正的杀敌一千自损八百啊。所以,可以选择优化:

1、如果确定应用不可能照成死锁,则可以暂时关闭;

2、另一种思路就是控制并发度,比如应用方添加限流器;

3、如果团队有能修改 Mysql源码的DBA;

 

 

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值