美文网首页
InnoDB锁与死锁

InnoDB锁与死锁

作者: 轻松的鱼 | 来源:发表于2020-06-29 12:08 被阅读0次

    InnoDB 有哪些锁?其表现和意义是什么?增删改查分别加什么锁?死锁是怎么产生的?怎么分析和避免?本文对这些问题做了些整理,没有写的很细,适合当大纲或者总结来看。

    1. IoonDB锁机制

    1.1 锁生存周期

    2PL(Two-Phase Locking)
    二阶段锁,说的是锁操作分为两个阶段:加锁阶段与解锁阶段,并且保证加锁阶段与解锁阶段不相交。

    • 加锁:实际访问到某个待更新的行时,对其加锁(而非一开始就将所有的锁都一次性持有);
    • 解锁:事务提交/回滚时(而非语句结束时就释放)。
    1.2 锁模式

    InnoDB实现标准的行级锁,其中有两种模式的锁:

    • 共享锁,LOCK_S
    • 排他锁,LOCK_X

    通常有一种含糊的说法:对行加S锁,其他事务只能对该行读,不能修改;对行加X锁,其他事务对该行不可读、写。其实这是不准确的,且不说InnoDB中的“读”为快照读不加锁,其关系也完全颠倒混乱了。应该以这样的顺序来理解:

    1. 事务T1执行 select * from t1 where id=1 lock in share mode,会先获取id=1行的共享(S)锁,才允许读取id=1这一行的数据;
    2. 事务T2同样执行 select * from t1 where id=1 lock in share mode,也要先申请获取id=1行的S锁,因为S锁之间兼容,可以成功获取锁,得以查询数据;
    3. 事务T3执行 update t1 set a=99 where id=1,要先申请id=1行的X锁,因为X锁与S锁互斥,所以获取不成功,需要等待,则更新也就无法执行。

    所以对某行加S锁,只能说该事务可以读取这一行;对某行加X锁,只能说该事务可以读取、修改这一行。而不能直接说不允许其他事务读或者修改这一行。

    1.3 锁属性

    InnoDB实现的是行级锁没错,但是一细分,其实还有其他属性的锁,下面一一介绍。

    • 记录锁(Record Locks)
      记录锁定是对索引记录的锁定。请记住,对象是索引上的记录。

    • 间隙锁(Gap Locks)
      间隙锁定是对索引记录之间的间隙的锁定。REPEATABLE-READ隔离级别就是通过间隙锁解决幻读的:T1锁定间隙,其他事务就无法在间隙中插入数据,这样T1下一次读到的数据不会变多。

      实际上除了REPEATABLE-READ隔离级别,在 READ-COMMITTED 隔离级别,也会存在 gap lock ,只发生在:唯一约束检查到有唯一冲突的时候,会加 S Next-key Lock,即对记录以及与和上一条记录之间的间隙加共享锁。

    • Next-Key Locks
      Next-Key Locks是索引记录上的记录锁和上一条记录之间的间隙上的间隙锁的组合。

    • 意向锁(Intention Locks)
      意向锁是一种表锁,分两种:

      1. 意向共享锁(intention shared lock, IS):事务有意向对表中的某些行加共享锁(S锁)。事务获取某些行的 S 锁前,必须先获得表的 IS 锁。
        SELECT column FROM table ... LOCK IN SHARE MODE;
      2. 意向排他锁(intention exclusive lock, IX):事务有意向对表中的某些行加排他锁(X锁)。事务获取某些行的 X 锁前,先获得表的 IX 锁。
        SELECT column FROM table ... FOR UPDATE;

      它是为了允许行锁和 server 层表级锁并存,并且高效而存在的。在执行 lock table t read 时,想要获取表锁,必须保证:

      1. 当前没有其他事务持有t表的表级排他锁;
      2. 当前没有其他事务持有t表中任意一行的排他锁。

      为了检测是否满足第二个条件,T2必须在确保t表不存在任何排他锁的前提下,去检测表中的每一行是否存在排他锁。很明显这是一个效率很差的做法。有意向锁后,只需要检测t表是否存在排他意向锁即可。

      最后一定要记住意向锁的特性:

      • 意向锁是为了行锁阻塞表级锁存在的;
      • 意向锁不会与行级的共享/排他锁互斥;
      • 意向锁不会与意向锁互斥。
    • 插入意向锁(Insert Intention Locks)
      执行insert时,在数据插入前需要对插入的间隙加的一种间隙锁称为插入意向锁。虽然也是一种间隙锁,但是属性特殊:

      • 它不会阻塞其他任何锁;
      • 它本身仅会被 gap lock 阻塞。

      只有在它被阻塞的时候才能观察到,所以常常会被忽略。

    • 自增锁(AUTO-INC Locks)
      自增锁是一种特殊的表级锁,在向具有 auto_increment 字段属性的表中插入数据时,因为要获取字段的自增值而获取的锁。由 innodb_autoinc_lock_mode 参数控制加锁行为:

      • 0,代表传统模式,也就是说,在对有自增属性的字段插入记录时,会持续持有一个表级别的自增锁,直到语句执行结束为止。
      • 1,默认值,普通 insert 能够确定行数的,可能一次性获取到需要的自增值,自增锁在申请之后马上释放。类似 insert ...select 这样不确定插入的行数时,需要等语句执行完才释放自增锁;
      • 2,建议使用,自增值即取即用,可以并发获取,但是会不连续。所有 insert 类型都是申请后就释放锁(需要 binlog_format=row,才能保证主从数据一致)。
    1.4 锁组合

    上面详细介绍了锁模式和锁属性,这两者是可以组合的,比如:记录锁+LOCK_S、记录锁+LOCK_X

    1.5 锁冲突矩阵
    要分析死锁,一定要清楚锁与锁之间的冲突关系: 锁冲突矩阵
    1.6 SQL与加锁

    有了前面的认识,接下来要掌握的就是在InnoDB中各种SQL操作需要加的锁。先记住一个概念:同一个SQL,在不同的索引、不同的隔离级别下加的锁是不同的。下面我们分别介绍在RC隔离级别下增、删、改、查4种SQL操作的加锁行为。

    • select
      快照读:不加锁(普通的 select * from t where id=1);
      当前读:对扫描的行记录加相应的锁(显示加锁的读 select * from t where id=1 for update,修改数据也属于当前读)。

    • delete
      对满足条件的所有记录加排他锁:LOCK_X + LOCK_REC_NOT_GAP

    • insert
      无unique key时:LOCK_INSERT_INTENTION + LOCK_X + LOCK_REC_NOT_GAP
      有unique key时:

      1. 先进行唯一性约束检查,如果发生冲突,会加 S Next-Key Lock,否则不加;
      2. 如果没有冲突,接下来向插入间隙加插入意向锁 LOCK_INSERT_INTENTION,如果该间隙已经存在Gap Lock,会被阻塞;
      3. 如果没被Gap Lock阻塞,数据插入成功,最后加 LOCK_X + LOCK_REC_NOT_GAP
    • update
      update可以看作delete+insert的组合:

      1. Step 1:定位到下一条满足查询条件的记录(查询过程)
      2. Step 2:删除当前定位到的记录(标记为删除状态)
      3. Step 3:拼装更新后项,根据更新后项定位到新的插入位置
      4. Step 4:在新的插入位置,判断是否存在 Unique 冲突(存在Unique Key时)
      5. Step 5:插入更新后项(不存在Unique冲突时)
      6. Step 6:重复Step 1到Step 5的操作,直至扫描完整个查询范围

    2. 阅读死锁日志

    MySQL默认是开启死锁检测的,一旦发生死锁,InnoDB会回滚其中一个事务,将锁解放。默认 show engine innodb status 会记录上一次的死锁日志,也可以设置innodb_print_all_deadlocks 将每一次死锁的日志记录到error log中。

    死锁日志中列出了死锁发生的时间,以及导致死锁的事务信息(只显示两个事务,如果由多个事务导致的死锁也只显示两个),并显示出每个事务正在执行的 SQL 语句(事务执行多个SQL,只会记录正在执行的那个)、等待的锁以及持有的锁信息等。死锁日志的局限性:

    1. 不显示事务1已经持有了什么锁;
    2. 不显示事务所有的SQL,也就没法推测事务1已经持有了什么锁。

    3. 得到完整的事务

    分析死锁原因的第2步就是联系开发获取事务1、事务2的全部SQL,然后写出每个SQL加的锁,构造可能死锁的执行顺序,然后进行复现。每个事务中SQL的执行顺序是固定的,但是2个事务并发执行,就会有多种顺序组合,并不是都会触发死锁。举个例子:

    1. 同样2个事务,按以下顺序执行会死锁:
    2. 按以下顺序执行却不会死锁:

    2个事务可以有n*m种顺序组合(n、m表示事务中的SQL数量),很难一一列举出来进行分析,所以我们需要记住一些死锁的常见原因:

    • 事务以相反的顺序操作相同的数据;
    • 事务以不同索引的过滤条件,来操作相同的记录;
    • 存在 Unique key 的表中,insert 时容易出现由于唯一性约束检查而产生的 gap lock,导致死锁概率的增加。

    4. 死锁案例

    1. 事务以相反的顺序操作相同的数据
    2. 事务以不同索引的过滤条件,来操作相同的记录
    3. 唯一键冲突,插入相同数据

    出了开发层面避免上面这些常见的死锁逻辑,数据库层面可以设置隔离级别为READ-COMMITTED,减少Gap Lock 产生的死锁。

    相关文章

      网友评论

          本文标题:InnoDB锁与死锁

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