MySql技术内幕 - 锁

一、锁的类型

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

  1. 共享锁(S Lock),允许事务读取一行数据
  2. 排他锁(X Lock),允许事务删除或更新一行数据
排他锁与共享锁的兼容性
  X S
X 不兼容 不兼容
S 不兼容 兼容

  可见只有共享锁之间是兼容的,同时因为他们都是行锁,所以兼容与不兼容指的是同一行记录的情况。

  同时InnoDB也支持多粒度锁定,这种锁定允许事务在行级别上的锁和表级别的锁同时存在,为了支持在不同粒度上进行加锁操作,InnoDB存储引擎支持一种额外的锁方式

  称之为意向锁(Intention Lock)。意向锁是将锁定的对象分为多个层次,意向锁意味着事务希望在更细粒度上进行加锁。

  

  如果想要在一个行记录上添加X锁,那么就需分别对数据库,表,页上添加意向锁,最后再对行记录添加X锁,如果在当前事务之前已经有一个事务在表上添加了X表锁

  那么就需要等之前的事务完成,释放了表的X锁之后,我们才能继续执行事务。

 

  InnoDB的意向锁即为表级别的锁,设计的主要目的是为了在一个事务中揭示下一行将被请求的锁类型,其支持两种意向锁:

  1. 意向共享锁(Intention S Lock),事务想要获得一张表中的某几行的共享锁
  2. 意向排他锁(Intention X Lock),事务想要获得一张表中的某几行的排他锁

  由于InnoDB存储引擎支持的是行级别的锁,因此意向锁其实不会阻碍除全表扫描以外的任何请求,故得出意向锁与行级别锁的兼容性

行级锁与意向锁的兼容性
  IS IX S X
IS 兼容 兼容 兼容 不兼容
IX 兼容 兼容 不兼容 不兼容
S 兼容 不兼容 兼容 不兼容
X 不兼容 不兼容 不兼容 不兼容

  用户可以通过:show engine innodb status 命令来查看当前锁的信息

  在InnoDB 1.0开始,在information_schema下增加了三张表来记录锁信息:INNODB_TRX , INNODB_LOCKS , INNODB_LOCK_WAITS

二、一致性非锁定读

  一致性的非锁定读是指InnoDB存储引擎通过多版本控制(MVCC)的方式来读取当前执行时间数据库中的行记录。如果读取的的行正在执行Delete或Update操作

这时不会因此去等待行上的锁释放,相反地会读取行的一个快照数据。

  这里之所以称之为非锁定读,是因为不需要等待访问行上的X锁。快照数据是指该行的之前版本的数据,该实现是通过undo段来完成的,而undo是用来在事务中回滚数据的

因此快照数据本身并没有额外的开销,此外快照数据也不需要上锁,因为没有事务会在历史数据上进行修改操作。

  可以看到非锁定读机制极大的提高了数据库的并发性,在InnoDB存储引擎的默认设置下,这是默认的读取方式,即读取不会占用和等待表上的锁。但是在不同的事务隔离级别下

读取的方式有所不同,并不是在每个事务隔离级别下都是采用非锁定的一致性读,此外即使都是使用非锁定的一致性读,但是对于快照数据的定义也各不相同。

  

Read Commited 和 Repeatable Read下:

  在这两种事务隔离级别下,InnoDB 存储引擎是采用非锁定一致性读的,但是对于快照数据的定义却不同。

  在RC下,对于快照数据非锁定一致性读取总数读取最新一份快照数据,而在RR下,是读取事务开始时的行数据版本

三、一致性锁定读

  与一致性非锁定读不同,可能在某些情况下,我们需要显示地对数据库读取操作加锁以保证数据逻辑的一致性。InnoDB存储引擎对于select语句支持两种一致性锁定读的操作:

  1. select for update
  2. select lock in share mode

  for update表示对读取的行添加一个X锁,lock in share mode表示对读取的行添加一个S锁,锁之间同样适用于开始提到的兼容性的关系。

四、自增长锁

  自增长在数据库中是一种比较常见的属性,在InnoDB存储引擎中,如果一个表中含有自增长的列,那么就会有一个自增长的计数器,当对表进行插入操作的时候

这个计数器就会被初始化,,执行如下的语句来得到计数器的值:

  select max(auto_inc_col) from t for update

  插入操作会一句这个自增长的计数器的值加1赋予自增长列,这种实现方式叫做AUTO_INC Locking,这种锁其实是采用一种特殊的表锁机制,为了提高插入的性能,这个锁不是在事务

结束之后释放的,而是在完成对自增长SQL语句的插入后立即释放。

  但是这样做还是会有一些性能的问题,比如在进行批量插入的时候,并发的性能是比较差的,所以在MySql 5.1.22 版本开始,在InnoDB存储引擎中提供了一种轻量级互斥量的自增长的实现机制。

  除此之外,在InnoDB存储引擎中,自增长的列必须是索引,同时必须是索引的第一个列,否则会报错。

五、外键和锁

  首先说明在MySql中对于一个外键列,如果没有显示的添加索引,那么InnoDB存储引擎会自动的为其添加一个索引,因为这样可以避免全表扫描。

对于含有外键的插入或更新,首先需要查询父表中的记录,但是这时采用的不是一致性非锁定读,而是通过lock in share mode的方式,否则可能会导致数据不不一致

但是如果这时需要查询的父表中的记录应存在X锁,那么当前的查询请求就会被阻塞住。

六、锁的算法

幻读:是指在同一事务下,连续执行两次同样的SQL语句导致不同的结果,第二次的SQL语句可能会返回之前不存在的行

为了避免幻读情况的出现影响事务,MySql 在RR下通过Gap Lock来解决幻读的问题,比如我们用select lock in share mode的方式执行一个查询

这时即使这个记录不存在,但是也会产生一个Gap Lock,这时用来保证再次插入的记录也是唯一的。

那么如果这个时候多个事务操作的话,会发生死锁的情况,最终通过发现死锁机制,保证只有一个事务能够插入成功。

 

在InnoDB中存在3种行锁的算法:

  1. Record Lock : 单个行记录上的锁
  2. Gap Lock : 间隙锁,锁定一个范围,但是不包括记录本身
  3. Next-Key Lock : 锁定一个范围,并且包含记录本身

Record Lock总是会去锁住索引记录,如果InnoDB存储引擎表在建立的时候没有设置任何一个索引,那么这时InnoDB存储引擎会使用隐式的主键来进行锁定。

当查询的索引含有唯一属性时,InnoDB存储引擎会对Next-Key Lock进行优化,将其降级为Record Lock,即仅锁住索引本身,而不是范围。

但是这种特例只是针对查询唯一索引列的情况,如果唯一索引列由多个列组成,而查询仅是查找多个唯一索引中的其中一个,那么其实还是范围查询,这种情况是不会降级为Record Lock。

上面说的是唯一索引,但是如果辅助索引,首先是会对查询的行添加一个Gap Lock,然后还会对辅助索引的下一个键值加上Gap Lock

 

我们可以通过以下两种方式来显示地关闭Gap Lock : 

  1. 将事务的隔离级别设置为RC
  2. 将参数innodb_locks_unsafe_for_binlog设置为1

七、锁问题

脏读:在不同的事务下,当前事务可以读取到另外事务未提交的数据。

不可重复读:在一个事务内多次读取同一数据集合,但是返回的结果却是不同的,读取到了其他事务已经提交了的数据。

死锁:是指两个或两个以上的事务在执行过程中,因争夺锁资源而造成的一种互相等待的现象。

 

  解决死锁问题最简单的一种方法是超时,即当两个事务相互等待时,当一个等待时间超过设置的阈值,其中一个事务进行回滚,另一个等待的事务就能继续进行了

在InnoDB存储引擎中,参数innodb_lock_wait_timeout用来设置超时的时间。

  超时机制虽然简单,但是其仅通过超时后对事务进行回滚的方式来处理,或者说其是根据FIFO的顺序选择回滚的对象,但是若超时的事务所占权重比较大,入事务操作更新了很多行

占用了较多的undo log ,这时采用FIFO的方式,就显得不合适了,因为回滚这个事务的时间相对另一个事务所占用的时间可能会很多。

  因此除了超时机制,MySql中采用了一个叫做wait-for graph(等待图)的方式来检测死锁,相比于超时,这是一种更为主动的检测方式,但是其要求数据库保留以下两种信息:

  1. 锁的信息链表
  2. 事务等待链表

通过上述链表可以构造出一张图,而在这个图中若存在回路,就代表存在死锁,因此资源间互相发生等待。

大部分的死锁不需要认为干预,MySql会根据等待图选择一个undo最小的进行回滚

 

在数据库中还存在一个概念,叫做锁升级。就是降低锁的粒度,比如从行级锁升级为表锁,目的就是为了减少开销,这种情况在Sql Server中会比较常见

但是在MySql中不存在锁升级的问题,因为其不是根据每个行记录来产生锁的,而是根据每个事务访问的每个页对锁进行管理的,采用的是位图的方式,因此不管是锁一个页中的一行记录

还是多行记录,其开销通常都是一样的。

 

posted @ 2020-03-26 15:10  SyrupzZ  阅读(121)  评论(0)    收藏  举报