Mysql 锁总结

459 阅读8分钟

前言

通常情况下,当访问某张表或者记录的时候,操作者必须先获取锁。

需要写锁还是读锁取决于执行的操作,获取写锁的优先级高于读锁。

但是当读取者已经拿到读锁在查询时,此时有写操作请求获取写锁,由于查询开始后不能中断,因此允许读取者完成操作,此时写锁请求需要等待。

锁类型

  • 共享锁(读锁,S锁)
    • 会阻塞其他事务修改表数据
    • 共享锁,允许读取者同时获取相同的锁,在读取过程中不允许其他请求获得写锁
  • 排他锁(写锁,X锁)
    • 会阻塞其他事务读和写
    • 独占的排他锁,为了避免同时写入导致数据错乱,或者读取到变化中的数据,在某一个写入操作获取到写锁后会阻塞其他请求获取读锁或者写锁
  • 意向锁(Intention Locks)
    • 意向锁的作用是为了解决表锁和行锁共存时,快速判断是否能获取到锁
    • 假设事务A锁定了表中的一行,事务B想获取表锁,为了避免事务B表锁和事务A行锁冲突,数据库要通过以下两步进行判断
    • step1:判断表是否已被其他事务用表锁锁表, step2:判断表中的每一行是否已被行锁锁住
    • step2通过遍历判断,效率太低所以就有了意向锁
    • 分为意向共享锁(IS)和意向排他锁(IX)
    • innodb事务在获取某行的共享锁或者排它锁之前,会先尝试获取IS或者IX,目的是加速判断是否可以获取到锁,即可以先进行一个初步判断
    • 意向锁是表级锁
    • 兼容性汇总如下
X IX S IS
X 冲突 冲突 冲突 冲突
IX 冲突 兼容 冲突 兼容
S 冲突 冲突 兼容 兼容
IS 冲突 兼容 兼容 兼容
  • 记录锁(Record Locks)
    • 记录锁是索引记录上的锁,如果一个表没有定义索引,那么就会去锁定隐式的“聚集索引”
  • 间隙锁(Gap Locks)
    • 间隙锁是一个在索引记录之间的间隙上的锁,一个间隙可能跨越单个索引值、多个索引值,甚至为空。
    • 当我们用范围条件而不是相等条件检索数据,并请求共享或排他锁时,InnoDB会给符合条件的已有数据的索引项加锁;对于键值在条件范围内但并不存在的记录,叫做“间隙(GAP)”,InnoDB也会对这个“间隙”加锁,这种锁机制就是所谓的间隙锁
    • 目的是防止幻读和满足其恢复和复制的需要
  • 临键锁(Next-key Locks)
    • Next-Key Locks是行锁与间隙锁的组合。
    • 当InnoDB扫描索引记录的时候,会首先对选中的索引记录加上记录锁(Record Lock),然后再对索引记录两边的间隙加上间隙锁(Gap Lock)。
  • 插入意向锁(Insert Intention Locks)
    • 插入意向锁是在数据行插入之前通过插入操作设置的间隙锁定类型。
    • 如果多个事务插入到相同的索引间隙中,如果它们不在间隙中的相同位置插入,则无需等待其他事务。例如:在4和7的索引间隙之间两个事务分别插入5和6,则两个事务不会发冲突阻塞。
  • 自增锁
    • 自增锁是事务插入到有自增列的表中而获得的一种特殊的表级锁。如果一个事务正在向表中插入值,那么任何其他事务都必须等待,保证第一个事务插入的行是连续的自增值。

锁粒度

  • 表级
    • 优点
      • 实现简单,开销小,加锁快,不会出现死锁
    • 缺点
      • 锁定粒度大,发生锁冲突的概率最高,并发度最低
  • 页级(MySQL特有)
    • 开销和加锁时间界于表锁和行锁之间;
    • 会出现死锁;
    • 锁定粒度界于表锁和行锁之间,并发度一般
  • 行锁
    • 优点
      • 锁定粒度最小,发生锁冲突的概率最低,并发度也最高
    • 缺点
      • 实现复杂,开销大,加锁慢,会出现死锁

锁的实现方式

  • InnoDB行锁是通过给索引加锁来实现的,如果不使用索引查询,InnoDB会通过隐藏的聚簇索引来对记录进行加锁(全表扫描,也就是表锁)。
  • 但是,为了效率考量,MySQL做了优化,对于不满足条件的记录,会放锁,最终持有的,是满足条件的记录上的锁。但是不满足条件的记录上的加锁/放锁动作是不会省略的。所以在没有索引时,不满足条件的数据行会有加锁又放锁的耗时过程。
  • 索引分为主键索引和非主键索引两种。如果一条sql语句操作了主键索引,MySQL就会锁定对应主键索引;如果一条语句操作了非主键索引,MySQL会先锁定非主键索引,再锁定对应的主键索引。

INNODB行锁注意事项

  • 由于InnoDB行锁是针对索引加的锁,不是针对记录加的锁,所以即使是访问不同行的记录,如果使用了相同的索引键,也是会出现锁冲突的
  • 即便条件中使用了索引字段,如果MYSQL优化器认为全表扫描效率更高,那么他会使用表锁,放弃行锁

死锁

  • 什么是死锁

    • 当两个事务分别锁定了两个单独的对象,这时每一个事务都要求在另一个事务锁定的对象上获得一个锁,因此每一个事务都必须等待另一个事务释放占有的锁。这就发生了死锁。
  • 解决办法

    • 理论上预防死锁的发生就是要破坏产生死锁的条件
    • 一次封锁法
      • 一次封锁法要求每个事务必须一次将所有要使用的数据全部加锁,否则就不能继续执行
      • 存在的问题
        • 一次将以后要用到的全部数据加锁,加大封锁范围,降低系统的并发度
        • 数据库中数据是不断变化的,原来不要求封锁的数据,在执行过程中可能会变成封锁对象,所以很难事先精确确定每个事务要封锁的数据对象,为此只能扩大封锁范围,将事务在执行过程中可能要封锁的数据对象全部加锁,这就更降低了并发度
    • 顺序封锁法
      • 预先对数据对象规定一个封锁顺序,所有事务都按这个顺序实行封锁。
      • 如:在B树结构的索引中,规定封锁的顺序必须从根结点开始,然后是下一级的子结点,逐级封锁
  • 死锁的诊断与解除

    • 诊所办法
      • 超时法
        • 指的是如果一个事务的等待时间超过了规定的时限,就认为发生死锁
        • 缺点
          • 有可能误判死锁,事务因为其他原因使等待时机超过时限
          • 时限若设置得太长,死锁发生后不能及时发现
      • 等待图法
        • 指的是用事务等待图动态反应所有事务的等待情况
          • 事务等待图是一个有向图G=(T,U),其中T为结点的集合,每个结点表示正在运行的事务。U为边的集合,每条边表示事务等待的情况。若T1等待T2,则T1、T2之间划一条有向边,从T1指向T2。事务等待图动态地反映了所有事务的等待情况。并发控制子系统周期性地检测事务等待图,如果发现图中存在回路,则表示系统中出现了死锁
    • 解除办法
      • 通常是选择一个处理死锁代价最小的事务,将其撤销,释放此事务持有的所有的锁,使其他事务能继续运行下去。(而且要对撤销的事务所执行的数据修改操作进行恢复)
    • 应用
      • 选择一个合理的超时时间来自动释放死锁
  • 应用中尽量减少死锁的方法

    • 对同样的表进行操作时,尽量使用相同的访问顺序
      • 如果不同的程序会并发存取多个表,应尽量约定以相同的顺序为访问表,这样可以大大降低产生死锁的机会。
      • 在程序以批量方式处理数据的时候,如果事先对数据排序,保证每个线程按固定的顺序来处理记录,也可以大大降低死锁的可
    • 在用一个事务中,尽可能依次锁定需要的资源
      • 在事务中,如果要更新记录,应该直接申请足够级别的锁,即排他锁,而不应该先申请共享锁,更新时再申请排他锁
    • 在业务允许的前提下,使用较低的隔离级别
      • 在REPEATEABLE-READ隔离级别下,如果两个线程同时对相同条件记录用SELECT...ROR UPDATE加排他锁,在没有符合该记录情况下,两个线程都会加锁成功。程序发现记录尚不存在,就试图插入一条新记录,如果两个线程都这么做,就会出现死锁。这种情况下,将隔离级别改成READ COMMITTED,就可以避免问题
    • 如果业务允许,并且又非常容易发生死锁,可以尝试升级锁粒度,改为表锁来避免死锁

参考资料

深入了解mysql--gap locks,Next-Key Locks

深究mysql锁