注册 登录
  • 欢迎访问"运维那点事",推荐使用Google浏览器访问,可以扫码关注本站的"微信公众号"。
  • 如果您觉得本站对你有帮助,那么可以扫码捐助以帮助本站更好地发展。

MySQL InnoDB锁系统阐述(一)

MySQL 彭东稳 6206次浏览 已收录 0个评论

一、什么是锁?

开发多用户、数据库驱动的应用时,最大的难点是:一方面要最大程度地利用数据库的并发访问,另外一方面还要确保每个用户能以一致的方式读取和修改数据。为此就有了锁的机制。同时这也是数据库系统区别于文件系统的一个关键特性,InnoDB存储引擎之MySQL数据库的其他存储引擎在这方面技高一筹,就是因为它支持高并发,而高并发的实现很大程度上得益于细粒度的锁实现(行级锁)。只有正确了解这些锁的内部机制才能充分发挥InnoDB存储引擎在锁方面的优势。

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

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

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

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

二、InnoDB存储引擎中的锁

  • 锁类型

InnoDB支持到行级别粒度的并发控制,其是通过给索引上的索引项加锁来实现行锁,这种特点也就意味着,只要通过索引条件检索数据,InnoDB才会使用行级锁,否则会使用表锁。这里简单先介绍一下两种标准的行级锁。

读锁:当事务1对某个数据对象加读锁,则其他事务只能对此数据对象加读锁,不能加写锁。直到事务1释放锁。所以读锁又称共享锁(S)。

写锁:当事务1对某个数据对象加写锁,则其他事务都不能再对此数据对象加任何锁。所以写锁又称排他锁(X)。

从对读锁和写锁的描述来看,X锁与任何的锁都不兼容;而S锁仅和S锁兼容。且需要注意的是SX锁都是行锁,兼容是指对同一记录(Row)锁的兼容性情况。

  • 锁粒度

InnoDB存储引擎在锁粒度上支持表锁(对整个表加上锁)和行锁(对某行记录加上锁)。锁粒度与锁类型组合起来就有:行级共享锁,表级共享锁,行级排他锁,表级排他锁。另外,InnoDB还支持间隙锁(Gap Locks),Next-Key Locks。InnoDB存储引擎支持多粒度锁定,这种锁定允许在行级上的锁和表级上的锁同时存在。为了支持在不同粒度上进行加锁操作,InnoDB存储引擎支持一种额外的锁方式,我们称之为意向锁(Intention Locks)。意向锁是表级别的锁,其设计目的主要是为了在一个事务中揭示下一行将被请求的锁的类型。

  • 意向锁

InnoDB存储引擎支持多种粒度锁(也就是多层级加锁),允许行锁和表锁共存。为了在多个粒度级别上进行锁定,InnoDB存储引擎使用意向锁来实现,意向锁是表级锁(InnoDB不支持数据库级别的锁及页锁,所以意向锁只能加在表上面,意向锁是表锁也就是这个意思),它先指明了该事务是那种类型的锁(共享锁或者独占锁),然后去锁定表中某行记录。InnoDB存储引擎支持两种意向锁:

意向共享锁(IS Lock:表明事务想要获得一个表中某几行的共享锁。

意向排他锁(IX Lock:表明事务想要获得一个表中某几行的排他锁。

例如,SELECT … LOCK IN SHARE MODE设置一个IS锁或SELECT … FOR UPDATE设置一个IX锁。

意向锁的添加规则如下:

1. 在一个事务对一张表的某行添加S锁之前,它必须对该表获取一个IS锁或者优先级更高的锁。

2. 在一个事务对一张表的某行添加X锁之前,它必须对该表获取一个IX锁。

意向锁是将锁定的对象分为多个层次,意向锁意味着事务希望在更细粒度上进行加锁。若将上锁的对象看成一棵树,那么对最下层的对象上锁,也就是对最细粒度的对象进行上锁,那么首先需要对粗粒度的对象上锁,如下图:

MySQL InnoDB锁系统阐述(一)

如果需要对页上的r记录上X锁,那么分别需要对数据库、表、页上意向锁IX(InnoDB不支持库、页级别的锁;这里只有表、记录锁),最后对记录r上X锁。若其中任何一个部分导致等待,那么该操作需要等待粗粒度锁的完成。

数据库引擎使用意向锁来保护锁层次结构的底层资源,以防止其他事务对自己锁住的资源造成伤害,提高锁冲突检测性能。

意向锁的存在价值在于在定位到特定的行所持有的锁之前,提供一种更粗粒度的锁,可以大大节约引擎对于锁的定位和处理的性能,因为在存储引擎内部,锁是由一块独立的数据结构维护的,锁的数量直接决定了内存的消耗和并发性能。例如,事务A对表t的某些行修改(DML通常会产生X锁),需要对t加上意向排它锁IX,在A事务完成之前,B事务需要给表加X锁操作(Flush tables with read lock),此时直接在表级别的意向排它锁就能告诉B事务需要等待(因为表t上有意向锁IX,IX与X互斥)。意向锁可以提高性能,因为数据库引擎仅在表级检查意向锁,确定事务是否能安全地获取该表上的锁,而不需要检查表中的每行或每页上的锁以确定事务是否可以锁定整个表。通过意向锁实现了行锁和表锁共存且满足事务隔离性的要求。

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

X IX S IS
X 冲突 冲突 冲突 冲突
IX 冲突 兼容的 冲突 兼容的
S 冲突 冲突 兼容的 兼容的
IS 冲突 兼容的 兼容的 兼容的

用户可以通过命令SHOW ENGINE INNODB STATUS来查看当前锁请求的信息,如果设置变量set global innodb_status_output_locks=on;的话将会显示更详细的锁信息:

另外从InnoDB 1.0开始在INFORMATION_SCHEMA架构下有INNODB_TRXINNODB_LOCKSINNODB_LOCK_WAITS这三种表。通过这三种表,可以有效地监控当前事务并分析可能存在的锁的问题。其中INFORMATION_SCHEMA.INNODB_TRX表字段含义。

表字段说明:

trx_id:InnoDB存储引擎内部唯一的事务ID。

trx_state:当前事务的状态。

trx_started:事务的开始时间。

trx_requested_lock_id:等待事务的锁ID,如果trx_state的状态为LOCK WAIT,那么该值代表当前的事务等待之前事务占用锁资源的ID。若trx_state不是LOCK WAIT,则该值为NULL。

trx_wait_started:事务等待的开始时间。

trx_weight:事务的权重,反映了一个事务修改和锁住的行数。在InnoDB存储引擎中,当发生死锁需要回滚时,InnoDB存储引擎会选择该值最小的进行回滚。

trx_mysql_thread_id:MySQL中的线程ID,SHOW PROCESSLIST显示的结果。

trx_query:事务运行的SQL语句,这个字段只显示正在执行的SQL,如果一个事务中SQL已经执行完毕,但还未提交,那么由于SQL执行完了,在这个字段显示为NULL。

通过列state可以观察到trx_id7955的事物当前正在运行,而trx_id7955的事物目前处于“LOCK WAIT”状态,且运行的SQL语句是select * from tab_with_index where id= 100 for update;,但是此值属于瞬时的,一般基本为NULL,所以对于排查问题来说基本无用,这一点本人是深有体会,当你的事务出现问题时来查看此表,基本是看不到执行的SQL语句的。该表只是显示了当前运行的InnoDB事务,并不能直接判断锁的一些情况。

如果需要查看锁,则还需要访问表INFORMATION_SCHEMA.INNODB_LOCKS,该表的字段含义如下。

表字段说明:

lock_id:锁的ID,由事务ID:表ID:Page no:Heap no组成。

lock_trx_id:事务ID。

lock_mode:锁的模式,S或X。

lock_type:锁的类型,表锁还是行锁。

lock_table:要加锁的表。

lock_index:锁住的索引。

lock_space:锁住的表ID,也称为space id。

lock_page:锁住的Page id。

lock_rec:锁住的heap no(每条记录都有heap no)。

lock_data:事务锁定的主键值,若是表锁,则该值为NULL。

这次可以清晰地看到当前锁的信息,trx_id7955的事务锁信息。虽然通过INNODB_LOCKS可以查看每张表上锁的情况,但是当事务量非常大时,其中锁和等待也时常发生,这个时候就不这么容易判断。但是通过表INNODB_LOCK_WAITS,可以很直观地反映当前事务的等待,表INFORMATION_SCHEMA.INNODB_LOCK_WAITS的含义。

表字段说明:

requesting_trx_id:申请锁资源的事务ID。

requested_lock_id:申请锁的ID,由事务ID:表ID:Page no:Heap no组成。

blocking_trx_id: 阻塞的事务ID。

blocking_lock_id: 阻塞的锁的ID,由事务ID:表ID:Page no:Heap no组成。

通过上述SQL语句,用户可以清楚直观地看到哪个事务阻塞了另一个事务(也是瞬时的,一般不能够用来排查问题)。当然,这里只给出了事务和锁的ID,如果需要,用户可以根据表INNODB_TRXINNODB_LOCKSINNODB_LOCK_WAITS得到更为直观的详细信息。例如,用户可以执行如下联合查询:

PS:由于这三张表很多信息都是瞬时的,所以如果想指望这三张表来排查一些事务或锁问题,基本无望。因为当出现问题后,你再来查看的时候,那些有用信息早已消失了,比如你想查看是哪个SQL语句产生了锁操作等。我工作中就碰到过事务产生锁导致多个事务产生等待,且执行update超时的问题,但是回到这三种表查看信息,只能看见有一堆事务一直处于RUNNING状态,以及每个事务产生几个行锁,但是我最想要的SQL语句信息且没有,由于事务没有提交,在二进制日志中也没有查到相关操作记录。

另外,在MySQL 5.7中提供了sys库,其中有一张系统视图innodb_lock_waits。此视图记录的是锁等待信息,其实就是innodb_trx与innodb_locks两个表的join,通过show create view innodb_lock_waits可以查看具体信息,并且innodb_lock_waits还提供了出现锁等待时建议kill哪个线程来解决当前锁等待的问题。

可以看见查询时有warning信息,提示了INFORMATION_SCHEMA.INNODB_LOCK_WAITS和INFORMATION_SCHEMA.INNODB_LOCKS视图即将在新版本被移除。建议多使用MySQL 5.7提供的sys库中的innodb_lock_waits系统视图,就不用再去information_schema查询了。

三、2PLTwo-Phase Locking

传统RDBMS加锁的一个原则,就是2PL (二阶段锁)Two-Phase Locking。相对而言,2PL比较容易理解,说的是锁操作分为两个阶段:加锁阶段与解锁阶段,并且保证加锁阶段与解锁阶段不相交。下面,仍旧以MySQL为例,来简单看看2PLMySQL中的实现。

MySQL InnoDB锁系统阐述(一)

从上图可以看出,2PL就是将加锁/解锁分为两个完全不相交的阶段。加锁阶段:只加锁,不放锁。解锁阶段:只放锁,不加锁。

四、一致性非锁定读

一致性非锁定读是InnoDB通过多版本并发控制(MVCCmulti version concurrency control)的方式来读取当前执行时间数据库中的最近一次快照,如果读取的行正在执行DELETEUPDATE操作,这时读取操作不会等待行上锁的释放,相反,InnoDB存储引擎会去读取行的一个快照数据,如下图:

MySQL InnoDB锁系统阐述(一)

上图直观地展示了InnoDB存储引擎一致性的非锁定度,之所以称其为非锁定读,因为不需要等待访问的行上X锁的释放。快照数据是指该行之前版本的数据,该实现是通过Undo段来实现,而Undo用来在事务中回滚数据,因此快照数据本身是没有额外的开销。此外,读取快照数据是不需要上锁的,因为没有必要对历史的数据进行修改。

可以看到,非锁定读的机制大大提高了数据读取的并发性,在InnoDB存储因为默认设置下,这是默认的读取方式,即读取不会占用和等待表上的锁。但是在不同事务隔离级别下,读取的方式不同,并不是每个事务隔离级别下读取的都是一致性读。同样,即使都是使用一致性读,但是对于快照数据的定义也不相同。

通过上图,我们可以看出快照数据其实就是当前数据之前的历史版本,可能有多个版本。一个行可能又不止一个快照数据。我们称这种技术为行多版本技术。由此带来的并发控制,称之为多版本并发控制(MVCCmulti version concurrency control

READ COMMITTEDREPEATABLE READ下,InnoDB存储引擎使用非锁定的一致性读。然而,对于快照数据的定义却不相同。在READ COMMITTED事务隔离级别下,对于快照数据,非一致性读总是读取被锁定行的最新一份快照数据。在REPEATABLE READ事务隔离级别下,对于快照数据,非一致性读总是读取事务开始时的行数据版本。下面看一个列子,在一个MySQL(默认隔离级别时RR)的连接会话A执行如下事务:

# SESSION A

会话A中事务已经BEGIN,读取了id=1的数据,但是没有结束事务。这时我们再开启另一个会话B,这样可以模拟并发的情况,然后对会话B做如下操作:

# SESSION B

会话B中将id=1的行修改为id=3,但是事务同样没有提交,这样id=1的行其实加了一个X锁。这时如果再在会话A中读取id=1的数据,根据InnoDB存储引擎的特性,在RCRR事务隔离级别下,会使用非锁定的一致性读。我们回到会话A,接着上次未提交的事务,执行select * from blog.test where id = 1;的操作,这时不管是使用RC还是RR的事务隔离级别,显示的数据应该都是。

# SESSION A

因为当前id=1的数据被修改了1次,因此只有一个版本的数据。接着,在会话B中提交事务,如:

# SESSION B

当会话B提交事务后,这时再会话A中再运行select * from blog.test where id = 1;的语句,在RCRR事务隔离级别下得到的结果就不一样了。对于RC的事务隔离级别,它总是读取行的最新版本,如果行被锁定了,则读取该行版本的最新一个快照。对于RR就是读取它创建快照的那一刻的版本。所以当隔离级别等于RR时,在会话A中执行select * from blog.test where id = 1;语句还是可以查到id=1的数据,但是当隔离级别等于RC时,在会话A中执行select * from blog.test where id = 1;语句就查不到id=1的数据,因为此时id=2

另外,对于RC事务隔离级别而言,从数据库理论的角度来看,其实违反了事务ACID中的I的特性,即隔离性。

四、一致性锁定读

前面说过,在默认情况下,InnoDB存储引擎的SELECT操作使用一致性非锁定读。但是在某些情况下,我们需要显示地对数据库读取操作进行加锁以保证数据逻辑的一致性。而这要求数据库支持加锁语句,即使是对于SELECT的只读操作。InnoDB对于SELECT语句支持两种一致性锁定读操作,如:

1Select * from table_name for update

对读取的行记录加一个X锁,其他事务想在这些行上加任何锁都会被阻塞。

2Select * from table_name lock in share mode

对读取行记录加一个S锁,其他事务可以向被锁定的记录加S锁,但是对于加X锁,则会被阻塞。

对于一致性非锁定读,即使读取的行已被使用Select * from table_name for update,也是可以进行读取的。另外这两个加锁语句必须事务中使用,当事务提交了,锁也就释放了。 你可以把事务隔离级别调整为SERIALIZABLE(记得重新进入MySQL才生效),验证for update。

# SESSION A

# SESSION B

五、MVCC(多版本并发控制)

MySQL的大多数事务型存储引擎实现的都不是简单的行级锁。基于提升并发性能的考虑,它们一般都同时实现了多版本并发控制。InnoDB存储引擎,实现的是基于多版本的并发控制协议——MVCC (Multi-Version Concurrency Control) (注:与MVCC相对的,是基于锁的并发控制,Lock-Based Concurrency Control)MVCC最大的好处:读不加锁,读写不冲突。在读多写少的OLTP应用中,读写不冲突是非常重要的,极大的增加了系统的并发性能,这也是为什么现阶段,几乎所有的RDBMS,都支持了MVCC

可以认为MVCC是行级锁的一个变种,但是他在很多情况下避免了加锁操作,因此开销更低。虽然实现机制有所不同,但大都实现了非阻塞的读操作,写操作也只锁定必要的行。在MVCC并发控制中,读操作可以分成两类:快照读 (snapshot read)与当前读 (current read)。快照读,读取的是记录的可见版本 (有可能是历史版本),不用加锁。当前读,读取的是记录的最新版本,并且,当前读返回的记录,都会加上锁,保证其他事务不会再并发修改这条记录。

在一个支持MVCC并发控制的系统中,哪些读操作是快照读?哪些操作又是当前读呢?以MySQL InnoDB为例:

快照读:简单的select操作,属于快照读,不加锁。(当然,也有例外,下面会分析)

当前读:特殊的读操作,插入/更新/删除操作,属于当前读,需要加锁。

所有以上的语句,都属于当前读,读取记录的最新版本。并且,读取之后,还需要保证其他并发事务不能修改当前记录,对读取记录加锁。其中,除了第一条语句,对读取记录加S (共享锁)外,其他的操作,都加的是X (排它锁)

为什么将 插入/更新/删除 操作,都归为当前读?可以看看下面这个 更新 操作,在数据库中的执行流程:

MySQL InnoDB锁系统阐述(一)

从图中,可以看到,一个Update操作的具体流程。当Update SQL被发给MySQL后,MySQL Server会根据where条件,读取第一条满足条件的记录,然后InnoDB引擎会将第一条记录返回,并加锁 (current read)。待MySQL Server收到这条加锁的记录之后,会再发起一个Update请求,更新这条记录。一条记录操作完成,再读取下一条记录,直至没有满足条件的记录为止。因此,Update操作内部,就包含了一个当前读。同理,Delete操作也一样。Insert操作会稍微有些不同,简单来说,就是Insert操作可能会触发Unique Key的冲突检查,也会进行一个当前读。

MVCC会保存某个时间点上的数据快照。这意味着事务可以看到一个一致的数据视图,不管他们需要跑多久。这同时也意味着不同的事务在同一个时间点看到的同一个表的数据可能是不同的。如果你从来没有过种体验的话,可能理解起来比较抽象,但是随着慢慢地熟悉这种理解将会很容易。各个存储引擎对于MVCC的实现各不相同。这些不同中的一些包括乐观和悲观并发控制。我们将通过一个简化的InnoDB版本的行为来展示MVCC工作的一个侧面。

InnoDB:通过为每一行记录添加两个额外的隐藏的值来实现MVCC,这两个值一个记录这行数据何时被创建,另外一个记录这行数据何时过期(或者被删除)。但是InnoDB并不存储这些事件发生时的实际时间,相反它只存储这些事件发生时的系统版本号(system version numberSVN)。这是一个随着事务的创建而不断增长的数字。每个事务在事务开始时会记录它自己的系统版本号。每个查询必须去检查每行数据的版本号与事务的版本号是否相同。让我们来看看当隔离级别是REPEATABLE READ时这种策略是如何应用到特定的操作的:

SELECTInnoDB必须每行数据来保证它符合两个条件:

1InnoDB必须找到一个行的版本,它至少要和事务的版本一样老(也即它的版本号不大于事务的版本号)。这保证了不管是事务开始之前,或者事务创建时,或者修改了这行数据的时候,这行数据是存在的。

2)这行数据的删除版本必须是未定义的或者比事务版本要大。这可以保证在事务开始之前这行数据没有被删除。这里的不是真正的删除数据,而是标志出来的删除。真正意义的删除是在commit的时候。

符合这两个条件的行可能会被当作查询结果而返回。

INSERTInnoDB为这个新行记录当前的系统版本号。

DELETEInnoDB将当前的系统版本号设置为这一行的删除ID

UPDATEInnoDB会写一个这行数据的新拷贝,这个拷贝的版本为当前的系统版本号。它同时也会将这个版本号写到旧行的删除版本里。

这种额外的记录所带来的结果就是对于大多数查询来说根本就不需要获得一个锁。他们只是简单地以最快的速度来读取数据,确保只选择符合条件的行。这个方案的缺点在于存储引擎必须为每一行存储更多的数据,做更多的检查工作,处理更多的善后操作。

MVCC目前只能在REPEATABLE READREAD COMMITTED这两个隔离级别下工作。主要因为SQL92标准定义的有问题,SQL92定义的四种隔离级别,而MVCC的出现打破了原有SQL92的标准,实现了不用锁就可以实现并行,但新的实现又没有办法放进原有的隔离级别中。于是他们只能想办法映射回去,也就是把MVCC映射到REPEATABLE READREAD COMMITTED这两个隔离级别中,有点像放上架梁的新方法。其他两个隔离级别都和MVCC不兼容,READ UNCOMMITED不是MVCC兼容的,因为查询不能找到适合他们事务版本的行版本;它们每次都只能读到最新的版本。SERIABLABLE也不与MVCC兼容,因为读操作会锁定他们返回的每一行数据。所以也可以看出一个标准的定义并不等于对,他只是一个标准。

六、行锁算法

InnoDB存储引擎有3种行锁类型,分别是:

1Record Lock:单个行记录上的锁,防止事务间修改或删除数据。

2Gap Lock:间隙锁,锁定一个范围,但不包含记录本身,防止事务间新增数据。

3Next-Key LockGap Lock + Record Lock,锁定一个范围,并且锁定记录本身。

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

Next-Key Lock是结合了Gap LockRecord Lock的一种算法,在Next-Key Lock算法下,InnoDB对于行的查询都是采用这种锁定算法,对于不同SQL查询语句,可能设置共享的Next-key Lock和排他的Next-Key Lock

七、阻塞

因为不同锁之间的兼容性关系,所以在有些时刻,一个事务中的锁需要等待另一个事务中的锁释放它所占用的资源,而Innodb_lock_wait_timeout这个参数就是用来控制等待的超时时间的,默认值为50s,属于动态变量。另外还有一个变量innodb_rollback_on_timeout是用来设定是否在等待超时时间进行中的事务进行回滚操作。属于静态变量。

需要记住的一点,默认情况下InnoDB存储引擎不会回滚超时印发的错误异常。其实InnoDB存储引擎在大部分情况下都不会对异常进行回滚(除非死锁除外)。


如果您觉得本站对你有帮助,那么可以支付宝扫码捐助以帮助本站更好地发展,在此谢过。
喜欢 (5)or分享 (0)
关于作者:

您必须 登录 才能发表评论!