mysql技术内幕(六)锁

#锁

6.1 什么是锁

  锁是数据库系统区别于文件系统的一个关键特性。锁机制用于管理对共享资源的并发访问飞InnoDB 存储引擎会在行级别上对表数据上锁,这固然不错。不过InnoDB 存储引擎也会在数据库内部其他多个地方使用锁,从而允许对多种不同资源提供并发访问。例如,操作缓冲池中的LRU 列表,删除、添加、移动LRU 列表中的元素,为了保证一致性,必须有锁的介入。数据库系统使用锁是为了支持对共享资源进行并发访问,提供数据的完整性和一致性。
  另一点需要理解的是,虽然现在数据库系统做得越来越类似,但是有多少种数据库,就可能有多少种锁的实现方法。在SQL 语法层面,因为SQL 标准的存在,要熟悉多个关系数据库系统并不是一件难事。
  InnoDB 存储引擎锁的实现和Oracle 数据库非常类似,提供一致性的非锁定读、行级锁支持。行级锁没有相关额外的开销,并可以同时得到并发性和一致性

6.2 lock 与latch

  这里还要区分锁中容易令人混淆的概念lock 与latch。在数据库中, lock 与latch 都可以被称为"锁"。但是两者有着截然不同的含义,本章主要关注的是lock。latch 一般称为问锁(轻量级的锁),因为其要求锁定的时间必须非常短。若持续的时间长,则应用的性能会非常差。在InnoDB 存储引擎中, latch 又可以分为mutex (互斥量〉和rwlock (读写锁〉。其目的是用来保证并发线程操作临界资源的正确性,并且通常没有死锁检测的机制。lock 的对象是事务,用来锁定的是数据库中的对象,如表、页、行。并且一般lock的对象仅在事务comrnit 或rollback 后进行释放〈不同事务隔离级别释放的时间可能不同〉。此外, lock, 正如在大多数数据库中一样, 是有死锁机制的。
在这里插入图片描述

6.3 InnoDB 存储引擎中的锁

6.3.1 锁的类型

  InnoDB 存储引擎实现了如下两种标准的行级锁:

  • 共享锁(S Lock) ,允许事务读一行数据。
  • 排他锁(X Lock) ,允许事务删除或更新一行数据。

  如果一个事务T1已经获得了行r 的共享锁,那么另外的事务T2 可以立即获得行r的共享锁,因为读取并没有改变行r 的数据,称这种情况为锁兼容(Lock Compatible) 。但若有其他的事务T3 想获得行r 的排他锁,则其必须等待事务Tl、T2 释放行r 上的共享锁-一这种情况称为锁不兼容。
在这里插入图片描述
  从表可以发现X 锁与任何的锁都不兼容,而S 锁仅和S 锁兼容。需要特别注意的是, S 和X 锁都是行锁,兼容是指对同一记录(row) 锁的兼容性情况。此外, InnoDB 存储引擎支持多粒度(granular) 锁定,这种锁定允许事务在行级上的锁和表级上的锁同时存在。为了支持在不同粒度上进行加锁操作, InnoDB 存储引擎支持一种额外的锁方式,称之为意向锁(Intention Lock) 。意向锁是将锁定的对象分为多个层次,意向锁意味着事务希望在更细位度〈fine granularity )上进行加锁,如图6-3 所示
在这里插入图片描述
   若将上锁的对象看成一棵树,那么对最下层的对象上锁,也就是对最细粒度的对象进行上锁,那么首先需要对粗粒度的对象上锁。例如图6-3 ,如果需要对页上的记录r 进行上X 锁,那么分别需要对数据库A、表、页上意向锁 IX ,最后对记录r 上X 锁。若其中任何一个部分导致等待,那么该操作需要等待粗粒度锁的完成。举例来说,在对记录r 加X 锁之前,已经有事务对表l 进行了S 表锁,那么表1 上已存在S 锁,之后事务需要对记录r 在表l 上加上IX ,由于不兼容,所以该事务需要等待表锁操作的完成。
  InnoDB 存储引擎支持意向锁设计比较简练,其意向锁即为表级别的锁。设计目的主要是为了在一个事务中揭示下一行将被请求的锁类型。其支持两种意向锁:
1 )意向共享锁(lS Lock) ,事务想要获得一张表中某几行的共享锁
2) 意向排他锁(IX Lock) ,事务想要获得一张表中某几行的排他锁
由于InnoDB 存储引擎支持的是行级别的锁,因此意向锁其实不会阻塞除全表扫以外的任何请求。故表级意向锁与行级锁的兼容性如表6-4 所示。
在这里插入图片描述

6.3.2 一致性非锁定读

  一致性的非锁定读(consistent nonlocking read) 是指InnoDB 存储引擎通过行多版本控制(multi versioning) 的方式来读取当前执行时间数据库中行的数据。如果读取的行正在执行DELETE 或UPDATE 操作,这时读取操作不会因此去等待行上锁的释放。相反地,InnoDB 存储引擎会去读取行的一个快照数据。之所以称其为非锁定读,因为不需要等待访问的行上X 锁的释放。
在这里插入图片描述
  快照数据是指该行的之前版本的数据,该实现是通过undo 段来完成。而undo用来在事务中回滚数据,因此快照数据本身是没有额外的开销。此外,读取快照数据是不需要上锁的,因为没有事务需要对历史的数据进行修改操作。
   可以看到, 非锁定读机制极大地提高了数据库的并发性。在InnoDB 存储引擎的默认设置下,这是默认的读取方式,即读取不会占用和等待表上的锁。但是在不同事务隔离级别下,读取的方式不同,并不是在每个事务隔离级别下都是采用非锁定的一致性读。此外,即使都是使用非锁定的一致性读,但是对于快照数据的定义也各不相同。
   ,快照数据其实就是当前行数据之前的历史版本,每行记录可能有多个版本。就图6-4 所显示的,一个行记录可能有不止一个快照数据,一般称这种技术为行多版本技术。由此带来的并发控制,称之为多版本并发控制(Multi Version Concurrency Control. MVCC) 。
在事务隔离级别READ COMMITTED 和REPEATABLE READ (InnoDB 存储引擎的默认事务隔离级别〉下, InnoDB 存储引擎使用非锁定的一致性读。然而,对于快照数据的定义却不相同。在READ COMMITTED 事务隔离级别下,对于快照数据,非一致性读总是读取被锁定行的最新一份快照数据。而在REPEATABLE READ 事务隔离级别下,对于快照数据,非一致性读总是读取事务开始时的行数据版本。

6.3 .3 一致性锁定读

在默认配置下,即事务的隔离级别为REPEATABLE READ 模式下, InnoDB 存储引擎的SELECT 操作使用一致性非锁定读。但是在某些情况下,用户需要显式地对数据库读取操作进行加锁以保证数据逻辑的一致性。而这要求数据库支持加锁语句,即使是对千SELECT 的只读操作。InnoDB 存储引擎对于select语句支持两种一致性的锁定读:

  • SELECT…FOR UPDATE
  • SELECT…LOCK IN SHARE MODE

  SELECT…FOR UPDATE 对读取的行记录加一个X 锁,其他事务不能对已锁定的行加上任何锁。SELECT…LOCK IN SHARE MODE 对读取的行记录加一个S 锁,其他事务可以向被锁定的行加S 锁,但是如果加X 锁,则会被阻塞。
  对于一致性非锁定读,即使读取的行已被执行了SELECT…FOR UPDATE, 也是可以进行读取的,这和之前讨论的情况一样。此外, SELECT…FOR UPDATE, SELECT…LOCK IN SHARE MODE 必须在一个事务中,当事务提交了,锁也就释放了。因此在使用上述两句SELECT 锁定语句时,务必加上BEGIN, START TRANSACTION 或者SET AUTOCOMMIT=0 。

6.3.4 自增长与锁

  在InnoDB 存储引擎的内存结构中,对每个含有自增长值的表都有一个自增长计数器(auto-increment counter) 。当对含有自增长的计数器的表进行插人操作时,这个计数器会被初始化。
  插入操作会依据这个自增长的计数器值加1 赋予自增长列。这个实现方式称做AUTO-INC Locking。这种锁其实是采用一种特殊的表锁机制,为了提高插入的性能,锁不是在一个事务完成后才释放,而是在完成对自增长值插入的SQL 语句后立即释放。
  InnoDB 存储引擎中提供了一种轻量级互斥量的自增长实现机制,这种机制大大提高了自增长值插入的性能。并且从该版本开始, InnoDB 存储引擎提供了一个参数innodb_autoinc lock_ mode 来控制自增长的模式

6.3.5 外键和锁

  前面已经介绍了外键,外键主要用于引用完整性的约束检查。在InnoDB 存储引擎中,对于一个外键列,如果没有显式地对这个列加索引, lnnoDB 存储引擎自动对其加一个索引,因为这样可以避免表锁。
  对于外键值的插入或更新,首先需要查询父表中的记录,即SELECT 父表。但是对于父表的SELECT 操作,不是使用一致性非锁定读的方式,因为这样会发生数据不一致的问题,因此这时使用的是SELECT…LOCK IN SHARE MODE 方式,即主动对父表加一个S 锁。如果这时父表上已经这样加X 锁,子表上的操作会被阻塞。

6.4 锁的算法

6.4.1 行锁的3 种算法

InnoDB 存储引擎有3 种行锁的算法,其分别是:

  • Record Lock: 单个行记录上的锁
  • Gap Lock: 间隙锁,锁定一个范围,但不包含记录本身
  • Next-Key Lock : Gap Lock+Record Lock, 锁定一个范围,并且锁定记录本身

  Record Lock 总是会去锁住索引记录,如果lnnoDB 存储引擎表在建立的时候没有设置任何一个索引,那么这时InnoDB 存储引擎会使用隐式的主键来进行锁定。Next-Key Lock 是结合了Gap Lock 和Record Lock 的一种锁定算法,在Next-Key Lock 算法下, InnoDB 对于行的查询都是采用这种锁定算法。
  例如一个索引有10, 11,13 和20 这四个值,那么该索引可能被Next-Key Locking 的区间为:
(-无穷,10]
(10, 11 ]
(11, 13]
(13, 20]
(20, +无穷)

若事务Tl 已经通过next-key locking 锁定了如下范围:
(10, 11) 、(11, 13)
当插入新的记录12 时,则锁定的范围会变成:
( 10 , 11] 、(11, 12] 、( 12, 13]
然而,当查询的索引含有唯一属性时, InnoDB 存储引擎会对Next-Key Lock 进行优化,将其降级为Record Lock, 即仅锁住索引本身,而不是范围。
innodb会对辅助索引的下一个范围加一个Gap lock,加入插入的记录索引为12 ,那么会在(11, 12)上加Next-Key Lock ,在(12,13)加间隙锁(就是把带有12 的整个范围都锁上),Gap Lock 的作用是为了阻止多个事务将记录插入到同一范围内。
   用户可以通过以下两种方式来显式地关闭Gap Lock:

  • 将事务的隔离级别设置为READ COMMITTED
  • 将参数innodb_locks_ unsafe for binlog 设置为1
      在上述的配置下,除了外键约束和唯一性检查依然需要的Gap Lock, 其余情况仅使用Record Lock 进行锁定。但需要牢记的是,上述设置破坏了事务的隔离性,并且对于replication, 可能会导致主从数据的不一致。此外,从性能上来看, READ COMMITTED也不会优于默认的事务隔离级别READ REPEATABLE 。

  对千唯一键值的锁定, Next-Key Lock 降级为Record Lock仅存在于查询所有的唯一索引列。若唯一索引由多个列组成,而查询仅是查找多个唯一索引列中的其中一个,那么查询其实是range 类型查询,而不是point 类型查询,故lnnoDB 存储引擎依然使用Next-Key Lock 进行锁定。

6.4.2 解决Phantom Problem

  在默认的事务隔离级别下,即REPEATABLE READ 下, InnoDB 存储引擎采用Next-Key Locking 机制来避免Phantom Problem (幻像问题)
  Phantom Problem 是指在同一事务下,连续执行两次同样的SOL 语句可能导致不同的结果,第二次的SOL 语句可能会返回之前不存在的行。InnoDB 存储引擎默认的事务隔离级别是REPEATABLE READ, 在该隔离级别下,其采用Next-Key Locking 的方式来加锁。而在事务隔离级别READ COMMITTED 下,其仅采用Record Lock。

6.5 锁问题

通过锁定机制可以实现事务的隔离性要求,使得事务可以并发地工作。锁提高了并发,但是却会带来潜在的问题。不过好在因为事务隔离性的要求,锁只会带来三种问题,如果可以防止这三种情况的发生,那将不会产生并发异常。

6.5.1 脏读

   脏页指的是在缓冲池中已经被修改的页,但是还没有刷新到磁盘中,即数据库实例内存中的页和磁盘中的页的数据是不一致的,当然在刷新到磁盘之前,日志都已经被写入到了重做日志文件中。而所谓脏数据是指事务对缓冲池中行记录的修改,并且还没有被提交(commit) 。
   对于脏页的读取,是非常正常的。脏页是因为数据库实例内存和磁盘的异步造成的,这并不影响数据的一致性(或者说两者最终会达到一致性,即当脏页都刷回到磁盘)。并且因为脏页的刷新是异步的,不影响数据库的可用性,因此可以带来性能的提高。脏数据却截然不同,脏数据是指未提交的数据,如果读到了脏数据,即一个事务可以读到另外一个事务中未提交的数据,则显然违反了数据库的隔离性。
   脏读指的就是在不同的事务下,当前事务可以读到另外事务未提交的数据,简单来说就是可以读到脏数据。
  脏读发生的条件是需要事务的隔离级别为READ UNCOMMITTED

6.5.2 不可重复读

  不可重复读是指在一个事务内多次读取同一数据集合。在这个事务还没有结束时,另外一个事务也访问该同一数据集合,并做了一些DML 操作。因此,在第一个事务中的两次读数据之间,由于第二个事务的修改,那么第一个事务两次读到的数据可能是不一样的。这样就发生了在一个事务内两次读到的数据是不一样的情况,这种情况称为不可重复读。
  不可重复读和脏读的区别是:脏读是读到未提交的数据,而不可重复读读到的却是已经提交的数据,但是其违反了数据库事务一致性的要求。可以通过下面一个例子来观察不可重复读的情况。
就是在一个事物没有结束的时候,查询了两次。如果另一个事物在此期间插入数据并提交事物,那么A事物两次查询的结果就不一致了。
  隔离级别是READ COMMITTED,可通过Next-Key Lock 算法来避免不可重复读的问题。

6.5.3 丢失更新

  丢失更新是另一个锁导致的问题,简单来说其就是一个事务的更新操作会被另一个事务的更新操作所覆盖, 从而导致数据的不一致。例如:

  1. 事务Tl 将行记录r 更新为vi, 但是事务Tl 并未提交。
  2. 与此同时,事务T2 将行记录r 更新为v2, 事务T2 未提交。
  3. 事务Tl 提交。
  4. 事务T2 提交。

6.6 阻塞

   因为不同锁之间的兼容性关系,在有些时刻一个事务中的锁需要等待另一个事务中的锁释放它所占用的资源,这就是阻塞。阻塞并不是一件坏事,其是为了确保事务可以并发且正常地运行。

6.7 死锁

6.7.1 死锁的概念

   死锁是指两个或两个以上的事务在执行过程中,因争夺锁资源而造成的一种互相等待的现象。若无外力作用, 事务都将无法推进下去。解决死锁问题最简单的方式是不要有等待,将任何的等待都转化为回滚,并且事务重新开始。毫无疑问,这的确可以避免死锁问题的产生。然而在线上环境中,这可能导致并发性能的下降,甚至任何一个事务都不能进行。而这所带来的问题远比死锁问题更为严重,因为这很难被发现并且浪费资源。
   解决死锁问题最简单的一种方法是超时,即当两个事务互相等待时,当一个等待时间超过设置的某一阙值时,其中一个事务进行回滚,另一个等待的事务就能继续进行。在InnoDB 存储引擎中,参数innodb_lock_ wait_ timeout 用来设置超时的时间。
   ,除了超时机制,当前数据库还都普遍采用wait-for graph (等待图)的方式来进行死锁检测。较之超时的解决方案,这是一种更为主动的死锁检测方式。InnoDB 存储引擎也采用的这种方式。

6.8 锁升级

   锁升级(Lock Escalation) 是指将当前锁的粒度降低。举例来说,数据库可以把一个表的1000 个行锁升级为一个页锁,或者将页锁升级为表锁。如果在数据库的设计中认为锁是一种稀有资源,而且想避免锁的开销,那数据库中会频繁出现锁升级现象。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

江北望江南

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值