MySQL技术内幕(InnoDB存储引擎概述)学习笔记03-锁

什么是锁

锁是数据库系统区别于文件系统的一个关键特性。锁机制用于管理对共享资源的并发访问。InnoDB会在行级别上对表数据上锁,这固然不错。不过InnoDB也会在数据库内部其他多个地方使用锁,从而允许对多种不同资源提供并发访问。例如,操作缓冲池中的LRU列表,删除,添加,移动LRU列表中的元素,为了保证一致性,必须有锁的介入。数据库系统使用锁是为了支持对共享资源的并发访问,提供数据的完整性和一致性。

另一点需要理解的是,虽然现在数据库系统做得越来越类似,但是有多少种数据库就有多少种锁的实现方法。在SQL语法层面,因为有SQL标准的存在,要熟悉多个数据库系统并不是一件难事。而对于锁,用户可能对某个特定的关系数据库系统的锁定模型有一定的经验,但这并不意味着知道其他数据库。不同数据库(如Microsoft SQL Server, Oracle等),甚至不同的存储引擎(如MyISAM,NDB Cluster)对于锁的实现完全不同。

对于MyISAM,其锁是表锁设计。并发情况下读没有问题,但是并发插入的性能就要差一些了,如果插入是在“底部”,MyISAM存储引擎还是可以有一点的并发写入操作。对于Microsoft SQL Server,在2005版本之前其都是页锁,相对于表锁并发性能有所提升。页锁容易实现,但是对于热点数据页依然无能为力。到2005版本开始支持乐观并发和悲观并发,在乐观并发下开始支持行级锁,但是其实现方式与InnoDB不同。用户会发现在Microsoft SQL Server下,锁是一种稀有的稀缺资源,锁越多开销越大,因此他会有锁升级,在这种情况下,行锁会升级为表锁,这时并发性能就又回到了以前。

InnoDB存储引擎锁的实现和Oracle数据库非常类似,提供一致性非锁定读,行级锁支持,行锁没有相关额外的开销,并可以同时得到并发性和一致性。

lock与latch

在数据库中,lock与latch都可以称为锁。但是两者有着截然不同的含义。

latch一版称为,闩锁(轻量级的锁),因为其要求锁定的时间必须非常短。若持续的时间长,则应用的性能会非常差。在InnoDB存储引擎中,latch又可以分为mutex(互斥锁)和rwlock(读写锁)。其目的是用来保证并发线程操作临界资源的正确性,并且通常没有死锁检测的机制。

lock的对象是事务,用来锁定的是数据库中的对象,如表,页,行。并且一般lock的对象在事务commit或rollback后进行释放(不同的事务隔离级别释放的时间可能不同)。此外,lock,正如在大多数数据库中一样,是有死锁机制的。

locklatch
对象事务线程
保护数据库内容内存数据结构
持续时间整个事务过程临界资源
模式行锁,表锁,意向锁读写锁,互斥量
死锁通过waits-for graph,time out等机制进行死锁检测与处理无死锁检测与处理机制。仅通过应用程序加锁的顺序(lock leveling)保证无死锁的情况发生
存在于Lock Manager的哈希表中每个数据结果的对象中

对于InnoDB中的latch,可以通过show engine innodb mutex来进行查看,输出结果说明

名称说明
countmutex被请求次数
spin_waitsspin lock(自旋锁)的次数,InnoDB存储引擎latch在不能获得锁时首先进行自旋,若自旋后还不能获得锁,则进入等待状态
spin_rounds自旋内部循环的总次数,每次自旋的内部循环是一个随机数。spin_rounds/spin_waits表示每次自旋所需的内部循环次数
os_waits表示操作系统等待的次数。当spin_lock通过自旋还是不能获得latch时,则会进入操作系统等待状态,等待被唤醒
os_yields进行os_thread_yield唤醒操作的次数
os_wait_times操作系统等待的时间,单位是ms

上述信息比较底层,一版仅供开发人员参考。

InnoDB存储引擎中的锁

锁的类型

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

  • 共享锁(S LOCK) : 允许事务读一行数据
  • 排它锁(X LOCK) : 允许事务删除或更新一行数据

如果一个事务T1已经获得了行r的共享锁,那么另外的事务T2可以立即获得行r的共享锁,因为读取并没有改变行r的数据,成这种情况为锁兼容(Lock Compatible)。但若有其他事务T3想要获得行r的排它锁,则必须等待事务T1,T2释放行r上的共享锁,这种情况称为锁不兼容。如下表

 XS
X不兼容不兼容
S不兼容兼容

从上表可以发现X锁与任何锁都不兼容,而S锁仅和S锁兼容。需要特备注意的是,S锁和X锁都是行锁,兼容是指对同一记录锁的兼容情况。

此外,InnoDB支持多粒度(granular)锁定,这种锁定允许事务在行级上的锁和表级上的锁同时存在。为了支持在不同粒度上进行加锁操作,InnoDB存储引擎支持一种额外的锁方式,称之为意向锁(Intention Lock)。意向锁是将锁定的对象分为多个层次,意向锁意味着事务希望在更细粒度上进行加锁。

若将上锁对象看成一棵树,那么对最下层的对象上锁,也就是对最细粒度(fine granularity)的对象进行上锁,那么首先需要对粗粒度的对象上锁。如果需要对页上的记录r上X锁,那么分别需要对数据库A,表,页上意向锁IX,最后对记录r上X锁。若其中任何一个部分导致等待,那么改操作需要等待粗粒度锁的完成。举例来说,在对记录r加X锁之前,已经有事务对表1进行了S表锁,那么表1上已存在S锁,之后事务需要对记录r在表1上加上IX,由于不兼容,所以该事务需要等待表锁操作的完成。

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

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

由于InnoDB支持的是行级别的锁,因此意向锁其实不会阻塞除全表扫变外的任何请求。意向锁与行级锁的兼容性入校

 ISIXSX
IS兼容兼容兼容不兼容
IX兼容兼容不兼容不兼容
S兼容不兼容兼容不兼容
X不兼容不兼容不兼容不兼容

可以在show engine innodb status的输出结果中搜索lock来查看当前锁的情况

在InnoDB 1.0版本之后,可以在INFORMATION_SCHEMA数据库中通过表INNODB_TRX,INNODB_LOCKS,INNODB_LOCK_WAITS来监控锁的使用情况。

INNODB_TRX结构

该表显示了当前运行的InnoDB事务的信息

字段名说明
trx_idInnoDB内部唯一的事务ID
trx_state当前事务的状态
trx_started事务的开始时间
trx_request_lock_id等待事务的锁ID。如trx_state的状态为LOCK WAIT,那么改值代表当前的事务等待之前事务占用锁资源的ID。若trx_state不是LOCK WAIT,则该值为NULL
trx_weight事务的权重,反映了一个事务修改和锁住的行数。在InnoDB中,当发生死锁需要回滚时,会选择该值最小的的进行回滚
trx_mysql_thread_idMySQL中的线程ID,SHOW PROCESSLIST显示的结果
trx_query事务运行的SQL语句

INNODB_LOCKS结构

该表显示了当前运行的InnoDB事务锁的信息

字段名说明
lock_id锁的ID
lock_trx_id事务ID
lock_mode锁的模式
lock_type锁的类型,表锁还是行锁
lock_table要加锁的表
lock_index锁住的索引
lock_space锁对象的space id
lock_page事务锁定的页的数量。若是表锁,则该值为NULL
lock_rec事务锁定的行的数量,若是表锁,则该值为NULL
lock_data事务锁定的记录的主键值,若是表锁,则该值为NULL

需要注意的是,lock_data这个值并非是“可信”的值。例如用户运行了一个范围查找时,lock_data可能只返回第一行的主键值。与此同时,如果当前资源被锁住了,若锁住的页因为InnoDB缓冲池的容量,导致该页从缓冲池中被刷出,则查看INNODB_LOCKS表时,该值同样会显示为NULL,即InnoDB不会从磁盘再进行一次查找。

INNODB_LOCK_WAITS结构

该表显示了当前运行的InnoDB事务等待的情况

字段名说明
requesting_trx_id申请锁资源的事务ID
requesting_lock_id申请的锁的ID
blocking_trx_id阻塞的事务ID
blocking_lock_id阻塞的锁ID

这个表可以清楚的看到那个事务阻塞了另一个事务

一致性非锁定读

一致性的非锁定读(consistent nonlocking read)是指InnoDB通过行多版本控制(muti versioning)的方式来读取当前执行时间数据库中行的数据。如果读取的行正在执行DELETE或UPDATE操作,这时读取操作不会因此去等待行上锁的释放。相反的,会取读取行的一个快照数据。

之所以称为非锁定读,是因为不需要等待访问的行上X锁的释放。快照数据是指之前版本的数据,该实现是通过undo端来完成。而undo段用来在事务中回滚数据,因此快照数据本身是没有额外的开销。此外,读取快照数据是无需上锁的,因为没有事务会对历史数据进行修改操作。

非锁定读机制极大提高了数据库的并发性。在InnoDB的默认设置下,这是默认的读取方式,即读取不会占用和等待表上的锁。但是在不同事务隔离级别下,读取的方式不同,并不是在每个事务隔离级别下都是采用非锁定一致性读。此外,即使都是使用非锁定一致性读,但是对于快照数据的定义也各不同。

快照数据其实就是当前行数据之前的历史版本,每行记录可能有多个版本。一行数据可能有不止一个快照数据,一般称这种技术为行多版本技术。由此带来的并发控制,称之为多版本并发控制(Mutil Version Concurrency Control, MVCC)。

在事务隔离级别READ COMMITTED和REPEATABLE READ(InnoDB存储引擎的默认事务隔离级别)下,InnoDB使用非锁定一致性读。然而,对于快照数据的定义确不同。在READ COMMITTED事务隔离级别下,对于快照数据,非一致性读总是读取被锁定行的最新的一份快照数据。而在REPEATABLE READ事务隔离级别下,对于快照数据,非一致性读总是读取事务开始时的行数据版本。

一致性锁定读

在某些情况下,用户需要显式的对读取操作加锁以保证数据逻辑的一致性。而要求数据库支持加锁语句,即使是对于select的只读操作。InnoDB对于select语句支持两种一致性锁定读(locking read)操作

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

SELECT ... FOR UPDATE对读取的行记录加一个X锁,其他事务不能对已锁定的行加上任何锁。SELECT ... LOCK IN SHARE MODE对读取的行记录加一个S锁,其他事务可以对被锁定的行加S锁,但是如果加X锁,则会被阻塞。

对于一致性锁定读,即使读取的行已经被执行了SELECT ... FOR UPDATE,也是可以进行读取的,这和之前讨论的情况一样。此外,这两个语句必须在事务中执行,当事务提交了,锁也就释放了。因此在使用这两个SQL语句时,务必加上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的大数据量的插入会影响插入的性能,因为另外一个事务中的插入会被阻塞。

从5.1.22版本开始,InnoDB提供了一种轻量级互斥量的自增长实现机制,这种机制能大大提高自增长值的性能。

在InnoDB存储引擎中,自增长的列必须是索引,同时必须是索引的第一个列。

外键和锁

外键用于引用完整性的约束检查。在InnoDB中,对于一个外键列,如果没有显示的对这个列加索引,InnoDB会自动对其加一个索引,因为这样可以避免表锁。

对于外键的插入或更新,首先需要查找父表中的记录,即select父表。但是对于父表的select操作,不是使用一致性非锁定读的方式,因为这样会产生数据不一致的问题,因此使用的是SELECT ... LOCK IN SHARE MODE方式,即主动对表加一个S锁。如果这是父表上已经加了X锁,子表上的操作会被阻塞。

锁的算法

锁的3中算法

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

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

Next-Key Lock是结合了Gap Lock和Record Lock的一种锁定算法,在Next Lock算法下,InnoDB对于行的查询都是采用这种锁定算法

略略略。。。

标签: mysql,

添加新评论