数据库事务需要满足ACID四个原则,"I"即隔离性,它要求两个事务互不影响,不能看到对方尚未提交的数据。数据库有4中隔离级别(isolationlevel),按照隔离性从弱到强(相应地,性能和并发性从强到弱)分别是:
"I"隔离性正是通过锁机制来实现的。提到锁就会涉及到死锁,需要明确的是死锁的可能性并不受隔离级别影响,因为隔离级别改变的是读操作的行为,而死锁是由写操作产生的。
MySQL的隔离级别比标准隔离级别提前了一个级别,具体如下:
准确地说,MySQL的InnoDB引擎在提已交读(READ-COMMITED)级别通过MVCC解决了不可重复读的问题,在可重复读(REPEATABLE-READ)级别通过间隙锁解决了幻读问题。也就是说MySQL的事务隔离级别比对应的标准事务隔离级别更为严谨,也即:
InnoDB有两种不同的SELECT,即普通SELECT和锁定读SELECT。锁定读SELECT又有两种,即SELECT...FORSHARE和SELECT...FORUPDATE;锁定读SELECT之外的则是普通SELECT。
不同的SELECT是否都需要加锁呢?
FORSHARE语法是MySQL8.0时加入的,FORSHARE和LOCKINSHAREMODE是等价的,即,FORSHARE用于替代LOCKINSHAREMODE,不过,为了向后兼容,LOCKINSHAREMODE依然可用。
隔离级别为RU和Serializable时不需要MVCC,因此,只有RC和RR时,才存在MVCC,才存在一致性非锁定读。
这两种锁定读在搜索时所遇到的(注意:不是最终结果集中的)每一条索引记录(indexrecord)上设置排它锁或共享锁。此外,如果当前隔离级别是RR,它还会在每个索引记录前面的间隙上设置排它的或共享的gaplock(排它的和共享的gaplock没有任何区别,二者等价)。
看完背景介绍,我们再来看一下InnoDB提供的各种锁。
InnoDB一共有8种锁类型,其中,意向锁(IntentionLocks)和自增锁(AUTO-INCLocks)是表级锁,剩余全部都是行级锁。此外,共享锁或排它锁(SharedandExclusiveLocks)尽管也作为8种锁类型之一,它却并不是具体的锁,它是锁的模式,用来“修饰”其他各种类型的锁。
MySQL5.7及之前,可以通过information_schema.innodb_locks查看事务的锁情况,但是,只能看到阻塞事务的锁;如果事务并未被阻塞,则在该表中看不到该事务的锁情况。
MySQL8.0删除了information_schema.innodb_locks,添加了performance_schema.data_locks,可以通过performance_schema.data_locks查看事务的锁情况,和MySQL5.7及之前不同,performance_schema.data_locks不但可以看到阻塞该事务的锁,还可以看到该事务所持有的锁,也就是说即使事务并未被阻塞,依然可以看到事务所持有的锁(不过,正如文中最后一段所说,performance_schema.data_locks并不总是能看到全部的锁)。表名的变化其实还反映了8.0的performance_schema.data_locks更为通用了,即使你使用InnoDB之外的存储引擎,你依然可以从performance_schema.data_locks看到事务的锁情况。
performance_schema.data_locks的列LOCK_MODE表明了锁的类型,下面在介绍各种锁时,我们同时指出锁的LOCK_MODE。
它并不是一种锁的类型,而是其他各种锁的模式,每种锁都有shard或exclusive两种模式。
数据行r上共享锁(S锁)和排它锁(X锁)的兼容性如下:
假设T1持有数据行r上的S锁,则当T2请求r上的锁时:
假设T1持有数据行r上的X锁,则当T2请求r上的锁时:
T2请求r上的任何类型的锁时,T2都无法获得锁,此时,T2必须要等待直到T1释放r上的X锁。
意向锁是表锁。含义是已经持有了表锁,稍候将获取该表上某个/些行的行锁。有shard或exclusive两种模式。
LOCK_MODE分别是:IS或IX。
意向锁用来锁定层级数据结构,获取子层级的锁之前,必须先获取到父层级的锁。可以这么看InnoB的层级结构:InnoDB所有数据是schema的集合,schema是表的集合,表是行的集合。意向锁就是获取子层级(数据行)的锁之前,需要首先获取到父层级(表)的锁。
意向锁的目的是告知其他事务,某事务已经锁定了或即将锁定某个/些数据行。事务在获取行锁之前,首先要获取到意向锁,即:
事务请求锁时,如果所请求的锁与已存在的锁兼容,则该事务可以成功获得所请求的锁;如果所请求的锁与已存在的锁冲突,则该事务无法获得所请求的锁。
表级锁(table-levellock)的兼容性矩阵如下:
对于上面的兼容性矩阵,一定注意两点:
所以,意向锁只会阻塞全表请求(例如:LOCKTABLES...WRITE|READ),不会阻塞其他任何东西。因为LOCKTABLES...WRITE|READ需要设置X|S表锁,这会被意向锁IX或IS所阻塞。
查看表锁语句:showopentableswherein_use>0;
InnoDB允许表锁和行锁共存,使用意向锁来支持多粒度锁(multiplegranularitylocking)。意向锁如何支持多粒度锁呢,我们举例如下
T1:SELECT*FROMt1WHEREi=1FORUPDATE;
T2:LOCKTABLEt1WRITE;
T1执行时,需要获取i=1的行的X锁,但T1获取行锁前,T1必须先要获取t1表的IX锁,不存在冲突,于是T1成功获得了t1表的IX锁,然后,又成功获得了i=1的行的X锁;T2执行时,需要获取t1表的X锁,但T2发现,t1表上已经被设置了IX锁,因此,T2被阻塞(因为表的X锁和表的IX锁不兼容)。
假设不存在意向锁,则:
T1执行时,需要获取i=1的行的X锁(不需要获取t1表的意向锁了);T2执行时,需要获取t1表的X锁,T2能否获取到T1表的X锁呢?T2无法立即知道,T2不得不遍历表t1的每一个数据行以检查,是否某个行上已存在的锁和自己即将设置的t1表的X锁冲突,这种的判断方法效率实在不高,因为需要遍历整个表。
所以,使用意向锁,实现了“表锁是否冲突”的快速判断。意向锁就是协调行锁和表锁之间的关系的,或者也可以说,意向锁是协调表上面的读写锁和行上面的读写锁(也就是不同粒度的锁)之间的关系的。
在隔离级别为RR模式下加锁的默认锁类型,会根据条件进行锁退化,退化成索引记录锁(RecordLocks)、间隙锁(GapLocks)、或者两者都存在。
假设表中数据存在id=1,5,10三条数据,以排它锁(X锁)为例:
也就是所谓的行锁,锁定的是索引记录。行锁就是索引记录锁,所谓的“锁定某个行”或“在某个行上设置锁”,其实就是在某个索引的特定索引记录(或称索引条目、索引项、索引入口)上设置锁。有shard或exclusive两种模式。
LOCK_MODE分别是:S,REC_NOT_GAP或X,REC_NOT_GAP。
行锁就是索引记录锁,索引记录锁总是锁定索引记录,即使表上并未定义索引。表未定义索引时,InnoDB自动创建隐藏的聚集索引(索引名字是GEN_CLUST_INDEX),使用该索引执行recordlock。
举个例子(MySQL8.0下),假设有如下表结构及数据:
LOCK_MODE分别是:S,GAP或X,GAP。
gaplock可以共存(co-exist)。事务T1持有某个间隙上的gaplock并不能阻止事务T2同时持有同一个间隙上的gaplock。sharedgaplock和exclusivegaplock并没有任何的不同,它俩并不冲突,它俩执行同样的功能。
gaplock锁住的间隙可以是第一个索引记录前面的间隙,或相邻两条索引记录之间的间隙,或最后一个索引记录后面的间隙。
索引是B+树组织的,因此索引是从小到大按序排列的,在索引记录上查找给定记录时,InnoDB会在第一个不满足查询条件的记录上加gaplock,防止新的满足条件的记录插入。
举个例子(MySQL8.0下),表结构及数据同上:
一种特殊的gaplock。INSERT操作插入成功后,会在新插入的行上设置indexrecordlock,但,在插入行之前,INSERT操作会首先在索引记录之间的间隙上设置insertintentionlock,该锁的范围是(插入值,向下的一个索引值)。有shard或exclusive两种模式,但,两种模式没有任何区别,二者等价。
LOCK_MODE分别是:S,GAP,INSERT_INTENTION或X,GAP,INSERT_INTENTION。
insertintentionlock发出按此方式进行插入的意图:多个事务向同一个indexgap并发进行插入时,多个事务无需相互等待。
假设已存在值为4和7的索引记录,事务T1和T2各自尝试插入索引值5和6,在得到被插入行上的indexrecordlock前,俩事务都首先设置insertintentionlock,于是,T1insertintentionlock(5,7),T2insertintentionlock(6,7),尽管这两个insertintentionlock重叠了,T1和T2并不互相阻塞。
如果gaplock或next-keylock与insertintentionlock的范围重叠了,则gaplock或next-keylock会阻塞insertintentionlock。隔离级别为RR时正是利用此特性来解决幻读问题;尽管insertintentionlock也是一种特殊的gaplock,但它和普通的gaplock不同,insertintentionlock相互不会阻塞,这极大的提供了插入时的并发性。总结如下:
INSERT插入行之前,首先在索引记录之间的间隙上设置insertintentionlock,操作插入成功后,会在新插入的行上设置indexrecordlock,也就是在不发生锁冲突的情况下在瞬间LOCK_MODE由X,GAP,INSERT_INTENTION变为X,REC_NOT_GAP。
在最简单的情况下,如果一个事务正在向表中插入记录,则任何其他事务必须等待对该表执行自己的插入操作,以便使第一个事务插入的行的值是连续的。
通过对它的设置可以达到性能与安全(主从的数据一致性)的平衡。
我们先对insert做一下分类
首先insert大致上可以分成三类:
innodb_autoinc_lock_mode的说明
innodb_autoinc_lock_mode有三个取值:
总结:
semi-consistentread(半一致性读)是什么简单来说,semi-consistentread是readcommitted与consistentread两者的结合。一个update语句(semi-consistentread只针对update),如果读到一行已经加锁的记录,此时InnoDB返回记录最近提交的版本,由MySQL上层(Server层)判断此版本是否满足update的where条件。若满足(需要更新),则MySQL会重新发起一次读操作,此时会读取行的最新版本(并加锁)。(对于updatescan返回的不满足条件的记录,会提前放锁)
使用限制
优点
缺点
这种类型的死锁十分好理解,跟各种语言中的死锁基本一致,即线程1和线程2都需要获取A、B两把锁,但是他们获取锁的顺序相反:线程1先获取A、再获取B,而线程2先获取B再获取A,两者获取锁的顺序相反产生相互等待的情况,产生了死锁,在Java语言中就有自检测这种死锁的机制,JVM堆栈会报发现deadlock异常,同样MySQL也会有这样的自检测机制,一旦出现死锁,引擎就会报deadlock异常。下面是一个这样的典型例子:这是一个由于争抢两个Gap锁导致的死锁