《MySQL 技术内幕》锁

前言

  • 开发多用户、数据库驱动的应用时,存在的最大难点
    • 一方面要最大程序地利用数据库的并发访问
    • 另一方面还要确保每个用户能以一致的方式读取和修改数据

什么是锁

  • 锁 是数据库系统区别于文件系统的一个关键特性
  • 锁机制 用于管理对共享资源的并发访问
  • 数据库系统使用锁是为了支持对共享资源进行并发访问,提供数据的完整性和一致性
  • 不同的数据库,底层实现锁的方法各不相同
    • 对于 MyISAM 引擎,其锁是表锁设计,并发读没有问题,但并发写入性能就要差一点了
    • 对于 SQL Server 数据库
      • 2005 版本之前都是 页锁,并发性能有所提高,但对热点数据页的并发问题依然无能为力
      • 2005 版本开始支持 乐观并发(支持行锁)和悲观并发,锁开销较大,同时存在锁升级(行锁 -> 表锁
    • InnoDB 存储引擎的实现 与 Oracle 类似,提供一致性的非锁定读、行级锁支持。
      • 行级锁没有相关额外的开销,并可以同时得到并发性和一致性

lock 与 latch

  • latch 一般称为 闩锁(轻量级的锁),因为其要求锁定的时间必须非常短
    • latch 可以分为 mutex(互斥量)和 rwlock(读写锁)
    • 目的:保证并发线程操作临届资源的正确性
    • 通常没有死锁检测的机制
  • lock 的对象是事务,用来锁定的是数据库中的对象,如表、页、行
    • 一般 lock 的对象仅在事务 commit 或 rollback 后进行释放(不同事务隔离级别可能不同
    • 拥有死锁机制
  • lock 与 latch 的比较
    image-20190224161639785

InnoDB 存储引擎中的锁

  1. 锁的类型
    • InnoDB 存储引擎实现了两种标准的行级锁
      • 共享锁(S Lock),允许事务读一行数据
      • 排他锁(X Lock),允许事务删除或更新一行数据
    • 排他锁和共享锁的兼容性
      image-20190224161710397
      • S 和 X 锁都是行锁,兼容是指对同一记录(row)锁的兼容性情况
    • InnoDB 存储引擎支持多粒度(granular)锁定,允许事务在行级上的锁和表级的锁同时存在
      • 意向锁是将锁定的对象分为多个层次,意向锁意味着事务希望在更细粒度上进行加锁
      • 层次结构
        image-20190224161745243
        • 如果需要对页上的 记录r 进行上 X 锁,那么首先需要分别对数据库A、表、页上意向锁 IX
    • InnoDB 存储引擎支持意向锁设计比较简练,其意向锁即为表级别的锁
      • 设计目的主要是为了在一个事务中揭示下一行将被请求的锁类型
      • 其支持的两种意向锁
        • 意向共享锁(IS Lock),事务想要获得一张表中某几行的共享锁
        • 意向排他锁(IX Lock),事务想要获得一张表中某几行的排他锁
      • InnoDB 存储引擎中锁的兼容性
        image-20190224161758792
      • 查看当前数据库的锁请求和判断事务锁的情况
        • InnoDB 1.0 之前,通过 SHOW FULL PROCCESSLIST , SHOW ENGINE INNODB STATUS
        • InnoDB 1.0 开始,可以通过 INFORMATION_SCHEMA 架构下的表查看当前事务和锁情况
        • 表 INNODB_TRX 的结构
          image-20190224161825646
          • 该表只是显示了当前允许的 InnoDB 事务,并不能直接判断锁的一些情况
        • 表 INNODB_LOCKS 的结构
          image-20190224161838490
          • lock_data 并不是一个 “可信” 的值
            • 范围查找时,lock_data 可能只返回第一行的主键值
            • 如果当前资源被锁住,但因为缓冲池容量问题被刷出,lock_data 显示为 NULL
              • 即 InnoDB 不会从磁盘进行再一次查找
        • 表 INNODB_LOCK_WAITS 的结构
          image-20190224161856025
          • 可以清楚直观地看到哪个事务阻塞了另一个事务
        • 可以联合查询以上三张表得到更为直观的详细信息
          image-20190223120002520
  2. 一致性非锁定读
    • 一致性非锁定读 是指 InnoDB 通过 行多版本控制 的方法来读取当前执行时间数据库中行的数据
      image-20190224163131853
      • 非锁定读 是指不需要等待访问的行上的 X 锁的释放,而是直接读取行的一个快照数据
    • 快照数据是指该行的之前版本的数据,该实现是通过 undo 段来完成
      • undo 用来在事务中回滚数据,因为快照数据本身是没有额外的开销
      • 读取快照数据是不需要上锁的,因为没有事务需要对历史的数据进行修改操作
    • 非锁定读机制极大地提高了数据库的并发性
      • InnoDB 存储引擎的默认设置下,这是默认读取方式,即读取不会占用和等待表上的锁
    • 一个行记录可能不止一个快照数据,一般称这种技术为 行多版本技术
      • 由此带来的并发控制,称之为 多版本并发控制(MVCC
        • MVCC : Multi Version Concurrency Control
    • 在事务隔离级别 READ COMMITTEDREPEATABLE READ(默认),InnoDB 使用 非锁定的一致性读
      • 两种隔离级别对应快照数据的定义却各不相同
        • READ COMMITTED 读取行的最新一份快照数据
        • REPEATABLE READ 读取事务开始时的行数据版本
      • 对于READ COMMITTED 而言,从数据库理论来看,其违反了事务 ACID 中 I(隔离性)的特性
  3. 一致性锁定读
    • InnoDB 存储引擎对于 SELECT 语句支持两种一致性的锁定读(locking read)操作
      • SELECT … FOR UPDATE
        • 对行记录加一个 X 锁
      • SELECT … LOCK IN SHARE MODE
        • 对行记录加一个 S 锁
    • 对于 SELECT 加锁读 必须在一起事务中,当事务提交了,锁也就释放了
  4. 自增长与锁
    • 自增长在数据库中时非常常见的一种数据,也是很多 DBA 或 开发人员首先的主键方式
    • 在 InnoDB 内存结构中,对每一个含有自增长值的表都有一个自增长计算器(auto-increment counter)
      • 得到计数器的值:SELECT MAX(auto_inc_col) FROM t FOR UPDATE
    • 插入操作会依据这个自增长的计数器值加 1 赋予自增长列
      • 这个实现方式称作 AUTO-INC Locking
      • 这种锁其实是采用的一种特殊的表锁机制,为了提高插入性能,能够在完成自增长值插入后立即释放锁
        • 不需要等待当前事务完成
        • 并发插入的效率较差,事务必须等待前一个事务中所有的自增长值插入的完成(不用等待事务完成
    • 从 MySQL 5.1.22 版本开始,InnoDB 提供了一种轻量级互斥量的自增长实现机制,提高插入性能
      • 参数innodb_autoinc_lock_mode配置控制自增长的模式(默认为 1
      • 插入类型
        image-20190224163221017
      • 参数 innodb_autoinc_lock_mode的说明
        image-20190224163251323
    • MyISAM 存储引擎是表锁设计,自增长不用考虑并发插入的问题
    • 在 InnoDB 存储引擎中,自增长的列必须是索引,同时必须是索引的第一列(如不是,MySQL 抛出异常
      • MyISAM 存储引擎没有此限制
  5. 外键和锁
    • 外键主要用于引用完整性的约束检查
    • 在 InnoDB 存储引擎中对于一个外键列,若没有显示创建,则 InnoDB 会自动对其创建一个索引
      • 目的:避免表锁
    • 对于外键值的插入或者更新,需要先 加锁 SELECT 父表,避免发生数据不一致的情况
      • SELECT ... LOCK IN SHARE MODE 主动对父表加一个 S 锁

锁的算法

  1. 行锁的 3 种算法

    • InnoDB 存储引擎有 3 种行锁的算法
      • Record Lock:单个行记录上的锁
      • Gap Lock:间隙锁,锁定一个范围,但不包含记录本身
      • Next-Key Lock:Gap Lock + Record Lock,锁定一个范围,并且锁定记录本身
    • Record Lock 总是会去锁住索引记录,如表在建立时没有设置任何一个索引,会使用隐式的主键进行锁定
    • 采用 Next-Key Lock 的锁定技术称为 Next-Key Locking
      • 目的:解决幻读问题(Phantom Problem)
      • 锁定的不是单个值,而是一个范围,是谓词锁(predict lock)的一种改进
      • 同时还存在 previous-key locking技术
        • 栗子: Next-Key Locking 锁定 (10,20] , previous-key locking 锁定 [10,20)
    • 对于查询所有的唯一索引列时,锁定由 Next-Key Lock 算法降级为 Record Lock,提高应用并发性
      • 若唯一索引由多个列组成,而查询仅查找多个唯一索引列中的其中几个,则依然使用 Next-Key Lock算法
    • 针对查询 辅助索引 时的栗子
      image-20190223163746982
      • 因为是对辅助索引 b 进行查询,因此索引 Next-Key Locking 技术加锁,由于有2个索引,需分别进行锁定
        • 对于聚集索引,其仅对列 a 等于 5 的索引加上 Record Lock
        • 对于辅助索引
          • 首先加上 Next-Key Lock:锁定范围(1,3]
          • 同时对辅助索引的下一个键值加上 gap lock:锁定范围(3,6)
    • 关闭 Gap Lock 及其带来的问题
      • 通过以下两种方式来显示地关闭 Gap Lock
        • 将事务的隔离级别设置为 READ COMMITTED
        • 将参数 innodb_locks_unsafe_for_binlog 设置为 1
      • 在上述配置下,除了外键约束和唯一性检查依然需要 Gap Lock,其余情况仅使用 Record Lock 锁定
      • 上述设置破坏了事务的隔离性,并且对于 Replication,可能会导致主从数据不一致
  2. 解决 Phantom Problem

    • Phantom Problem 是指在同一事务下,连续执行两次同样的 SQL 语句可能导致不同的结果

      第二次的 SQL 语句可能会返回之前不存在的行

    • REPEATABLE READ下,InnoDB 采用Next-Key Locking 机制来避免幻读问题

      • 这地不同于其他数据库,如 Oracle 只能在 SERIALIZABLE的事务隔离级别下才能解决
    • Phantom Problem 的演示

    image-20190224163341442image-20190224163424056

    • SELECT … FOR UPDATE , 对(2,+∞)这个范围加了 X 锁,从而避免 幻读问题

    • 通过Next-Key Locking实现应用程序的唯一性检查

    image-20190224163447559

    • 当有多个事务并发操作,为了保证唯一性检查机制,通过死锁机制,保证只有一个事务的插入操作成功

锁问题

  • 通过锁定机制可以实现事务的隔离性要求,使得事务可以并发地工作,但同时也带来了潜在的问题
  1. 脏读
    • 脏页与脏数据
      • 对于脏页的读取是非常正常的,其是因为内存和磁盘的异步造成的
        • 不影响数据的一致性(两者最终会达到一致性,即当脏页都刷回到磁盘
        • 脏页的刷新是异步的,不影响数据库的可用性
      • 脏数据是指某个未提交的数据(即一个事务可以读到另一个事务未提交的数据
        • 这违反了数据库的隔离性
    • 脏读指的是在不同的事务下,当前事务可以读到另外事务未提交的数据(简单来说就是读到脏数据
    • 脏读的示例
      image-20190224163512168
    • 脏读发生的条件是需要事务隔离级别为 READ UNCOMMITTED,目前大部分数据库都至少设置为 READ COMMITTED
  2. 不可重复读
    • 在一个事务内两次读取到的数据是不一样的情况,这种情况称为不可重复读
    • 不可重复读和脏读的区别
      • 脏读是读到未提交的数据
      • 不可重复读读到的却是已经提交的数据,但其违反了数据库事务一致性的要求
    • 不可重复读的示例
      image-20190224163531181
    • Oracle、SQL Server 等数据库的事务默认隔离级别都是 READ COMMITTED
    • InnoDB 存储引擎默认事务隔离级别是 READ REPEATABLE,采用 Next-Key Lock 算法,避免了不可重复读
  3. 丢失更新
    • 丢失更新时另一个锁导致的问题,即一个事务的更新操作会被另外一个事务的更新操作所覆盖,导致数据不一致
    • 在当前数据库的任何隔离级别下,都不会导致数据库理论意义上的丢失更新问题
      • 对于行的 DML 操作,都需要对行或者其他粗粒度级别的对象加 X 锁
    • 虽然数据库能阻止丢失更新问题的产生,但应用中还有另一个逻辑意义的丢失更细问题
      image-20190223203747341
      • 显示,整个过程用户 User1 的修改更新操作 “丢失”,导致丢失更新
      • 避免的方法的是让事务在这种情况下的操作变成串休化,而不是并行的操作
        • 例如:可以使用 SELECT ... FROM update 读取记录的时候 加上一个 X 锁(也可以使用加 S 锁

阻塞

  • 因不同锁之间的兼容性关系,在有些时刻一个事务中的锁需要等待另一个事务中的锁释放它所占的资源,这就是阻塞

    • 阻塞并不是一件坏事,其是为了确保事务可以并发且正常地允许
  • 参数innodb_lock_wait_timeout配置等待的时间

    • 默认是 50秒
    • 动态参数,可以在数据库运行时调整
  • 参数innodb_rollback_on_timeout配置是否在等待超时时对进行中的事务进行回滚操作

    • 默认是 OFF,即不回滚

    牢记:在默认情况下 InnoDB 存储引擎不会回滚超时引发的错误异常

死锁

  1. 死锁的概念

    • 死锁是指两个或两个以上的事务在执行过程中,因争夺锁资源而造成的一种互相等待的现象
    • 解决死锁
      • 最简单的一种方法是超时
        • 当两个事务互相等待时,当一个等待时间超过设置的阈值时,其中一个事务回滚,另一个继续进行
          • 根据 FIFO 的顺序选择回滚的对象
        • 参数innodb_lock_wait_timeout配置超时的时间
        • 缺点:若超时的事务所占权重较大或已经更新了很多行,占用了较多 undo log,回滚代价过大
          • 回滚这个事务的时间相对另一个事务所占用的时间可能会很多
      • 当前数据库还都普遍采用 wait-for graph(等待图)的方式来进行死锁检测
        • 较之超时解决方案,这是一种更为主动的死锁检测方式,InnoDB 存储引擎也是采用这种方式
        • 等待图 要求数据库保存以下两种信息
          • 锁的信息链表
          • 事务等待链表
        • 示例事务状态和锁的信息
          image-20190224163614114
          • 事务 T1 指向 T2 边的定义为
            • 事务 T1 等待事务 T2 所占用的资源
            • 事务 T1 最终等待 T2 所占用的资源,也就是事务之间存在等待相同的资源,而 T1 发生在 T2 前
        • 通过上述链表可以构造出一张图,而在这个图中若存在回路,就代表存在死锁,即资源间相互发生等待
          image-20190224163630152
          • 在每个事务请求锁并发生等待时都会判断是否存在回路,若存在则有死锁
          • 通常来说 InnoDB 存储引擎选择回滚 undo 量最小的事务
        • 等待图 的死锁检测通常采用深度优先的算法实现
          • InnoDB 1.2 版本之前,采用递归方式实现,1.2版本开始,改为非递归方式实现,进一步提高性能
  2. 死锁概率

    • 死锁应该非常少发生,若经常发生,则系统是不可用的

    • 从整个系统来看,任何一个事务发生死锁的概率为:

    • n 2 r 4 4 R 2 系统中任何一个事务发生死锁的概率 ≈ \frac{n^2r^4}{4R^2}

    • 事务发生死锁的概率与以下几点因素有关

      • 系统中事务的数量( n ),数量越多发生死锁的概率越大
      • 每个事务操作的数量( r ),每个事务操作的数量越多,发生死锁的概率越大
      • 操作数据的集合( R ),越小则发生死锁的概率越大
  3. 死锁的示例

    • 死锁只存在于并发的情况,而数据库本身就是一个并发运行的程序
    • 死锁用例1
      • AB - BA 死锁
        image-20190224163658820
        • 死锁的原因是会话 A 和 B 的资源在互相等待
        • 发送死锁后,InnoDB 存储引擎会马上回滚一个事务
          • 因此应用程序中捕获 1213 这个错误,其实并不需要对其进行回滚
      • Oracle 数据库中产生死锁的常见原因是没有对外键添加索引,而 InnoDB 存储引擎会自动对其进行添加
    • 死锁用例2
      • 当前事务持有了待插入记录的下一个记录的 X 锁,但是在等待队列中存在一个 S 锁的请求,则可能会死锁
        image-20190224163725331

锁升级

  • 锁升级(Lock Escalation)是指将当前锁的粒度降低
    • 栗子:将一个表的 1000 个行锁升级为一个页锁,或者将页锁升级为表锁
      • ps:这句话微微有点瑕疵,万一这 1000个行不在同一个 页呢?
  • SQL Server 数据库在适合的时候回自动地将行、键或分页锁升级为更粗粒度的表级锁
    • 这种升级保护了系统资源,防止系统使用太多的内存来维护锁,在一定程度上提高了效率
    • SQL Server 2005 版本支持行锁之后,依然可能发生锁升级
    • 由一句单独的 SQL 语句在一个对象上持有的锁的数量超过了阈值(默认为 5000
    • 锁资源占用的内存超过了激活内存的 40% 时就会发生锁升级
    • 锁升级带来的另一个问题就是因为锁粒度的降低而导致并发性能降低
  • InnoDB 存储引擎不存在锁升级的问题
    • 其根据每个事务访问的每个页对锁进行管理,采用的是位图的方式,因此锁住一行或多行的开销通常是一致的
发布了98 篇原创文章 · 获赞 197 · 访问量 6万+

猜你喜欢

转载自blog.csdn.net/YangDongChuan1995/article/details/87909902