概述
开发多用户,数据库驱动的应用时,最大的一个难点是:一方面要最大程度地利用数据库的并发访问,另外一方面还要确保每个用户能以一致的方式读取和修改数据。为此就有了锁(locking)机制,同时这也是数据库系统区别于文件系统的一个关键特性。
什么是锁
锁是数据库系统区别于文件系统的一个关键特性。 锁机制用于管理对共享资源的并发访问。InnoDB存储引擎会在行级别上对表数据上锁,这固然不错。不过InnoDB存储引擎也会在数据库内部其他多个地方使用锁,从而允许对多种不同资源提供并发访问。例如,操作缓冲池中的LRU列表,删除,添加,移动LRU列表中的元素,为了保证一致性,必须有锁的接入。数据库系统使用锁是为了支持对共享资源进行并发访问,提供数据的完整性和一致性。
InnoDB存储引擎锁的实现,提供了一致性的非锁定读、行级锁支持。行级锁没有相关额外的开销,并可以同时得到并发性和一致性。
lock与latch
latch一般称为闩锁(轻量级的锁),因为其要求锁定的时间必须非常短。若持续的时间长,则应用的性能会非常差。在InnoDB存储引擎中,latch又可以分为mutex(互斥量)和rwlock(读写锁)。其目的是用来保证并发线程操作临界资源的正确性,并且通常没有死锁检测的机制。
lock的对象是事务,用来锁定的是数据库中的对象,如表、页、行。并且一般lock的对象仅在事务commit或rollback后进行释放(不同事务隔离级别释放的时间可能不同)。此外,lock,正如大多数数据库中一样,是有死锁机制的。
lock | latch | |
---|---|---|
对象 | 事务 | 线程 |
保护 | 数据库内容 | 内存数据结构 |
持续时间 | 整个事务过程 | 临界资源 |
模式 | 行锁、表锁、意向锁 | 读写量、互斥量 |
死锁 | 通过wait-for graph、time out等机制进行死锁检测与处理 | 无死锁检测与处理机制。仅通过应用程序加锁的顺序保证无死锁的情况发生 |
存在于 | Lock Manager的哈希表中 | 每个数据结构的对象中 |
对于InnoDB存储引擎中的latch,可以通过命令SHOW ENGINE INNODB MUTEX来进行查看。
InnoDB存储引擎中的锁
锁的类型
InnoDB存储引擎实现了如下两种标准的行级锁:
- 共享锁(S Lock),允许事务读一行数据。
- 排它锁(X Lock),允许事务删除或更新一行数据。
如果一个事务T1已经获得了行r的共享锁,那么另外的事务T2可以立即获得行r的共享锁,因为读取并没有改变行r的数据,称这种情况为锁兼容(Lock Compatible)。但若有其他的事务T3想获得行r的排它锁,则其必须等待事务T1、T2释放行r上的共享锁-这种情况称为锁不兼容。
X | S | |
---|---|---|
X | 不兼容 | 不兼容 |
S | 不兼容 | 兼容 |
从表中可以发现X锁与任何的锁都不兼容,而S锁仅和S锁兼容。需要特别注意的是,S和X锁都是行锁,兼容是指对同一记录(row)锁的兼容性情况。
此外,InnoDB存储引擎支持多粒度锁定,这种锁定允许事务在行级上的锁和表级上的锁同时存在。为了支持在不同粒度上进行加锁操作,InnoDB存储引擎支持一种额外的锁方式,称之为意向锁(Intention Lock)。意向锁是将锁定的对象分为多个层次,意向锁意味着事务希望在更细的粒度上进行加锁。若将上锁的对象看成一棵树,那么最下层的对象上锁,也就是对最细粒度的对象进行上锁,那么首先需要对粗粒度的对象上锁。如果需要对页上的记录r进行上X锁,那么分别需要对数据库A、表、页上意向锁IX,最后对记录r上X锁。若其中任何一个部分导致等待,那么该操作需要等待粗粒度锁的完成。举例来说,在对记录r加X锁之前,已经有事务对表1进行了S表锁,那么表1上已存在S锁,之后事务需要对记录r在表1上加上IX,由于不兼容,所以该事务需要等待表锁操作的完成。
InnoDB存储引擎支持意向锁设计比较简练,其意向锁即为表级别的锁。设计目的主要是为了在一个事务中揭示下一行将被请求的锁类型。其支持两种意向锁。
- 意向共享锁(IS Lock),事务想要获得一张表中某几行的共享锁。
- 意向排它锁(IX Lock),事务想要获得一张表中某几行的排它锁。
由于InnoDB存储引擎支持的是行级别的锁,因此意向锁其实不会阻塞除全表扫以外的任何请求。故表级意向锁与行级锁的兼容性如下表。
IS | IX | S | X | |
---|---|---|---|---|
IS | 兼容 | 兼容 | 兼容 | 不兼容 |
IX | 兼容 | 兼容 | 不兼容 | 不兼容 |
S | 兼容 | 不兼容 | 兼容 | 不兼容 |
X | 不兼容 | 不兼容 | 不兼容 | 不兼容 |
字段名 | 说明 |
---|---|
trx_id | InnoDB存储引擎内部唯一的事务ID |
trx_state | 当前事务的状态 |
trx_started | 事务的开始时间 |
trx_requested_lock_id | 等待事务的锁ID |
trx_wait_started | 事务等待开始的时间 |
trx_weight | 事务的权重,反映了一个事务修改和锁住的行数。在InnoDB存储引擎中,当发生死锁需要回滚时,InnoDB存储引擎会选择该值最小的进行回滚 |
trx_mysql_thread_id | MySQL中的线程ID,SHOW PROCESSLIST显示的结果 |
trx_query | 事务运行的SQL语句 |
一致性非锁定读
一致性非锁定读(consistent nonlocking read)是指InnoDB存储引擎通过行多版本控制(multi version)的方式来读取当前执行时间数据库中行的数据。如果读取的行正在执行DELETE或UPDATE操作,这时读取操作不会因此去等待行上锁的释放。相反地,InnoDB存储引擎会读取行的一个快照数据。
InnoDB存储引擎一致性的非锁定读。之所以称其为非锁定读,因为不需要等待访问的行上X锁的释放。快照数据是指该行的之前版本的数据,该实现是通过undo段来完成的。而undo用来在事务中回滚数据,因此快照数据本身是没有额外的开销。此外,读取快照数据是不需要上锁的,因为没有事务需要对历史的数据进行修改操作。
可以看到,非锁定读机制极大地提高了数据库的并发性。在InnoDB存储引擎的默认设置下,这是默认的读取方式,即读取不会占用和等待表上的锁。但是在不同事务隔离级别下,读取的方式不同,并不是在每个事务隔离级别下都是采用非锁定的一致性读。此外,即使都是使用非锁定的一致性读,但是对于快照数据的定义也各不相同。
快照数据其实就是当前行数据之前的历史版本,每行记录可能有多个版本。一个行记录可能有不止一个快照数据,一般成这种技术为行多版本技术。由此带来的并发控制,称之为多版本并发控制(Multi Version Concurrency Control,MVCC)。
在事务隔离级别READ COMMITTED和REPETABLE READ(InnoDB存储引擎的默认事务隔离级别)下,InnoDB存储引擎使用非锁定的一致性读。然而,对于快照数据的定义却不相同。在READ COMMITTED事务隔离级别下,对于快照数据,非一致性读总是读取被锁定行的最新一份快照数据。而在REPETABLE READ事务隔离级别下,对于快照数据,非一致性读总是读取事务开始时的行数据版本。
一致性锁定读
在默认配置下,即事务的隔离级别为REPETABLE READ模式下,InnoDB存储引擎的SELECT操作使用一致性非锁定读。但是在某些情况下,用户需要显示地对数据库读取操作进行加锁以保证数据逻辑的一致性。而这要求数据库支持加锁语句,即使是对于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,也是可以进行读取的,这和之前讨论的情况一样。此外,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的大数据量的插入会影响插入的性能,因为另一个事务中的插入会被阻塞。
从MySQL5.1.22版本开始,InnoDB存储引擎中提供了一种轻量级互斥量的自增长实现机制,这种机制大大提高了自增长值插入的性能。并且从该版本开始,InnoDB存储引擎提供了一个参数innodb_autoinc_lock_mode来控制自增长的模式,该模式的默认值为1
插入类型 | 说明 |
---|---|
insert-like | insert-like指所有的插入语句,如INSERT、REPLACE、INSERT···SELECT,REPLACE···SELECT、LOAD DATA等 |
simple inserts | simple inserts指能在插入前就确定插入行数的语句。这些语句包括INSERT、REPLACE等。需要注意的是:simple inserts不包含INSERT···ON DUPLICATE KEY UPDATE这类SQL语句 |
bulk inserts | buld inserts指在插入前不能确定得到插入行数的语句,如INSERT···SELECTREPLACE···SELECT,LOAD DATA |
mixed-mode inserts | mixed-mode inserts指插入中有一部分的值是自增长的,有一部分是确定的。 |
innodb_autoinc_lock_mode | 说明 |
---|---|
0 | 这是MySQL5.1.22版本之前自增长的实现方式,即通过表锁的AUTO_INC Locking方式 |
1 | 这是该参数的默认值。对于simple inserts,该值会用互斥量去对内存中的计数器进行累加的操作。对于bulk inserts,还是使用传统表锁的AUTO-INC Locking方式。在这种配置下,如果不考虑回滚操作,对于自增列的增长还是连续的。并且在这种方式下,statement-base方式的replication还是能很好地工作。需要注意的是,如果已经使用AUTO-INC Locking方式去产生自增长的值,而这时需要再进行simple inserts的操作时,还是需要等到AUTO-INC Locking的释放 |
2 | 在这个模式下,对于所有INSERT-like自增长的值的产生都是通过互斥量,而不是AUTO-INC Locking的方式。显然,这是性能最高的方式。然而,这会带来一定的问题。因为并发插入的存在,在每次插入时,自增长的值可能不是连续的。此外,最重要的是,基于statement-base replication会出现问题。因此,使用这个模式,任何时候都应该使用row-base replication。这样才能保证最大的并发性能及replication主从数据的一致。 |
另外,在InnoDB存储引擎中,自增长的列必须是索引,同时必须是索引的第一个列。如果不是第一个列,则MySQL数据库会抛出异常,而MyISAM存储引擎没有这个问题。
锁的算法
行锁的3种算法
InnoDB存储引擎有3种行锁的算法,其分别是:
- Record Lock:单个行记录上的锁。
- Gap Lock:间隙锁,锁定一个范围,但不包含记录本身。
- Next-Key Lock:Gap Lock+Record Lock,锁定一个范围,并且锁定记录本身。
Record Lock总是会去锁住索引记录,如果InnoDB存储引擎表在建立的时候没有设置任何一个索引,那么这时InnoDB存储引擎会使用隐式的主键来进行锁定。
Next-key是结合了Gap Lock和Record Lock的一种锁定算法,在Next-Key Lock算法下,InnoDB对于行的查询都是采用这种锁定算法。
采用Next-Key Lock的锁定技术称为Next-Key Locking。其设计的目的是为了解决Phantom Problem。而利用这种锁技术,锁定的不是单个值,而是一个范围,是谓词锁(Predict Lock)的一种改进。
然而当查询的索引含有唯一属性时,InnoDB存储引擎会对Next-Key Lock进行优化,将其降级为Record Lock,即仅锁住索引本身,而不是范围。
最后需再次提醒的是,对于唯一键值的锁定,Next-Key Lock降级为Record Lock仅存在于查询所有的唯一索引列。若唯一索引由多个列组成,而查询仅是查找多个唯一索引列其中的一个,那么查询其实是range类型查询,而不是point类型查询,故InnoDB存储引擎依然使用Next-Key Lock进行锁定。
时间 | 会话A | 会话B |
---|---|---|
1 | BEGIN; | |
2 | SELECT * FROM t WHERE a = 5 FOR UPDATE; | |
3 | BEGIN; | |
4 | INSERT INTO t SELECT 4; | |
5 | COMMIT;成功,不需要等待 | |
6 | COMMIT |
表t共有1、2、5三个值。在上面的例子中,在会话A中首先对a=5进行X锁定。而由于a是主键且唯一,因此锁定的仅是5这个值,而不是(2,5)这个范围,这样在会话B中插入值4而不会阻塞,可以立即插入并返回。即锁定由Next-Key Lock算法降级为了Record Lock,从而挺高应用的并发性。
正如前面介绍的,Next-key Lock降级为Record Lock仅在查询的列是唯一索引的情况下。若是辅助索引,则情况会完全不同。同样,若在会话A中执行下面的SQL语句:
CREEATE TABLE z (a INT , b INT, PRIMARY KEY(a),KEY(b));
INSERT INTO z SELECT 1,1;
INSERT INTO z SELECT 3,1;
INSERT INTO z SELECT 5,3;
INSERT INTO z SELECT 7,6;
INSERT INTO z SELECT 10,8;
SELECT * FROM t WHERE b = 3 FOR UPDATE;
很明显,这时SQL语句通过索引列b进行查询,因此其使用传统的Next-key Locking技术加锁,并且由于有两个索引,其需要分别进行锁定。对于聚集索引,其仅对列a等于5的索引加上Record Lock。而对于辅助索引,其加上的是Next-key Lock,锁定的范围是(1,3),特别需要注意的是,InnoDB存储引擎还会对辅助索引下一个键值加上gap lock,即还有一个辅助索引范围为(3,6)的锁。因此,若新的会话B中运行下面的SQL语句,都会被阻塞。
SELECT * FROM z WHERE a = 5 LOCK IN SHARE MODE;
INSERT INTO z SELECT 4,2;
INSERT INTO z SELECT 6,5;
第一个SQL不能执行,因为会话A中执行的SQL语句已经对聚集索引中列a=5的值加上x锁,因此执行会被阻塞。第二个SQL语句,主键插入4,没有问题,但是插入的辅助索引值2在锁定范围(1,3)中,因此执行同样会被阻塞。第三个SQL语句,插入的主键6没有被锁定,5也不在范围(1,3)之间。但插入的值5在另一个锁定范围(3,6)中,故同样需要等待。而下面的SQL语句,不会被阻塞,可以立即执行:
INSERT INTO z SELECT 8,6;
INSERT INTO z SELECT 2,0;
INSERT INTO z SELECT 6,7;
从上面的例子中可以看到,Gap Lock的作用是为了阻止多个事务将记录插入到同一范围内,而这会导致Phantom Problem问题的产生。例如在上面的例子中,会话A中用户已经锁定了b=3的记录。若此时没有Gap Lock锁定(3,6),那么用户可以插入索引b列为3的记录,这会导致会话A中的用户再次执行同样的查询时会返回不同的记录,即导致Phantom Problem问题的产生。
用户可以通过一下两种方式来显示地关闭Gap Lock:
- 将事务的隔离级别设置为READ COMMITTED
- 将参数innodb_locks_unsafe_for_binlog设置为1
在InnoDB存储引擎中,对于INSERT的操作,其会检查插入记录的下一条记录是否被锁定,若已经被锁定,则不允许查询。对于上面的例子,会话A已经锁定了表z中b=3的记录,即已经锁定了(1,3)的范围,这时若在其他会话中进行如下的插入同样会导致阻塞:
INSERT INTO z SELECT 2,2;
因为在辅助索引列b上插入值为2的记录时,会监测到下一个记录3已经被索引,而将插入修改为如下的值,可以立即执行:
INSERT INTO SELECT 2,0;
最后需再次提醒的是,对于唯一键值的锁定,Next-key Lock降级为Record Lock仅存在于查询所有的唯一索引列。若唯一索引由多个列组成,而查询仅是查找多个唯一索引列中的其中一个,那么查询其实是range类型查询,而不是point查询,故InnoDB存储引擎依然会使用Next-key Lock进行锁定。
解决Phantom Problem
在默认的事务隔离级别下,即REPETABLE READ下,InnoDB存储引擎采用Next-Key Locking机制来避免Phantom Problem。
Phantom Problem是指在同一事务下,连续执行两次同样的SQL语句可能导致不同的结果,第二次的SQL语句可能会返回之前不存在的行。下面将演示这个例子,使用前面所创建的表t。表t由1,2,5这三个值组成,若这时事务T1执行如下的SQL语句:
SELECT * FROM t WHERE a > 2 FOR UPDATE;
注意这时事务T1并没有进行提交操作,上述应该返回5这个结果。若与此同时,另一个事务T2插入了4这个值,并且数据库允许该操作,那么事务T1再次执行上述SQL语句会得到4和5.这与第一次得到的结果不同,违反了事务的隔离性,即当前事务能够看到其他事务的结果。
InnoDB存储引擎采用Next-Key Locking的算法避免了Phantom Problem。对于上述的SQL语句SELECT * FROM t WHERE a > 2 FOR UPDATE,其锁住的不是5这单个值,而是对(2,+∞)这个范围加了X锁。因此任何对于这个范围的插入都是不被允许的,从而避免了Phantom Problem。
InnoDB存储引擎默认的事务隔离级别是REPETABLE READ,在该隔离级别下,其采用Next-Key Locking的方式来加锁。而在事务隔离级别READ COMMITTED下,其仅采用Record Lock。
锁问题
脏读
在理解脏读(Dirty Read)之前,需要理解脏数据的概念。但是脏数据和之前所介绍的脏页完全是不同的概念。脏页指的是在缓冲池中已经被修改的页,但是还没有刷新到磁盘中,即数据库实例内存中的页和磁盘中的页的数据是不一致的,当然在刷新到磁盘之前,日志都已经被写入到了重做日志文件中。而所谓脏数据是指事务对缓冲池中行记录的修改,并且还没有被提交(commit)。
对于脏页的读取,是非常正常的。脏页是因为数据库实例内存和磁盘的异步造成的,这并不影响数据的一致性。并且因为脏页的刷新是异步的,不影响数据库的可用性,因此可以带来性能的提高。
脏数据却截然不同,脏数据是指未提交的数据,如果读到了脏数据,即一个事务可以读到另外一个事务未提交的数据,则显然违反了数据库的隔离性。
脏读指的就是在不同的事务下,当前事务可以读到另外事务未提交的数据,简单说就是可以读到脏数据。
不可重复读
不可重复读是指在一个事务内多次读取同一数据集合。在这个事务还没有结束时,另外一个事务也访问该同一数据集合,并做了DML操作、因此,在第一个事务中的两次读数据之间,由于第二个事务的修改,那么第一个事务两次读取到的数据可能是不一样的。这样就发生了在一个事务内两次读到的数据是不一样的情况,这种情况称为不可重复读。
不可重复读和脏读的区别是,脏读是读到未提交的数据,而不可重复读读到的却是已经提交的数据,但是违反了数据库事务一致性的要求。
Time | 会话A | 会话B |
---|---|---|
1 | SET@@tx_isolation=‘read-committed’; | |
2 | SET@@tx_isolation=‘read-committed’; | |
3 | BEGIN | BEGIN |
4 | SELECT * FROM t | |
5 | INSERT INTO t SELECT 2; | |
6 | COMMIT; | |
7 | SELECT * FROM t |
在会话A中开始一个事务,第一次读取到的记录是1,在另一个事务B中开始了另一个事务,插入一条为2的记录,在没有提交之前,对会话A中的事务进行再次读取时,读到的记录还是1,没有发生脏读现象。但会话B中的事务提交后,在对会话A中的事务进行读取时,这时读到的是1和2两条记录。这个例子的前提是,在事务开始前,会话A和会话B的事务隔离级别都调整为READ COMMITTED。
一般来说,不可重复读的问题是可以接受的,因为其读到的是已经提交的数据,本身并不会带来很大的问题。
在InnoDB存储引擎中,通过使用Next-Key Lock算法来避免不可重复读的问题。在MySQL官方文档中将不可重复读的问题定义为Phantom Problem,即幻象问题。在Next-Key Lock算法下,对于索引的扫描,不仅是锁住扫描到的索引,而且还锁住这些索引覆盖的范围(gap)。因此在这个范围内的插入都是不允许的。这样就避免了另外的事务在这个范围内插入数据导致的不可重复读的问题。因此,InnoDB存储引擎的默认事务隔离级别是REPETABLE READ,采用Next-Key Lock算法,避免了不可重复读的现象。
丢失更新
丢失更新是另一个锁导致的问题,简单来说其就是一个事务的更新操作会被另一个事务的更新操作所覆盖,从而导致数据的不一致。例如:
1、事务T1将行记录r更新为v1,但是事务T1并未提交。
2、与此同时,事务T2将行记录r更新为v2,事务T2提交。
3、事务T1提交。
4、事务T2提交。
但是,在当前数据库的任何隔离级别下,都不会导致数据库理论意义上的丢失更新问题。这是因为,即使是READ UNCOMMITTED的事务隔离级别,对于行的DML操作,需要对行或其他粗粒度级别的对象加锁。
因此在上述步骤2中,事务T2并不能对行记录r进行更新操作,其会被阻塞,直到事务T1提交。
阻塞
因为不同锁之间的兼容性关系,在有些时刻一个事务中的锁需要等待另一个事务中的锁释放它所占用的资源,这就是阻塞。阻塞并不是一件坏事,其是为了确保事务可以并发且正常地运行。
在InnoDB存储引擎中,参数innodb_lock_wait_timeout用来控制等待的时间(默认是50秒),innodb_rollback_on_timeout用来设定是否在等待超时时对进行中的事务进行回滚操作。参数innodb_lock_wait_timeout是动态的,可以在MySQL数据库运行时进行调整。
需要牢记的是,在默认情况下InnoDB存储引擎不会回滚超时引发的错误异常。其实InnoDB存储引擎在大部分情况下都不会对异常进行回滚。
死锁
死锁的概念
死锁是指两个或两个以上的事务在执行过程中,因争夺锁资源而造成的一种互相等待的现象。若无外力作用,事务都将无法推进下去。解决死锁问题最简单的方式是不要有等待,将任何的等待都转化为回滚,并且事务重新开始。毫无疑问,这的确可以避免死锁问题的产生。然后在线上环境中,这可能导致并发性能下降,甚至任何一个事务都不能进行,而这所带来的问题远比死锁问题更为严重,因为这很难被发现并且浪费资源。
解决死锁问题最简单的一种方法是超时,即当两个事务互相等待时,当一个等待时间超过设置的某一阈值时,其中一个事务进行回滚,另一个等待的事务就能继续进行。在InnoDB存储引擎中,参数innodb_lock_wait_timeout用来设置超时时间。
超时机制虽然简单,但是其仅通过超时后对事务进行回滚的方式来处理,或者说其是根据FIFO的顺序选择回滚对象。但若超时的事务所占权重比较大,如事务操作更新了很多行,占用了较多的undo log,这时采用FIFO的方式,就显得不合适了,因为回滚这个事务的时间相对另一个事务所占用的时间可能会很多。
因此,除了超时机制,当前数据库还都普遍采用了wait-for graph(等待图)的方式来进行死锁检测。较之超时的解决方案,这是一种更为主动的死锁检测方式。InnoDB存储引擎也采用的这种方式。wait-for graph要求数据库保存以下两种信息:
- 锁的信息链表
- 事务等待链表
通过上述链表可以构造出一张图,而在这个图中若存在回路,就代表存在死锁,因此资源间相互发生等待。在wait-for graph中,事务为图中的节点。而在图中,事务T1指向T2边的定义为:
- 事务T1等待事务T2所占用的资源。
- 事务T1最终等待T2所占用的资源,也就是事物之间在等待相同的资源,而事务T1发生在事务T2的后面。
wait-for graph是一种较为主动的死锁检测机制,在每个事务请求锁发生等待时都会判断是否存在回路,若存在则有死锁,通常来说InnoDB存储引擎选择回滚undo量最小的事务。
wait-for graph的死锁检测通常采用深度优先的算法实现,在InnoDB1.2版本之前,都是采用递归方式实现。而从1.2版本开始,对wait-for graph的死锁检测进行了优化,将递归用非递归的方式实现,从而进一步提高了InnoDB存储引擎的性能。
锁升级
InnoDB存储引擎不存在锁升级的问题。因为其不是根据每个记录来产生行锁的,相反,其根据每个事务访问的每个页对锁进行管理的,采用的是位图的方式。因此不管一个事务锁住页中一个记录还是多个记录,其开销通常都是一致的。
InnoDB存储引擎不存在锁升级的问题。因为其不是根据每个记录来产生行锁的,相反,其根据,每个事务访问的每个页对锁进行管理的,采用的是位图的方式。因此不管一个事务锁住页还是多个记录,其开销通常都是一致的。
假设一张表有3000000个数据页,每个页大约有100条记录,那么总共有300000000条记录。若有一个事务执行全表更新的SQL语句,则需要对所有记录加X锁。若根据每行记录产生锁对象进行加锁,并且每个锁占用10字节,则仅对锁管理就需要差不多3GB的内存。而InnoDB存储引擎根据页进行加锁,并采用位图的方式,假设每个页存储的锁信息占用30个字节,则锁对象仅需90M内存。由此可见两者对于锁资源开销的差距之大。