MySQL锁<一>

作者: 飞翔的Tallgeese | 来源:发表于2019-09-17 10:34 被阅读0次

    MDL锁

    对表的增删改查,都需要MDL锁,无所不在

    MDL读锁之间不互斥,但MDL读写锁互斥

    #举个栗子

    假设t是一张大表

    session1对t执行一个查询(SR)

    session2对t执行一个DDL(SU,可能升级到X)

    session3对t执行一个查询(SR)

    可知session1持有t表的MDL读锁(SR),session1的查询还没有结束的时候,去执行session2的DDL(SU),此时session2需要MDL写锁(SU升级到X,需要X锁),由于MDL读写锁互斥,因此session2需要等待session1释放MDL读锁(SR阻塞X);同时session2对后面的所有MDL读锁互斥(X阻塞SR),因此session2又继续阻塞了session3...

    #注释:一开始的DDL能看到的状态是SU,但如果SU的某个阶段被阻塞,会被升级到X,从而引发SR阻塞X,达到实验的效果。但实际测试中,DDL是分阶段的,如果没有满足一定的要求,就不会引发阻塞,看到的结果就是SR和SU并没有互相阻塞。这个过程需要具体的去查看源码,此处不展开。

    事务中的MDL锁在语句开始时申请,但并不会在语句结束后就马上释放,而是会等到事务结束时才进行释放

    忙时对大表DDL会产生的灾难性的结果就是:如果后续对该表有查询操作,而且web端又有重试机制的话,那么会有一个新的session再次发起读请求,反复如此,线程池就会在短时间内爆炸

    在线执行DDL的时候,需要检查一下information_schema.innodb_trx表中有没有当前操作表对应的事务,此外还可以使用ALTER TABLE tbl_name NOWAIT...进行操作(MySQL8.0新特性)

    eg.

    session1

    select * from cpf where payid<>'xxx'

    union

    select * from cpf where payid<>'xxx'

    union (union重复50次,确保查询时间几十秒以上)

    session2

    alter table cpf modify payer_userid varchar(500);

    session3

    select * from cpf where payer_userid='18051512003600300034';

    #执行结果

    session1执行了31秒,当session1完成的时候session2和session3相继完成

    在session4中执行show processlist,结果如下

    #变种1

    如果session1在执行select之前,添加一句start transaction

    会发现session1什么时候执行完commit,sesssion2和session3什么时候完成

    也就是证实了在事务中的MDL锁,在语句查询完之后并不会释放,而是会随着事务的释放而释放

    #变种2

    session1和session3在执行select之前,添加一句start transaction,然后session1,2,3依次按顺序执行

    会发现session1阻塞了session2,而session3在执行完start transaction之后就被阻塞,根本没有办法去执行后面的select

    当session1执行commit释放之后,session2仍然处于阻塞状态,session3亦是如此

    直到session2或者session3当中任意一个执行了停止(navicat客户端操作,类似于rollback)后,另一个才能完成执行

    单纯从变种2的结果来看,MDL锁并没有按照执行时间的先后来进行分配,当session1的锁释放之后,session3先获得了读锁


    检测查找MDL锁

    MySQL是server-engine结构,MDL锁是server层的锁

    检测MDL锁

    通过show processlist可以发现waiting for table metadata lock,但这还远远不够,需要在performance_schema库中进行设置(MySQL8.0默认开启)

    5.7临时开启

    UPDATE performance_schema.setup_instruments SET ENABLED='YES', TIMED='YES' WHERE NAME='wait/lock/metadata/sql/mdl';

    5.7永久开启(修改cnf配置)

    [mysqld]

    performance-schema-instrument = 'wait/lock/metadata/sql/mdl=ON'

    锁的级别

    从上到下分4个级别

    global:全局级(FTWRL)

    schema:库级(drop database)

    table:表级(lock table read/write)

    commit:提交级

    关于global对象,主要作用是防止DDL和写操作的过程中,执行set golbal_read_only = on或flush tables with read lock。

    关于commit对象锁,主要作用是执行flush tables with read lock后,防止已经开始在执行的写事务提交。insert/update/delete在提交时都会上(COMMIT,MDL_EXPLICIT,MDL_INTENTION_EXCLUSIVE)锁

    MDL锁的种类

    ①.IX锁

    DML和DDL在执行之前都会申请IX锁,DML会在global级别上加,而DDL会在global和schema这2个级别上都加IX(也就是2把锁)

    IX与大部分锁都是兼容的,除了S,当然了X肯定是不兼容的;但IX与IX之间是兼容的,比如下图

    ②.S锁

    flush table with read lock会持有这个锁(在global级别和commit级别)

    FTWRL在全局级和事务级上分别加上了S锁

    IX与S是不兼容的

    所以DML和DDL都会与FTWRL产生阻塞

    eg.逻辑备份导致和大表DML的关系

    逻辑备份第一句:flush table with read lock(S锁)

    大表DML(IX锁)

    先执行的阻塞后执行的,逻辑备份之前需要检查是否有在线DDL(X锁)以及DML(IX锁),否则逻辑备份产生等待;尽量不要在忙时进行逻辑备份,否则阻碍忙时DML

    如下图,前面2行是FTWRL持有的S锁,第3行是一个update语句,IX直接被阻塞,处于pending的锁等待状态;同时由于S锁的持有时间为EXPLICIT,表明FTWRL需要一个显示的释放(unlock tables)

    延伸

    DML并不是只有IX锁,DML和select .. for update在执行中持有的锁实际是SW锁(DML需要找一个大一点的表来验证,目前只验证了select .. for update),IX只是DML初期需要获得的锁

    如下图是一个select for update语句,start transaction对应的是第2行的SR锁,而语句本身对应的是SW锁

    如果在此时执行一个FTWRL,我们会发现2个会话并不会相互阻塞(因为S锁与SR和SW都是兼容的),如下图

    但如果我们是先执行的FTWRL再执行的select for update,那么画风就不是像上图那样了

    如下图所示,在先执行FTWRL的情况下,select for update压根没有获得SW锁,而是在获取IX锁的过程中就受挫了,一直处于pending状态。(如果这个S锁不释放,那么后面的IX会一直等待,直到超时)

    S锁除了逻辑备份时的FTWRL以外,createa table as也会持有这个锁

    ③SH锁

    目前已知的是desc操作会持有这个SH锁

    SH锁与绝大部分锁都兼容,除开X锁

    也就是说在做rename一类的操作的时候,你是无法去执行desc的

    ④SR锁

    前面提到的start transaction,以及所有的非当前读都需要持有这个锁

    非当前读的意思就是快照读,也就是普通的select

    与SR锁有冲突的有2个,一个是X,另一个是SNRW

    研发有时候会很困惑的问我,“我这个表只有几十行数据,select查不出来???”  这时候就需要检查MDL锁了

    ⑤SW锁

    当前读需要持有此锁,常见的DML和select for update都对应此锁,但不包括DDL

    与SW锁有冲突的有4个,SU,SRO,SNRW,X

    ⑥SWL锁(MDL_SHARED_WRITE_LOW_PRIO)

    看到一种说法是这个锁仅对MyISAM引擎生效,冲突范围与SW锁类似

    ⑦SU锁(MDL_SHARED_UPGRADABLE)

    部分alter语句会持有该锁。该锁可能会升级成SNW,SNRW,X;而X锁也有可能逐步降级到SU锁

    SU锁和SU,SNW,SNRW,X锁互斥

    表面看起来DML的SW锁和SU锁不互斥(DML和DDL),但实际上因为SU锁存在升级的属性,SU锁会升级到SNW锁,从而和SW产生互斥

    如下图,SU并没有被SW锁阻塞,但升级到SNW之后,SNW被SW阻塞,一直处于pending状态

    SU锁的兼容性如下

    查看改过源码的例子,在执行alter的时候,SU会升级到X,之后X降级到SU,然后SU再升级到X

    SU锁的阻塞实验如下

    先SU,再SW,SW被SU阻塞

    先SW,再SU,SU并未被SW阻塞,但是SU向上升级的过程中产生的SNW被SW阻塞;于是将SW的会话commit,之后SNW向下降级成SU,并成功获得锁;

    所以虽然看起来SW和SU不是一个双向阻塞,但实际效果就是双向阻塞,无论DML和DDL谁在前面,都必然会发生相互的阻塞

    ⑧SNW锁(MDL_SHARED_NO_WRITE)

    不兼容的有点多,先贴一个兼容性

    SU升级X的过程中会升级成SNW

    SU升级成X的过程中,有一个copy的过程,这个过程就是SNW,在这个copy的过程中,允许DML但是不允许select(SR)

    copy是一个非常耗时的过程

    ⑨SRO锁(MDL_SHARED_READ_ONLY)

    lock tables read的语句会持有这个锁

    SRO阻塞SW,SNRW,X

    兼容性如图

    ⑩SNRW(MDL_SHARED_NO_READ_WRITE)

    lock tables write的语句会持有这个锁

    阻塞的锁非常多,除开SH和S以外,其他的都阻塞,连SR都阻塞了

    兼容性如下

    换句话说flush tables with read lock; (S)会堵塞lock table write; (SNRW)

    但是flush tables with read lock;(S)却不会堵塞lock table read (SRO)

    ⑪X锁(MDL_EXCLUSIVE)

    阻塞一切

    各种DDL均属于这个范畴

    create,drop,rename  (alter table add column也属于这个范畴)

    X锁案例

    SW锁阻塞X锁,(X锁是为了去执行一个drop)

    X锁阻塞SH

    thread104在做一个create table as的表复制操作,在表里面并没有发现X锁的信息,在thread95上对新表做一个desc操作,可以看到SH锁处于等待状态,然而这里阻碍SH的并不是X锁

    只有1行的select被堵住

    thread95做一个start transaction之后不提交,thread107对95的表做出一个rename操作,X锁被前面的SR锁阻塞,这时候thread108对该表发起一个limit仅仅为1的查询,但被X锁阻塞。由于lock_wait_timeout这个参数通常是1年,所以一连串查询被堵死

    关于DDL造成的SU锁、X锁和SR锁的阻塞

    alter开头的几个SQL,无论是modify还是add,查询出来都是SU锁,但DDL是一个过程,其中的有一部分如果发生了阻塞,可能会发现是X锁阻塞;拿SR阻塞X锁的实验来说,SR阻塞X的过程非常短暂,如果没有刚好卡到那个点,看到的结果可能就是SR和SU互不干涉,但如果卡到那个点,就会观测到X被SR所阻塞。具体的需要读源码,这里不展开


    解决方案

    SELECT

    locked_schema,

    locked_table,

    locked_type,

    waiting_processlist_id,

    waiting_age,

    waiting_query,

    waiting_state,

    blocking_processlist_id,

    blocking_age,

    substring_index(sql_text,"transaction_begin;" ,-1)ASblocking_query,

    sql_kill_blocking_connection

    FROM

    (

    SELECT

    b.OWNER_THREAD_IDASgranted_thread_id,

    a.OBJECT_SCHEMAASlocked_schema,

    a.OBJECT_NAMEASlocked_table,

    "Metadata Lock"ASlocked_type,

    c.PROCESSLIST_IDASwaiting_processlist_id,

    c.PROCESSLIST_TIMEASwaiting_age,

    c.PROCESSLIST_INFOASwaiting_query,

    c.PROCESSLIST_STATEASwaiting_state,

    d.PROCESSLIST_IDASblocking_processlist_id,

    d.PROCESSLIST_TIMEASblocking_age,

    d.PROCESSLIST_INFOASblocking_query,

    concat('KILL', d.PROCESSLIST_ID)ASsql_kill_blocking_connection

    FROM

    performance_schema.metadata_locks a

    JOINperformance_schema.metadata_locks bONa.OBJECT_SCHEMA=b.OBJECT_SCHEMA

    ANDa.OBJECT_NAME=b.OBJECT_NAME

    ANDa.lock_status='PENDING'

    ANDb.lock_status='GRANTED'

    ANDa.OWNER_THREAD_ID<>b.OWNER_THREAD_ID

    ANDa.lock_type='EXCLUSIVE'

    JOINperformance_schema.threads cONa.OWNER_THREAD_ID=c.THREAD_ID

    JOINperformance_schema.threads dONb.OWNER_THREAD_ID=d.THREAD_ID

    ) t1,

    (

    SELECT

    thread_id,

    group_concat(CASEWHENEVENT_NAME='statement/sql/begin'THEN"transaction_begin"ELSEsql_textENDORDERBYevent_id SEPARATOR ";" )ASsql_text

    FROM

    performance_schema.events_statements_history

    GROUPBYthread_id

    ) t2

    WHERE

    t1.granted_thread_id=t2.thread_id


    参考文章

    MDL锁处理

    MDL元数据锁

    快速处理MDL锁

    相关文章

      网友评论

        本文标题:MySQL锁<一>

        本文链接:https://www.haomeiwen.com/subject/prsbyctx.html