锁定容许事务在行级上的锁和表级上的锁同时存在。为了支持在不一样粒度上进行加锁操做,InnoDB存储引擎支持一种额外的锁方式html
意向共享锁(IS Lock)事务想要得到一张表中某几行的共享锁mysql
意向排他锁(IX Lock)事务想要得到一张表中某几行的排他锁算法
因为InnoDB存储引擎支持的是行级别的锁,所以意向锁其实不会阻塞除全表扫之外的任何请求。故表级意向锁与行级锁的兼容性以下所示sql
若将上锁的对象当作一棵树,那么对最下层的对象上锁,也就是对最细粒度的对象进行上锁,那么首先须要对粗粒度的对象上锁。例上图,若是须要对页上的记录r进行上X锁,那么分别须要对数据库A、表、页上意向锁IX,最后对记录r上X锁。若其中任何一个部分致使等待,那么该操做须要等待粗粒度锁的完成。举例来讲,在对记录r加X锁以前,已经有事务对表1进行了S表锁,那么表1上已存在S锁,以后事务须要对记录r在表1上加上IX,因为不兼容,因此该事务须要等待表锁操做的完成。数据库
innodb的意向锁主要用户多粒度的锁并存的状况。好比事务A要在一个表上加S锁,若是表中的一行已被事务B加了X锁,那么该锁的申请也应被阻塞。若是表中的数据不少,逐行检查锁标志的开销将很大,系统的性能将会受到影响。为了解决这个问题,能够在表级上引入新的锁类型来表示其所属行的加锁状况,这就引出了“意向锁”的概念。架构
举个例子,若是表中记录1亿,事务A把其中有几条记录上了行锁了,这时事务B须要给这个表加表级锁,若是没有意向锁的话,那就要去表中查找这一亿条记录是否上锁了。若是存在乎向锁,那么假如事务A在更新一条记录以前,先加意向锁,再加X锁,事务B先检查该表上是否存在乎向锁,存在的意向锁是否与本身准备加的锁冲突,若是有冲突,则等待直到事务A释放,而无须逐条记录去检测。事务B更新表时,其实无须知道到底哪一行被锁了,它只要知道反正有一行被锁了就好了。并发
主要做用是处理行锁和表锁之间的矛盾,可以显示“某个事务正在某一行上持有了锁,或者准备去持有锁”post
Record Lock:单个行记录上的锁性能
Gap Lock:间隙锁,锁定一个范围,但不包含记录自己优化
Next-Key Lock∶Gap Lock+Record Lock,锁定一个范围、索引之间的间隙,而且锁定记录自己;目的是为了防止幻读
多版本并发控制 MVCC,是行级锁的一个变种,经过保存数据在某个时间节点的快照(snapshot),相似实现了行级锁。由此不一样事务对同一表,同一时刻看到的数据多是不同的。 实现上经过在不一样的数据行后增长建立日期版本号和删除日期版本号,且版本号不断递增,进而实现了数据快照
下面两条简单的SQL,他们加什么锁?
select * from t1 where id = 10
delete from t1 where id = 10
若是要分析加锁状况,必须还要知道如下的一些前提,前提不一样,加锁处理的方式也不一样
根据上述状况,有如下几种组合
排列组合尚未列举彻底,可是看起来,已经不少了。真的有必要这么复杂吗?事实上,要分析加锁,就是须要这么复杂。可是从另外一个角度来讲,只要你选定了一种组合,SQL须要加哪些锁,其实也就肯定了。接下来挑几个比较经典的组合
这个组合,是最简单,最容易分析的组合。id是主键,Read Committed隔离级别,给定SQL:delete from t1 where id = 10; 只须要将主键上,id = 10的记录加上X锁便可。以下图所示:
结论:id是主键时,此SQL只须要在id=10这条记录上加X锁便可。
这个组合,id不是主键,而是一个Unique的二级索引键值。那么在RC隔离级别下,delete from t1 where id = 10; 须要加什么锁呢?见下图:
此组合中,id是unique索引,而主键是name列。此时,加锁的状况因为组合一有所不一样。因为id是unique索引,所以delete语句会选择走id列的索引进行where条件的过滤,在找到id=10的记录后,首先会将unique索引上的id=10索引记录加上X锁,同时,会根据读取到的name列,回主键索引(聚簇索引),而后将聚簇索引上的name = ‘d’ 对应的主键索引项加X锁。为何聚簇索引上的记录也要加锁?试想一下,若是并发的一个SQL,是经过主键索引来更新:update t1 set id = 100 where name = ‘d’; 此时,若是delete语句没有将主键索引上的记录加锁,那么并发的update就会感知不到delete语句的存在,违背了同一记录上的更新/删除须要串行执行的约束。
结论:若id列是unique列,其上有unique索引。那么SQL须要加两个X锁,一个对应于id unique索引上的id = 10的记录,另外一把锁对应于聚簇索引上的[name='d',id=10]的记录。
相对于组合1、二,组合三又发生了变化,隔离级别仍旧是RC不变,可是id列上的约束又下降了,id列再也不惟一,只有一个普通的索引。假设delete from t1 where id = 10; 语句,仍旧选择id列上的索引进行过滤where条件,那么此时会持有哪些锁?一样见下图:
根据此图,能够看到,首先,id列索引上,知足id = 10查询条件的记录,均已加锁。同时,这些记录对应的主键索引上的记录也都加上了锁。与组合二惟一的区别在于,组合二最多只有一个知足等值查询的记录,而组合三会将全部知足查询条件的记录都加锁。
结论:若id列上有非惟一索引,那么对应的全部知足SQL查询条件的记录,都会被加锁。同时,这些记录在主键索引上的记录,也会被加锁。
相对于前面三个组合,这是一个比较特殊的状况。id列上没有索引,where id = 10;这个过滤条件,无法经过索引进行过滤,那么只能走全表扫描作过滤。对应于这个组合,SQL会加什么锁?或者是换句话说,全表扫描时,会加什么锁?这个答案也有不少:有人说会在表上加X锁;有人说会将聚簇索引上,选择出来的id = 10;的记录加上X锁。那么实际状况呢?请看下图:
因为id列上没有索引,所以只能走聚簇索引,进行所有扫描。从图中能够看到,知足删除条件的记录有两条,可是,聚簇索引上全部的记录,都被加上了X锁。不管记录是否知足条件,所有被加上X锁。既不是加表锁,也不是在知足条件的记录上加行锁。
有人可能会问?为何不是只在知足条件的记录上加锁呢?这是因为MySQL的实现决定的。若是一个条件没法经过索引快速过滤,那么存储引擎层面就会将全部记录加锁后返回,而后由MySQL Server层进行过滤。所以也就把全部的记录,都锁上了。
注:在实际的实现中,MySQL有一些改进,在MySQL Server过滤条件,发现不知足后,会调用unlock_row方法,把不知足条件的记录放锁 (违背了2PL的约束)。这样作,保证了最后只会持有知足条件记录上的锁,可是每条记录的加锁操做仍是不能省略的。
结论:若id列上没有索引,SQL会走聚簇索引的全扫描进行过滤,因为过滤是由MySQL Server层面进行的。所以每条记录,不管是否知足条件,都会被加上X锁。可是,为了效率考量,MySQL作了优化,对于不知足条件的记录,会在判断后放锁,最终持有的,是知足条件的记录上的锁,可是不知足条件的记录上的加锁/放锁动做不会省略。同时,优化也违背了2PL的约束。
上面的四个组合,都是在Read Committed隔离级别下的加锁行为,接下来的四个组合,是在Repeatable Read隔离级别下的加锁行为。
组合五,id列是主键列,Repeatable Read隔离级别,针对delete from t1 where id = 10; 这条SQL,加锁与组合一:[id主键,Read Committed]一致。
与组合五相似,组合六的加锁,与组合二:[id惟一索引,Read Committed]一致。两个X锁,id惟一索引知足条件的记录上一个,对应的聚簇索引上的记录一个。
还记得前面提到的MySQL的四种隔离级别的区别吗?RC隔离级别容许幻读,而RR隔离级别,不容许存在幻读。可是在组合5、组合六中,加锁行为又是与RC下的加锁行为彻底一致。那么RR隔离级别下,
组合七,Repeatable Read隔离级别,id上有一个非惟一索引,执行delete from t1 where id = 10; 假设选择id列上的索引进行条件过滤,最后的加锁行为,是怎么样的呢?一样看下面这幅图:
此图,相对于组合三:[id列上非惟一锁,Read Committed]看似相同,其实却有很大的区别。最大的区别在于,这幅图中多了一个GAP锁,并且GAP锁看起来也不是加在记录上的,倒像是加载两条记录之间的位置,GAP锁有何用?
其实这个多出来的GAP锁,就是RR隔离级别,相对于RC隔离级别,不会出现幻读的关键。确实,GAP锁锁住的位置,也不是记录自己,而是两条记录之间的GAP。
如何保证两次当前读返回一致的记录,那就须要在第一次当前读与第二次当前读之间,其余的事务不会插入新的知足条件的记录并提交。为了实现这个功能,GAP锁应运而生。
如图中所示,有哪些位置能够插入新的知足条件的项 (id = 10),考虑到B+树索引的有序性,知足条件的项必定是连续存放的。记录[6,c]以前,不会插入id=10的记录;[6,c]与[10,b]间能够插入[10, aa];[10,b]与[10,d]间,能够插入新的[10,bb],[10,c]等;[10,d]与[11,f]间能够插入知足条件的[10,e],[10,z]等;而[11,f]以后也不会插入知足条件的记录。所以,为了保证[6,c]与[10,b]间,[10,b]与[10,d]间,[10,d]与[11,f]不会插入新的知足条件的记录,MySQL选择了用GAP锁,将这三个GAP给锁起来。
Insert操做,如insert [10,aa],首先会定位到[6,c]与[10,b]间,而后在插入前,会检查这个GAP是否已经被锁上,若是被锁上,则Insert不能插入记录。所以,经过第一遍的当前读,不只将知足条件的记录锁上 (X锁),与组合三相似。同时仍是增长3把GAP锁,将可能插入知足条件记录的3个GAP给锁上,保证后续的Insert不能插入新的id=10的记录,也就杜绝了同一事务的第二次当前读,出现幻象的状况。
有心的朋友看到这儿,能够会问:既然防止幻读,须要靠GAP锁的保护,为何组合5、组合六,也是RR隔离级别,却不须要加GAP锁呢?
首先,这是一个好问题。其次,回答这个问题,也很简单。GAP锁的目的,是为了防止同一事务的两次当前读,出现幻读的状况。而组合五,id是主键;组合六,id是unique键,都可以保证惟一性。一个等值查询,最多只能返回一条记录,并且新的相同取值的记录,必定不会在新插入进来,所以也就避免了GAP锁的使用。其实,针对此问题,还有一个更深刻的问题:若是组合5、组合六下,针对SQL:select * from t1 where id = 10 for update; 第一次查询,没有找到知足查询条件的记录,那么GAP锁是否还可以省略?此问题留给你们思考。
结论:Repeatable Read隔离级别下,id列上有一个非惟一索引,对应SQL:delete from t1 where id = 10; 首先,经过id索引定位到第一条知足查询条件的记录,加记录上的X锁,加GAP上的GAP锁,而后加主键聚簇索引上的记录X锁,而后返回;而后读取下一条,重复进行。直至进行到第一条不知足条件的记录[11,f],此时,不须要加记录X锁,可是仍旧须要加GAP锁,最后返回结束。
何时会取得gap lock或nextkey lock 这和隔离级别有关,只在REPEATABLE READ或以上的隔离级别下的特定操做才会取得gap lock或nextkey lock。
组合八,Repeatable Read隔离级别下的最后一种状况,id列上没有索引。此时SQL:delete from t1 where id = 10; 没有其余的路径能够选择,只能进行全表扫描。最终的加锁状况,以下图所示:
如图,这是一个很恐怖的现象。首先,聚簇索引上的全部记录,都被加上了X锁。其次,聚簇索引每条记录间的间隙(GAP),也同时被加上了GAP锁。这个示例表,只有6条记录,一共须要6个记录锁,7个GAP锁。试想,若是表上有1000万条记录呢?
在这种状况下,这个表上,除了不加锁的快照度,其余任何加锁的并发SQL,均不能执行,不能更新,不能删除,不能插入,全表被锁死。
固然,跟组合四:[id无索引, Read Committed]相似,这个状况下,MySQL也作了一些优化,就是所谓的semi-consistent read。semi-consistent read开启的状况下,对于不知足查询条件的记录,MySQL会提早放锁。针对上面的这个用例,就是除了记录[d,10],[g,10]以外,全部的记录锁都会被释放,同时不加GAP锁。semi-consistent read如何触发:要么是read committed隔离级别;要么是Repeatable Read隔离级别,同时设置了innodb_locks_unsafe_for_binlog 参数。更详细的关于semi-consistent read的介绍,可参考我以前的一篇博客:MySQL+InnoDB semi-consitent read原理及实现分析 。
结论:在Repeatable Read隔离级别下,若是进行全表扫描的当前读,那么会锁上表中的全部记录,同时会锁上聚簇索引内的全部GAP,杜绝全部的并发 更新/删除/插入 操做。固然,也能够经过触发semi-consistent read,来缓解加锁开销与并发影响,可是semi-consistent read自己也会带来其余问题,不建议使用。
针对前面提到的简单的SQL,最后一个状况:Serializable隔离级别。对于SQL2:delete from t1 where id = 10; 来讲,Serializable隔离级别与Repeatable Read隔离级别彻底一致,所以不作介绍。
Serializable隔离级别,影响的是SQL1:select * from t1 where id = 10; 这条SQL,在RC,RR隔离级别下,都是快照读,不加锁。可是在Serializable隔离级别,SQL1会加读锁,也就是说快照读不复存在,MVCC并发控制降级为Lock-Based CC。
结论:在MySQL/InnoDB中,所谓的读不加锁,并不适用于全部的状况,而是隔离级别相关的。Serializable隔离级别,读不加锁就再也不成立,全部的读操做,都是当前读。
这种状况很好理解,事务A和事务B操做两张表,但出现循环等待锁状况。
这种状况比较常见,以前遇到两个job在执行数据批量更新时,jobA处理的的id列表为[1,2,3,4],而job处理的id列表为[8,9,10,4,2],这样就形成了死锁。
这种状况比较隐晦,事务A在执行时,除了在二级索引加锁外,还会在聚簇索引上加锁,在聚簇索引上加锁的顺序是[1,4,2,3,5],而事务B执行时,只在聚簇索引上加锁,加锁顺序是[1,2,3,4,5],这样就形成了死锁的可能性。
innodb在RR级别下,以下的状况也会产生死锁,比较隐晦。不清楚的同窗能够自行根据上节的gap锁原理分析下。
从InnoDB1.0开始,在INFORMATION_SCHEMA架构下添加了表INNODB_TRX、INNODB_LOCKS、INNODB_LOCK_WAITS。(详情见附录)经过这三张表,用户能够更简单地监控当前事务并分析可能存在的锁问题。
#全局分析系统上中行锁的争夺状况
show status like 'innodb_row_lock%';
#查看事务
SELECT * FROM information_schema.INNODB_TRX;
#查看锁
SELECT * FROM information_schema.INNODB_LOCKS;
#查看锁等待状况
SELECT * FROM information_schema.INNODB_LOCK_WAITS;
#经过联合查询能够比较直观的查看哪一个事务阻塞了哪一个事务
SELECT r.trx_id '等待事务ID',
r.trx_mysql_thread_id '等待线程ID',
r.trx_query '等待事务运行语句',
b.trx_id '阻塞事务ID',
b.trx_mysql_thread_id '阻塞线程ID',
b.trx_query '阻塞事务运行语句'
FROM information_schema.innodb_lock_waits w
INNER JOIN information_schema.innodb_trx b
ON b.trx_id = w.blocking_trx_id
INNER JOIN information_schema.innodb_trx r
ON r.trx_id = w.requesting_trx_id;
复制代码
直观方法是在两个事务相互等待时,当一个等待时间超过设置的某一阀值时,对其中一个事务进行回滚,另外一个事务就能继续执行。
每当加锁请求没法当即知足须要并进入等待时,wait-for graph算法都会被触发。
wait-for graph要求数据库保存如下两种信息:
锁的信息链表
事务等待链表
经过上述链表能够构造出一张图,而在这个图中若存在回路,就表明存在死锁,所以资源间相互发生等待。在wait-for graph中,事务为图中的节点。而在图中,事务T1指向T2边的定义为:
事务T1等待事务T2所占用的资源
事务T1最终等待T2所占用的资源,也就是事务之间在等待相同的资源,而事务T1发生在事务T2的后面
示例事务状态和锁的信息
在Transaction Wait Lists中能够看到共有4个事务t一、t二、t三、t4,故在wait-for graph中应有4个节点。而事务t2对row1占用x锁,事务t1对row2占用s锁。事务t1须要等待事务t2中row1的资源,所以在wait-for graph中有条边从节点t1指向节点t2。事务t2须要等待事务t一、t4所占用的row2对象,故而存在节点t2到节点t一、t4的边。一样,存在节点t3到节点t一、t二、t4的边,所以最终的wait-for graph以下图所示。
ps:若存在则有死锁,一般来讲InnoDB存储引擎选择回滚undo量最小的事务并重新开始
列名 | 描述 |
---|---|
innodb_row_lock_current_waits | 当前正在等待锁定的数量 |
innodb_row_lock_time | 从系统启动到如今锁定总时间长度 |
innodb_row_lock_time_avg | 每次等待所花平均时间 |
innodb_row_lock_time_max | 从系统启动到如今等待最常的一次所花的时间 |
innodb_row_lock_waits | 系统启动后到如今总共等待的次数;直接决定优化的方向和策略 |
提供有关当前正在内部执行的每一个事务的信息 InnoDB,包括事务是否在等待锁定,事务什么时候启动以及事务正在执行的SQL语句(若是有)。详见dev.mysql.com/doc/refman/…
列名 | 描述 |
---|---|
TRX_ID | 事务Id |
TRX_WEIGHT | 事务的权重,反映(但不必定是确切的计数)更改的行数和事务锁定的行数。要解决死锁,请 InnoDB``选择权重最小的事务做为回滚的“ 受害者 ”。不管更改和锁定行的数量如何,已更改非事务表的事务都被认为比其余事务更重。 |
TRX_STATE | 事务执行状态。容许值是 RUNNING,LOCK WAIT , ROLLING BACK,和 COMMITTING 。 |
TRX_STARTED | 交易开始时间。 |
TRX_REQUESTED_LOCK_ID | 事务当前正在等待的锁的ID,若是TRX_STATE是LOCK WAIT ; 不然NULL``。 |
TRX_WAIT_STARTED | 交易开始等待锁定的时间,若是 TRX_STATE是LOCK WAIT ; 不然NULL``。 |
TRX_MYSQL_THREAD_ID | MySQL线程ID,与show processlist中的ID值相对应 |
TRX_QUERY | 事务正在执行的SQL语句 |
TRX_OPERATION_STATE | 交易的当前操做,若是有的话; 不然 NULL``。 |
TRX_TABLES_IN_USE | InnoDB``处理此事务的当前SQL语句时使用 的表数。 |
TRX_TABLES_LOCKED | InnoDB``当前SQL语句具备行锁定 的表的数量。(由于这些是行锁,而不是表锁,因此一般仍能够经过多个事务读取和写入表,尽管某些行被锁定。) |
TRX_LOCK_STRUCTS | 事务保留的锁数。 |
TRX_LOCK_MEMORY_BYTES | 内存中此事务的锁结构占用的总大小 |
TRX_ROWS_LOCKED | 此交易锁定的大体数字或行数。该值可能包括实际存在但对事务不可见的删除标记行 |
TRX_ROWS_MODIFIED | 此事务中已修改和插入的行数。 |
TRX_CONCURRENCY_TICKETS | 一个值,指示当前事务在被换出以前能够执行多少工做 |
TRX_ISOLATION_LEVEL | 当前事务的隔离级别。 |
TRX_UNIQUE_CHECKS | 是否为当前事务打开或关闭惟一检查。例如,在批量数据加载期间可能会关闭它们 |
TRX_FOREIGN_KEY_CHECKS | 是否为当前事务打开或关闭外键检查。例如,在批量数据加载期间可能会关闭它们 |
TRX_LAST_FOREIGN_KEY_ERROR | 最后一个外键错误的详细错误消息(若是有); 不然NULL`` |
TRX_ADAPTIVE_HASH_LATCHED | 自适应哈希索引是否被当前事务锁定。当自适应哈希索引搜索系统被分区时,单个事务不会锁定整个自适应哈希索引。自适应哈希索引分区由innodb_adaptive_hash_index_parts``,默认设置为8。 |
TRX_ADAPTIVE_HASH_TIMEOUT | 是否当即为自适应哈希索引放弃搜索锁存器,或者在MySQL的调用之间保留它。当没有自适应哈希索引争用时,该值保持为零,语句保留锁存器直到它们完成。在争用期间,它倒计时到零,而且语句在每次行查找后当即释放锁存器。当自适应散列索引搜索系统被分区(受控制 innodb_adaptive_hash_index_parts``)时,该值保持为0。 |
TRX_IS_READ_ONLY | 值为1表示事务是只读的。 |
TRX_AUTOCOMMIT_NON_LOCKING | 值为1表示事务是 SELECT](https://dev.mysql.com/doc/refman/5.7/en/select.html)不使用FOR UPDATE or或 LOCK IN SHARED MODE子句的语句,而且正在执行, [autocommit 所以事务将仅包含此一个语句。当此列和TRX_IS_READ_ONLY都为1时,InnoDB 优化事务以减小与更改表数据的事务关联的开销 |
提供有关InnoDB 事务已请求但还没有获取的每一个锁的信息,以及事务持有的阻止另外一个事务的每一个锁。详见dev.mysql.com/doc/refman/…
列名 | 描述 |
---|---|
LOCK_ID | 一个惟一的锁ID号,内部为 InnoDB``。 |
LOCK_TRX_ID | 持有锁的交易的ID |
LOCK_MODE | 如何请求锁定。容许锁定模式描述符 S,X , IS,IX , GAP,AUTO_INC ,和 UNKNOWN``。锁定模式描述符能够组合使用以识别特定的锁定模式。 |
LOCK_TYPE | 锁的类型 |
LOCK_TABLE | 已锁定或包含锁定记录的表的名称 |
LOCK_INDEX | 索引的名称,若是LOCK_TYPE是 RECORD; 不然NULL |
LOCK_SPACE | 锁定记录的表空间ID,若是 LOCK_TYPE是RECORD ; 不然NULL`` |
LOCK_PAGE | 锁定记录的页码,若是 LOCK_TYPE是RECORD ; 不然NULL``。 |
LOCK_REC | 页面内锁定记录的堆号,若是 LOCK_TYPE是RECORD ; 不然NULL``。 |
LOCK_DATA | 与锁相关的数据(若是有)。若是 LOCK_TYPE是RECORD ,是锁定的记录的主键值,不然NULL。此列包含锁定行中主键列的值,格式为有效的SQL字符串。若是没有主键,LOCK_DATA 则是惟一的InnoDB内部行ID号。若是对键值或范围高于索引中的最大值的间隙锁定,则LOCK_DATA 报告_supremum_ pseudo-record。当包含锁定记录的页面不在缓冲池中时(若是在保持锁定时将其分页到磁盘),InnoDB不从磁盘获取页面,以免没必要要的磁盘操做。相反, LOCK_DATA 设置为 NULL``。 |
包含每一个被阻止InnoDB 事务的一个或多个行,指示它已请求的锁以及阻止该请求的任何锁。详见dev.mysql.com/doc/refman/…
列名 | 描述 |
---|---|
REQUESTING_TRX_ID | 请求(阻止)事务的ID。 |
REQUESTED_LOCK_ID | 事务正在等待的锁的ID。 |
BLOCKING_TRX_ID | 阻止事务的ID。 |
BLOCKING_LOCK_ID | 由阻止另外一个事务继续进行的事务所持有的锁的ID |
《MySQL技术内幕:InnoDB存储引擎》