InnoDB的锁
[toc]
InnoDB与MyISAM的最大不同有两点:一是支持事务(TRANSACTION);二是采用了行级锁。(还有一个支持外键)
获取InnoDB行锁争用情况
可以通过检查InnoDB_row_lock状态变量来分析系统上的行锁的争夺情况:
1 | mysql> show status like 'innodb_row_lock%'; |
如果发现锁争用比较严重,如InnoDB_row_lock_waits和InnoDB_row_lock_time_avg的值比较高,还可以通过设置InnoDB Monitors来进一步观察发生锁冲突的表、数据行等,并分析锁争用的原因。
InnoDB的行锁模式及加锁方法
InnoDB实现了以下两种类型的行锁。
- 共享锁(S, shared locks):允许一个事务去读一行,阻止其他事务获得相同数据集的排他锁。
- 排他锁(X, exclusive locks):允许获得排他锁的事务更新数据,阻止其他事务取得相同数据集的共享读锁和排他写锁。
InnoDB支持多粒度的锁,也即,除了行锁外还支持表锁,表锁通过意向锁实现,用以提示事务即将对表中的行添加什么样的锁。InnoDB有两种内部使用的意向锁(Intention Locks):
- 意向共享锁(IS):事务打算给数据行加行共享锁,事务在给一个数据行加共享锁前必须先取得该表的IS锁。
- 意向排他锁(IX):事务打算给数据行加行排他锁,事务在给一个数据行加排他锁前必须先取得该表的IX锁。
上述锁模式的兼容情况具体如表:
如果一个事务请求的锁模式与当前的锁兼容,InnoDB就将请求的锁授予该事务;反之,如果两者不兼容,该事务就要等待锁释放。
意向锁是InnoDB自动加的,不需用户干预。对于UPDATE、DELETE和INSERT语句,InnoDB会自动给涉及数据集加排他锁(X);对于普通SELECT语句,InnoDB不会加任何锁;事务可以通过以下语句显示给记录集加共享锁或排他锁。
i:共享锁(S):SELECT * FROM table_name WHERE ... LOCK IN SHARE MODE
ii:排他锁(X):SELECT * FROM table_name WHERE ... FOR UPDATE
用 SELECT … IN SHARE MODE 获得共享锁,主要用在需要数据依存关系时来确认某行记录是否存在,并确保没有人对这个记录进行UPDATE或者DELETE操作。但是如果当前事务也需要对该记录进行更新操作,则很有可能造成死锁,对于锁定行记录后需要进行更新操作的应用,应该使用SELECT… FOR UPDATE方式获得排他锁。
从InnoDB锁住的对象来看,InnoDB又可分为Record Lock, Gap Lock和Next-Key Lock。其中Record Lock只对索引记录加锁,Gap Lock在索引记录之间的”间隙“加锁,这个”间隙“也包含第一条索引记录之前和最后一条索引记录之后的”间隙“。Next-Key Lock是Record Lock和Gap Lock的结合,除了锁住索引记录本身外还锁住索引记录之前的”间隙“。(注意Record Lock只对索引记录加锁,即使创建表时没有指定任何索引(此时InnoDB创建一个隐藏的索引使用该索引加锁))。
默认情况下,InnoDB引擎使用 REPEATABLE READ 事务隔离级别和innodb_locks_unsafe_for_binlog=0的设置。此时,InnoDB使用Next-Key Lock来避免”幻象“读问题。
假定索引包含4,9,12,18几条记录,Record Lock直接在4,9,12,18这几条索引记录上加锁,Gap Lock覆盖的间隙包括(-,4)、(4,9)、(9,12)、(12,18)、(18,-),而Next-Key Lock可能锁定的区间包括(-,4]、(4,9]、(9,12]、(12,18]、(18,-)。
若事务持有索引R上的S锁或X锁,另外的事务便无法在R之前的间隙插入记录(这避免幻象读的基本原理)。如果,使用了唯一索引来检索唯一的行(不包括搜索条件只包含多列唯一索引中的部分列的情况)不会使用Gap Lock。例如user表id列上有唯一索引则SELECT * FROM user WHERE id = 100只会在id=100的行上对唯一索引加Record Lock,若该id不存在或者该列上无唯一索引则语句会锁住对应的间隙。
InnoDB中存在一种称为 insert intention gap lock的Gap Lock,由INSERT语句在执行插入操作前设置,用以表明多个事务向相同的索引间隙插入数据时若在间隙内插入的位置不同则不必相互等待。假定有值为4和7的索引记录。两个不同的事务分别同时向4和7之间的间隙插入5和6两条记录,在获取插入行上的X锁前会先获取间隙内的插入意向锁,因为行并不冲突,所以不会相互阻塞。
需要特别注意的是,不同的事务可以在同一gap上持有不兼容的锁类型。也即,对于Gap lock来说,gap X-lock与gap S-lock是等价的。Gap lock仅用来阻止其他事务在gap内插入。
在讲解InnoDB四种事务隔离级别的时候,讲到过”幻象“读问题,也即在”READ COMMITTED“隔离级别下,同一事务的两次相同的查询可能返回不同的结果,或者”REPEATABLE READ“事务隔离级别下,事务B的更新对另一事务A的更新语句可见,且在事务A在对更新后的结果进行了更新后随后同一事务中的SELECT能够看到之前不存在的记录更新后的结果。具体可以参考对应章节的示例。InnoDB使用Next-key Lock来解决”幻读“。InnoDB默认事务隔离级别为REPEATABLE READ,该隔离级别下InnoDB使用Next-key Lock来锁住相关索引记录以及记录之前的”间隙“,以保证其他session中的事务不仅不能更新记录而且不能在其中插入数据,从而避免”幻读“问题。
还用最初的表做实验,删除原有记录并在表t中插入i=4,9,12,20,30几条记录。
先将事务隔离级别调整为READ-COMMITED,我们已经知道,该隔离级别下用非锁定读时可以读取到其他事务中提交的更新,使前后两次同样条件的查询读取到的结果不一致,那么使用锁定读呢?从下边的例子可以看出,在READ-COMMITED隔离级别下,SESSION A中的事务使用锁定读(这里为X锁),可以阻塞SESSION B中的事务对锁定的记录进行更新或删除,”一定程度上“避免了”不可重复读“或者”幻读”,但是,这里的锁只锁定了记录本身,也即Record Lock,没能阻止SESSION B中的事务在锁定的记录之间插入新的记录,因此当其他事物在这些”间隙“内插入记录并提交后SESSION A中的事务能够看到新插入的记录,还是存在”不可重复读“或者”幻读”。
接下来将隔离级别调整为REATABLE READ,该隔离级别下使用非锁定读一般不会读取到其他事物提交的更新,但其他事务中的更新对其更新操作是可见的,当该事务对更新后的记录进行了更新后,同一事物内的查询便可以看到最新的结果,造成前后两次读取结果不一致(前边已有例子进行说明)。那么该隔离级别下使用锁定读呢?会不会也不能避免其他事物在锁定记录的”间隙“插入新的记录呢?答案是否定的,该隔离级别下,InnoDB使用Next-key Lock,除了锁定记录本身外还会锁定记录之间的”间隙”,从而阻止其他事物在读取的记录键插入新的记录,避免前后同一查询条件查询结果却不一致的问题。
锁的类型
Innodb Lock大体上可以分为:
- 共享锁,shared locks(S锁)
- 排它锁,exclusive locks(X锁)
- 意向锁,intention lock,分为 意向共享锁(IS锁) 和 意向排它锁(IX锁)
- 行锁,(record Locks、gap locks、next-key locks、Insert Intention Locks)
- 自增锁,auto-inc locks
行锁的作用范围是行级别,数据库能够确定那些行需要锁的情况下使用行锁,如果不知道会影响哪些行的时候就会使用表锁。举个例子,一个用户表user,有主键id和用户生日birthday,当使用update ... where id=?这样的语句数据库明确知道会影响哪一行,它就会使用行锁,当使用update ... where birthday=?这样的的语句的时候因为事先不知道会影响哪些行就可能会使用表锁。
InnoDB行锁是通过给索引上的索引项加锁来实现的,InnoDB这种行锁实现特点意味着:只有通过索引条件检索数据,InnoDB才使用行级锁,否则,InnoDB将使用表锁!
由于MySQL的行锁是针对索引加的锁,不是针对记录加的锁,所以虽然是访问不同行的记录,但是如果是使用相同的索引键,是会出现锁冲突的。
当表有多个索引的时候,不同的事务可以使用不同的索引锁定不同的行,另外,不论是使用主键索引、唯一索引或普通索引,InnoDB都会使用行锁来对数据加锁。
即便在条件中使用了索引字段,但是否使用索引来检索数据是由MySQL通过判断不同执行计划的代价来决定的,如果MySQL认为全表扫描效率更高,比如对一些很小的表,它就不会使用索引,这种情况下InnoDB将使用表锁,而不是行锁。 因此,在分析锁冲突时,别忘了检查SQL的执行计划,以确认是否真正使用了索引。
InnoDB行锁是通过给索引上的索引项加锁来实现的,如果没有索引,InnoDB将通过隐藏的聚集索引来对记录加锁。InnoDB行锁主要分为3种情形:
- Record lock:对索引项加锁
- Gap lock:对索引项之间的“间隙”、第一条记录前的“间隙”或最后一条记录后的“间隙”加锁
- Next-key lock:前两种的组合,对记录及其前面的间隙加锁。
共享/排他锁
行锁分为:S Lock 和 X Lock。S Lock是共享锁,可以理解为读锁;X Lock即写锁。
兼容性:读锁可以读,读锁不可写;写锁不可读也不可写。
意向锁
什么是意向锁呢?意向锁是一种不与行级锁冲突的表级锁。未来的某个时刻,事务可能要加共享或者排它锁时,先提前声明一个意向。注意一下,意向锁,是一个表级别的锁哈。
为什么需要意向锁呢? 或者换个通俗的说法,为什么要加共享锁或排他锁时的时候,需要提前声明个意向锁呢呢?
因为InnoDB是支持表锁和行锁共存的,如果一个事务A获取到某一行的排他锁,并未提交,这时候事务B请求获取同一个表的表共享锁。因为共享锁和排他锁是互斥的,因此事务B想对这个表加共享锁时,需要保证没有其他事务持有这个表的表排他锁,同时还要保证没有其他事务持有表中任意一行的排他锁。
然后问题来了,要保证没有其他事务持有表中任意一行的排他锁的话,去遍历每一行?这样显然是一个效率很差的做法。为了解决这个问题,InnoDB提出了意向锁。
意向锁是如何解决这个问题的呢? 我们来看下意向锁分为两类:
- 意向共享锁:简称
IS锁,当事务准备在某些记录上加S锁时,需要现在表级别加一个IS锁。(换个表达就是:事务有意向对表中的”某些行”加S锁。) - 意向排他锁:简称
IX锁,当事务准备在某条记录上加上X锁时,需要现在表级别加一个IX锁。
比如:
select ... lock in share mode,要给表设置IS锁;select ... for update,要给表设置IX锁;
意向锁又是如何解决这个效率低的问题呢:
如果一个事务A获取到某一行的排他锁,并未提交,这时候表上就有
意向排他锁和这一行的排他锁。这时候事务B想要获取这个表的共享锁,此时因为检测到事务A持有了表的
意向排他锁,因此事务A必然持有某些行的排他锁,也就是说事务B对表的加锁请求需要阻塞等待,不再需要去检测表的每一行数据是否存在排他锁啦。这样效率就高很多啦。
意向锁仅仅表明意向的锁,意向锁之间并不会互斥,是可以并行的,整体兼容性如下图所示:
意向锁是 innodb 自动加的,不需要用户干预。对于 update、delete 和 insert 语句,innodb 会自动给涉及数据集加排它锁(X);对于普通 select 语句,innodb 不会加任何锁。
主键自增锁
自增锁(auto-inc Locks)是一种特殊的表级锁,专门针对事务插入AUTO_INCREMENT类型的列,往往就是主键列。可以保证主键的值自增是“原子操作”,不会出现一致性、唯一性问题。
行锁的具体分类
记录锁(Record Lock)
记录锁(Record Lock) 是会去锁住索引记录的一行, 记录锁也叫 行锁。
例如:SELECT * FROM `test` WHERE `id`=1 FOR UPDATE;
假如没有任何一个索引,那么InnoDB会锁住隐形创建的那个主键。注意:这里锁的是索引,不一定只是主键索引,还可能是二级普通索引。
间隙锁(Gap Lock)
顾名思义,它会封锁索引记录中的“间隔”,限制其他事务在“间隔”中插入数据。它锁定的是一个不包含索引本身的范围。
举例来说,假如emp表中只有101条记录,其empid的值分别是 1,2,…,100,101,select * from emp where empid > 100 for update;是一个范围条件的检索,InnoDB不仅会对符合条件的empid值为101的记录加锁,也会对empid大于101(这些记录并不存在)的“间隙”加锁。
临键锁(Next-Key Lock)
Next-Key Lock 可以说是记录锁(Record Lock)和间隙锁(Gap Lock)的组合,它的封锁范围,既包含索引记录,及这条记录前面间隙上的锁。说得更具体一点就是:临键锁会封锁索引记录本身,以及索引记录之前的区间,即它的锁区间是前开后闭,比如(5,10]。
临键锁的主要目的,也是为了避免幻读。如果把事务的隔离级别降级为RC,临键锁则也会失效。
不可重复读(No Reaptable Read)和幻读(Phantom Problem)
不可重复读:修改。在同一个事务中,主要是说多次读取一条记录, 发现该记录中某些列值被修改过。
幻读: 增加或者删除。在同一个事务中,同一条完全相同的查询语句返回的结果集行数不同。
多版本并发控制 MVCC(读)和 临键锁 Next-Key Lock(写)共同解决了幻读问题。
关于MVCC的原理,就是每份数据会有快照,事务中读取数据(简单的select xxx from,select xx from xx for update或者select xx from xxx in share mode不行)的时候,如果数据被锁住了,就读以前留下的快照数据。
MVCC只在 Read Committed 和 Repeatable Read 下会开启。但是在这两种隔离级别下对于快照指定的数据定义不同。
在Read Committed下,MVCC读取的是被锁定数据的最新的一份数据。
在Repeatable Read下,MVCC读取的是事务刚开始时候的数据。
InnoDB行锁实现方式
InnoDB行锁是通过给索引上的索引项加锁来实现的,这一点MySQL与Oracle不同,后者是通过在数据块中对相应数据行加锁来实现的。InnoDB这种行锁实现特点意味着:只有通过索引条件检索数据,InnoDB才使用行级锁,否则,InnoDB将使用表锁!
在实际应用中,要特别注意InnoDB行锁的这一特性,不然的话,可能导致大量的锁冲突,从而影响并发性能。
(1)在不通过索引条件查询的时候,InnoDB确实使用的是表锁,而不是行锁。
(2)由于MySQL的行锁是针对索引加的锁,不是针对记录加的锁,所以虽然是访问不同行的记录,但是如果是使用相同的索引键,是会出现锁冲突的。
(3)当表有多个索引的时候,不同的事务可以使用不同的索引锁定不同的行,另外,不论是使用主键索引、唯一索引或普通索引,InnoDB都会使用行锁来对数据加锁。
(4)即便在条件中使用了索引字段,但是否使用索引来检索数据是由MySQL通过判断不同执行计划的代价来决定的,如果MySQL认为全表扫描效率更高,比如对一些很小的表,它就不会使用索引,这种情况下InnoDB将使用表锁,而不是行锁。因此,在分析锁冲突时,别忘了检查SQL的执行计划,以确认是否真正使用了索引。
间隙锁(Next-Key锁)
当我们用范围条件而不是相等条件检索数据,并请求共享或排他锁时,InnoDB会给符合条件的已有数据记录的索引项加锁;对于键值在条件范围内但并不存在的记录,叫做“间隙(GAP)”,InnoDB也会对这个“间隙”加锁,这种锁机制就是所谓的间隙锁(Next-Key锁)。
举例来说,假如emp表中只有101条记录,其empid的值分别是 1,2,…,100,101,下面的SQL:
Select * from emp where empid > 100 for update;
是一个范围条件的检索,InnoDB不仅会对符合条件的empid值为101的记录加锁,也会对empid大于101(这些记录并不存在)的“间隙”加锁。
InnoDB使用间隙锁的目的,一方面是为了防止幻读,以满足相关隔离级别的要求,对于上面的例子,要是不使用间隙锁,如果其他事务插入了empid大于100的任何记录,那么本事务如果再次执行上述语句,就会发生幻读;另外一方面,是为了满足其恢复和复制的需要。有关其恢复和复制对锁机制的影响,以及不同隔离级别下InnoDB使用间隙锁的情况,在后续的章节中会做进一步介绍。
很显然,在使用范围条件检索并锁定记录时,InnoDB这种加锁机制会阻塞符合条件范围内键值的并发插入,这往往会造成严重的锁等待。因此,在实际应用开发中,尤其是并发插入比较多的应用,我们要尽量优化业务逻辑,尽量使用相等条件来访问更新数据,避免使用范围条件。
还要特别说明的是,InnoDB除了通过范围条件加锁时使用间隙锁外,如果使用相等条件请求给一个不存在的记录加锁,InnoDB也会使用间隙锁!
转载自:
https://blog.csdn.net/u013967628/article/details/85293041
https://blog.csdn.net/wangyiyungw/article/details/81002256