Mysql之Innodb锁机制详解

InnoDB与MyISAM的最大不同有两点:一是支持事务(TRANSACTION);二是采用了行级锁。关于事务我们之前有专题介绍,这里就着重介绍下它的锁机制。

总的来说,InnoDB按照不同的分类共有七种类型的锁:

共享/排它锁(Shared and Exclusive Locks)

意向锁(Intention Locks)

间隙锁(Gap Locks)

记录锁(Record Locks)

临键锁(Next-key Locks)

插入意向锁(Insert Intention Locks)

自增锁(Auto-inc Locks)

共享/排它锁(Shared and Exclusive Locks)

按照兼容性来分类,InnoDB有共享锁和排它锁两种行级锁。

  • 共享锁(S):又称读锁。允许一个事务去读一行,阻止其他事务获得相同数据集的排他锁。若事务T对数据对象A加上S锁,则事务T可以读A但不能修改A,其他事务只能再对A加S锁,而不能加X锁,直到T释放A上的S锁。这保证了其他事务可以读A,但在T释放A上的S锁之前不能对A做任何修改。
  • 排他锁(X):又称写锁。允许获取排他锁的事务更新数据,阻止其他事务取得相同的数据集共享读锁和排他写锁。若事务T对数据对象A加上X锁,事务T可以读A也可以修改A,其他事务不能再对A加任何锁,直到T释放A上的锁。

对于共享锁大家可能很好理解,就是多个事务只能读数据不能改数据。 对于排他锁大家的理解可能就有些差别,我当初就犯了一个错误,以为排他锁锁住一行数据后,其他事务就不能读取和修改该行数据,其实不是这样的。排他锁指的是一个事务在一行数据加上排他锁后,其他事务不能再在其上加其他的锁,但是仍然可以进行普通无锁查询。mysql InnoDB引擎默认的修改数据语句:update,delete,insert都会自动给涉及到的数据加上排他锁,select语句默认不会加任何锁类型,如果加排他锁可以使用select …for update语句,加共享锁可以使用select … lock in share mode语句。所以加过排他锁的数据行在其他事务中是不能修改数据的,也不能通过for update和lock in share mode锁的方式查询数据,但可以直接通过select …from…查询数据,因为普通查询没有任何锁机制。

另外还需要注意的是:InnoDB行锁是通过给索引上的索引项加锁来实现的,这一点MySQL与Oracle不同,后者是通过在数据块中对相应数据行加锁来实现的。InnoDB这种行锁实现特点意味着:只有通过索引条件检索数据,InnoDB才使用行级锁,否则,InnoDB将使用表锁!

意向锁(Intention Locks)

InnoDB为了支持多粒度锁机制(multiple granularity locking),即允许行级锁与表级锁共存,而引入了意向锁(intention locks)。意向锁是指,未来的某个时刻,事务可能要加共享/排它锁了,先提前声明一个意向。

意向锁是一个表级别的锁(table-level locking);

意向锁又分为:

  • 意向共享锁(intention shared lock, IS),它预示着,事务有意向对表中的某些行加共享S锁;
  • 意向排它锁(intention exclusive lock, IX),它预示着,事务有意向对表中的某些行加排它X锁;

加锁的语法为:

select ... lock in share mode;  //要设置IS锁

select ... for update;      //要设置IX锁

意向锁和共享级排它锁的兼容性关系如下:

前面讨论的行锁按照兼容性分为共享/排它锁,但是从实现算法上来分的话,Innodb的三种行锁分别是:间隙锁、记录锁和临键锁,下面分别阐述。

间隙锁(Gap Locks)

当我们用范围条件而不是相等条件检索数据,并请求共享或排他锁时,InnoDB会给符合条件的已有数据的索引项加锁;

对于键值在条件范围内但并不存在的记录,叫做“间隙(GAP)”,InnoDB也会对这个“间隙”加锁,这种锁机制就是所谓的间隙锁(Next-Key锁)。

举例来说,假如lock_example表中只有101条记录,其id的值分别是1,2,...,100,101,下面的SQL:

Select * from lock_example where id > 100 for update;

InnoDB 不仅会对符合条件的 id值为 101 的记录加锁;

也会对 id大于101(这些记录并不存在)的“间隙”加锁。

间隙锁的目的

  • 防止幻读,以满足相关隔离级别的要求

对于上例,若不使用间隙锁,如果其他事务插入 id大于 100 的任何记录,那么本事务如果再次执行上述语句,就会发生幻读。

  • 满足其恢复和复制的需要

在使用范围条件检索并锁定记录时,InnoDB 这种加锁机制会阻塞符合条件范围内键值的并发插入,这往往会造成严重的锁等待,因此,在实际开发中,尤其是并发插入较多的应用,我们要尽量优化业务逻辑,尽量使用相等条件来访问更新数据,避免使用范围条件。

记录锁(Record Locks)

顾名思义,记录锁就是为某行记录加锁,它封锁该行的索引记录:

-- id 列为主键列或唯一索引列

SELECT * FROM lock_example WHERE id = 1 FOR UPDATE;

id 为 1 的记录行会被锁住。

需要注意的是:id 列必须为唯一索引列或主键列,否则上述语句加的锁就会变成临键锁。

同时查询语句必须为精准匹配(=),不能为 >、<、like等,否则也会退化成临键锁。

其他实现

在通过 主键索引与唯一索引对数据行进行UPDATE 操作时,也会对该行数据加记录锁:

-- id 列为主键列或唯一索引列

UPDATE lock_example SET age = 50 WHERE id = 1;

临键锁(Next-Key Locks)

Next-Key 可以理解为一种特殊的间隙锁,也可以理解为一种特殊的算法。通过临键锁可以解决幻读的问题。 每个数据行上的非唯一索引列上都会存在一把临键锁,当某个事务持有该数据行的临键锁时,会锁住一段左开右闭区间的数据。需要强调的一点是,InnoDB 中行级锁是基于索引实现的,临键锁只与非唯一索引列有关,在唯一索引列(包括主键列)上不存在临键锁,会降级为记录锁,即仅锁住索引本身,不是范围。

假设有如下表:

MySql,InnoDB,Repeatable-Read:lock_example(id PK, age KEY, name)

该表中 age 列潜在的临键锁有:

(-∞, 10],

(10, 24],

(24, 32],

(32, 45],

(45, +∞],

在事务 A 中执行如下命令:

-- 根据非唯一索引列 UPDATE 某条记录

UPDATE lock_example SET name = Vladimir WHERE age = 24;

-- 或根据非唯一索引列 锁住某条记录

SELECT * FROM lock_example WHERE age = 24 FOR UPDATE;

不管执行了上述 SQL 中的哪一句,之后如果在事务 B 中执行以下命令,则该命令会被阻塞:

INSERT INTO table VALUES(100, 16, 'Ezreal');

很明显,事务 A 在对 age 为 24 的列进行 UPDATE 操作的同时,也获取了 (10, 24] 这个区间内的临键锁。

不仅如此,在执行以下 SQL 时,也会陷入阻塞等待:

INSERT INTO table VALUES(100, 30, 'Tom');

那最终我们就可以得知,在根据非唯一索引 对记录行进行 UPDATE \ FOR UPDATE \ LOCK IN SHARE MODE 操作时,InnoDB 会获取该记录行的临键锁 ,并同时获取该记录行下一个区间的间隙锁。

即事务 A在执行了上述的 SQL 后,最终被锁住的记录区间为 (10, 32]。

插入意向锁(Insert Intention Locks)

对已有数据行的修改与删除,必须加强互斥锁(X锁),那么对于数据的插入,是否还需要加这么强的锁,来实施互斥呢?插入意向锁,孕育而生。

插入意向锁,是间隙锁(Gap Locks)的一种(所以,也是实施在索引上的),它是专门针对insert操作的。多个事务,在同一个索引,同一个范围区间插入记录时,如果插入的位置不冲突,不会阻塞彼此。先看下官方的解释:

Insert Intention Lock signals the intent to insert in such a way that multiple transactions inserting into the same index gap need not wait for each other if they are not inserting at the same position within the gap.

举个例子(表依然是如上的例子lock_example,数据依然是如上),事务A先执行,在10与24两条记录中插入了一行,还未提交:

insert into lock_example values(11,23, 'Jim');

事务B后执行,也在10与24两条记录中插入了一行:

insert into lock_example values(12,24, 'Bob');

因为是插入操作,虽然是插入同一个区间,但是插入的记录并不冲突,所以使用的是插入意向锁,此处A事务并不会阻塞B事务。

自增锁(Auto-inc Locks)

自增锁是一种特殊的表级别锁(table-level lock),专门针对事务插入AUTO_INCREMENT类型的列。最简单的情况,如果一个事务正在往表中插入记录,所有其他事务的插入必须等待,以便第一个事务插入的行,是连续的主键值。官方解释如下:

AUTO-INC lock is a special table-level lock taken by transactions inserting into tables with AUTO_INCREMENT columns. In the simplest case, if one transaction is inserting values into the table, any other transactions must wait to do their own inserts into that table, so that rows inserted by the first transaction receive consecutive primary key values.

举个例子(表依然是如上的例子lock_example),但是id为AUTO_INCREMENT,如果A事务执行如下语句:

insert into lock_example values(23, 'Jim');

B事务执行的语句如下:

insert into lock_example values(24, 'Bob');

此时事务B插入操作会阻塞,直到事务A提交。

总结

以上总结的7种锁,可以按两种方式来区分:

1. 按锁的兼容性来划分,可以分为共享、排他锁;

共享锁(S锁、IS锁),可以提高读读并发;

为了保证数据强一致,InnoDB使用强互斥锁(X锁、IX锁),保证同一行记录修改与删除的串行性;

2. 按锁的粒度来划分,可以分为:

表锁:意向锁(IS锁、IX锁)、自增锁;

行锁:记录锁、间隙锁、临键锁、插入意向锁;

其中,InnoDB的细粒度锁(即行锁),是实现在索引记录上的(如果未命中索引则会失效);  

记录锁锁定索引记录;间隙锁锁定间隔,防止间隔中被其他事务插入;临键锁锁定索引记录+间隔,防止幻读;

InnoDB使用插入意向锁,可以提高插入并发;

间隙锁(gap lock)与临键锁(next-key lock)只在Repeatable-Read(RR可重复读)以上的级别生效,Read-Committed(RC已提交读)下会失效。

posted @ 2021-03-30 20:20  黄进广寒  阅读(497)  评论(0编辑  收藏  举报