第6章 锁

第6章 锁

6.1 什么是锁

锁是数据库系统区别于文件系统的一个关键特性。锁机制用于管理对共享资源的并发访问。InnoDB存储引擎会在行级别上对表数据上锁。InnoDB存储引擎也会对数据库内部其他多个地方使用锁,从而允许对多个不同资源提供并发访问。例如,操作缓冲池中的LRU列表,删除、添加。移动LRU列表中的元素,为了保证一致性,必须有锁的介入。数据库系统使用锁是为了支持对共享资源进行并发访问,提供数据的完整性和一致性。
InnoDB存储引擎锁提供了一致性的非锁定读、行级锁支持,行级锁没有相关的开销,可以同时得到并发性和一致性。

6.2 InnoDB存储引擎中的锁

锁的类型

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

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

排他锁和共享锁的兼容性

XS
X冲突冲突
S冲突兼容

InnoDB存储引擎支持多粒度锁定,这种锁定允许在行级上的锁和表级上的锁同时存在。为了支持在不同粒度上进行加锁操作,InnoDB存储引擎支持一种额外的锁方式—意向锁。意向锁是表级别的锁,其设计目的主要是为了在一个事务中揭示下一行将被请求的锁的类型。InnoDB存储引擎支持两种意向锁:

  • 意向共享锁(IS Lock),事务想要获得一个表中某几行的共享锁。
  • 意向排他锁(IX Lock),事务想要获得一个表中某几行的排它锁。

因为InnoDB存储引擎支持的是行级别的锁,所以意向锁其实不会阻塞除全表扫以外的任何请求。可以通过SHOW ENGINE INNODB STATUS命令查看当前请求锁的信息。
在InnoDB Plugin中,在INFORMATION_SCHEMA架构下,添加了INNODB_TRX、INNODB_LOCKS、INNODB_LOCK_WAITS。通过这三张表,可以简单地监控当前的事务并分析可能存在的锁的问题。

#查询方式
select *from information_schema.INNODB_TRX\G

INNODB_TRX字段

字段名字段含义
trx_idInnoDB存储引擎内部唯一的事务ID
trx_state当前事务的状态
trx_started事务的开始时间
trx_requested_lock_id等待事务的锁ID
trx_wait_started事务等待开始的时间
trx_weight事务的权重,反映了一个事务修改和锁定的行数
trx_mysql_thread_idMySQL中的线程ID,SHOW PROCESSLIST显示的结果
trx_query事务运行的SQL语句

这个表只是显示了当前运行的InnoDB的事务,并不能判断锁的一些情况,如果需要查看锁,则需要INNODB_LOCKS表

INNODB_LOCKS

字段字段含义
lock_id锁的ID
lock_trx_id事务ID
lock_mode锁的模式
lock_type锁的类型,表锁还是行锁
lock_table要加锁的表
lock_index锁的索引
lock_spaceInnoDB存储引擎表空间的ID号
lock_page被锁住的页的数量。若是表锁,则该值为NULL
lock_rec被锁住的行的数量。若未表锁,则该值为NULL
lock_data被锁住的行的主键值。若是表锁,则该值为NULL

需要注意的是,lock_data这个值并非总是可信的值,例如当我们运行一个范围查找时,lock_data可能只返回第一行的主键值。另一个不能忽视的是,如果当前资源被锁住了,与此同时,由于锁住的页因为InnoDB存储引擎缓冲池的容量,而导致替换缓冲池中的该页,当查看INNODB_LOCKS表时,该值会显示为NULL,即InnoDB存储引擎不会从磁盘进行再一次查找。
知道了每张表上锁的情况后,我们可以来判断由此而引发的等待情况了。当事务较小时,我们认为地、直观地就可以进行判断了。但是当事务量非常大,锁和等待也时常发生时,这个时候不容易判断,但是可以通过INNODB_LOCK_WAITS,可以很直观地反映出当前的等待。

INNODB_LOCK_WAITS

字段字段含义
requesting_trx_id申请锁资源的事务ID
requesting_lock_id申请的锁的ID
blocking_trx_id阻塞的事务ID
blocking_lock_id阻塞的锁的ID

通过这些字段,可以清晰地看到哪个事务阻塞了另一个事务。

一致性的非锁定读操作

一致性的非锁定读(consistent nonlocking read)是指InnoDB存储引擎通过行多版本控制(multi versioning)的方式来读取当前执行时间数据库中行的数据。如果读取的行正在执行DELETE、UPDATE操作,这时读取操作不会因此而等待行上锁的释放,相反,InnoDB存储引擎会去读取行的一个快照数据。
之所以称之为非锁定读,因为不需要等待访问的行上X锁的释放。快照数据是指该行之前版本的数据,该实现是通过Undo段来实现。而Undo用来在事务中回滚数据,因此快照数据本身是没有额外的开销。此外,读取快照数据是不需要上锁的,因为没有必要对历史的数据进行修改。历史版本可能有多个,也就是一个行可能有不止一个快照数据,称这种技术为行多版本技术。由此带来的并发控制,称之为多版本并发控制(Multi Version Concurrency Control,MVCC)。
在InnoDB存储引擎默认设置下,这是默认的读取方式,即读取不会占用和等待表上的锁。但是在不同事务隔离级别下,读取的方式不同,并不是每个事务隔离级别下读取的都是一致性读。同样,即使都是使用一致性读,但是对于快照数据的定义也不同。
在read committed和repeatable read下,InnoDB存储引擎使用非锁定的一致性读。然而对于快照数据的定义却不相同。在read committed事务隔离级别下,对于快照数据,非一致性读总是读取被锁定行的最新一份快照数据,在repeatable事务隔离级别下和repeatable read事务隔离级别下,对于快照数据,非一致性读总是读取事务开始时的行数据版本。
对于read committed的事务隔离级别而言,从数据库理论的角度来看,其实违反了事务ACID中的I特性,即隔离性。

SELECT…FOR UPDATE & SELECT…LOCK IN SHARE MODE

在默认情况下,InnoDB存储引擎的SELECT操作使用一致性非锁定读。但是在某些情况下,需要对读取操作进行加锁。InnoDB存储引擎对于SELECT 语句支持两种加锁操作:

  • 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。

自增长和锁

自增长在数据库中是非常常见的一种属性,也会很多人的首选的主键方式。在InnoDB存储引擎的内存结构中,对每个含有自增长值的表都有一个自增长计数器(auto-increment counter)。当对含有自增长计数器的表进行插入操作时,这个计数器会被 初始化,执行如下的语句来得到计数器的值:

SELECT MAX(auto_inc_col)FROM t FOR UPDATE;

插入操作会依据这个自增长的计数器值加1赋予自增长列。这个实现方式称做AUTO-INC Locking。这种锁其实是采用一种特殊的表锁机制,为了提高插入的性能,锁不是在一个事务完成后才释放,而是在完成对自增长值插入的SQL语句后立即释放。
虽然AUTO-INC Locking从一定程度上提高了并发插入的效率,但这里还是存在一些问题。首先,对于有自增长值的列的并发插入性能较差,所以必须等待前一个插入的完成(虽然不用等待事务的完成)。其次,对于INSERT…SELECT的大数据量的插入,会影响插入的性能,因为另一个事务中的插入会被阻塞。
InnoDB存储引擎提供了一个参数innodb_autoinc_lock_mode,默认值为1。

外键和锁

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

6.3 锁的算法

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

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

Record Lock总是会去锁定索引记录。如果InnoDB存储引擎表建立的时候没有设置任何一个索引,这时InnoDB存储引擎会使用隐式的主键来进行锁定。
Next-Key Lock是结合了Gap Lock和Record Lock的一种锁定算法,在Next-Key Lock算法下,InnoDB对于行的查询都是采用这种锁定算法。对于不同SQL查询语句,可能设置共享(Share)Next-Key和排他的(Exclusive)Next-Key Lock。

6.4 锁问题

通过锁可以实现事务的隔离性的 要求,使得事务可以并发地工作。锁提高了并发,但是却会带来3种问题,如果可以防止这3种情况的发生,就不会产生异常。

丢失更新

丢失更新(lost update)是一个经典的数据库问题。出现下面的情况时,就会发生丢失更新:

  • 事务T1查询一行数据,放入本地内存,并显示给一个终端用户User1
  • 事务T2也查询该行数据,并将取得的数据显示给终端用户User2
  • User1修改这行记录,更新数据库并提交
  • User2修改这行记录,更新数据库并提交

要避免丢失更新发生,其实需要让这种情况下的事务变成串行操作,而不是并发的操作。即在上述四种的第1种情况下,对用户读取的记录加上一个排他锁,同样,发生第2种情况下的操作时,用户也需要加一个排它锁。

脏读

脏数据和脏页有所不同。
脏页指的是在缓冲池中已经被修改的页,但是还没有刷新到磁盘,即数据实例内存中的页和磁盘的页中的数据是不一致的,当然在刷新到磁盘之前,日志都已经被写入了重做日志文件中。而所谓脏数据,是指在缓冲池中被修改的数据,并且还没有被提交(commit)。
对于脏页的读取,是非常正常的。脏页是因为数据库实例内存和磁盘的异步同步造成的,这并不影响数据的一致性。并且因为是异步的,因此可以带来性能的提高。而脏数据却不同,脏数据是指未提交的数据,如果读到了脏数据,即一个事务可以读到另外一个事务中未提交的数据,则显然违反了数据库的隔离性。
InnoDB存储引擎默认的事务隔离级别为READ REPEATABLE。
脏读现象在生产环境中并不常发生,脏读发生的条件是READ UNCOMMITTED

不可重复读

不可重复读是指在一个事务内多次读同一个数据。在这个事务还没有结束时,另外一个事务也访问该同一数据,那么,在第一个事务的两次读数据之间,由于第二个事务的修改,第一个事务两次读到的数据可能是不一样的。这样就发生了在一个事务内两次读到的数据是不一样的,因此称为不可重复读。
不可重复读和脏页的区别是:脏读是读到未提交的数据,而不可重复读读到的确实是已经提交的数据,但是违反了数据库事务一致性的要求。
InnoDB存储引擎中,通过使用Next-Key Lock算法来避免不可重复读的问题。在MySQL官方文档中,将不可重复读定义为Phantom Problem,即幻读问题。在Next-Key Lock算法下,对于索引的扫描,不仅仅是锁定扫描的索引,而且还锁定这些索引覆盖的范围(gap)。因此对于这个范围内的插入都是不允许的。这样就避免了另外的事务在这个范围内插入数据导致的不可重复读的问题。因此,InnoDB存储引擎的默认事务隔离级别是READ REPEATABLE,采用Next-Key Lock算法,就避免了不可重复读的现象。

6.5 阻塞

因为不同锁之间的兼容性关系,所以在有些时刻,一个事务中的锁需要等待另一个事务中的锁释放它所占用的资源。在InnoDB存储引擎的源代码中,用Mutex数据结构来实现锁。在访问资源前需要用mutex_enter函数进行申请,在资源访问或修改完毕后立即执行mutex_exit函数。当一个资源已被一个事务占有时,另一个事务执行mutex_enter函数会发生等待,这就是阻塞。阻塞并不是一件坏事,阻塞是为了保证事务可以并发并且正常运行。
在InnoDB存储引擎中,参数innodb_lock_wait_timeout用来控制等待的时间(默认是50秒),innodb_rollback_on_timeout用来设定是否在等待超时时对进行中的事务进行回滚操作(默认是OFF,表示不回滚)。参数innodb_lock_with_timeout是动态的,可以在MySQL数据库运行时进行调整,而innodb_rollback_on_timeout是静态的,不可在启动时进行调整。
需要牢记的是,默认情况下InnoDB存储引擎不会回滚超时引发的错误异常。其实InnoDB存储引擎在大部分情况下都不会对异常进行回滚。

6.6 死锁

死锁只发生于并发的情况,数据库就是一个并发着的程序,因此可能发生死锁。InnoDB存储引擎有一个后台的锁监控线程,该线程负责查看可能的死锁问题,并自动告知用户。
InnoDB存储引擎并不会回滚大部分的错误异常,但是死锁除外。发生死锁后,InnoDB存储引擎会马上回滚一个事务。

6.7 锁升级

锁升级(Lock Escalation)是指将当前锁的粒度降低。比如将一个行锁升级为一个页锁,将一个页锁升级为一个表锁。
InnoDB存储引擎不存在锁升级的问题,在InnoDB存储引擎中,1个锁的开销与1000000个锁的开销是一样的,都没有开销。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值