关于innodb中锁的讨论

使用mysql的过程中经常会遇到死锁,语句show engine innodb status可以列出详细的innodb内部信息,包括死锁信息,不过这些内容并不那么容易理解。我们确实可以从中看到哪个事务阻塞在哪个锁上,以及哪个事务占有了哪些锁,但是这些内容没有回答两个重要的问题:

  • 这些锁是什么意思,它们有什么效果
  • 为什么在执行事务时要加这些锁
    这两个问题可以在mysql官方文档中找到解答:InnoDB Locking介绍了innodb中有哪些类型的锁以及他们的作用。Locks Set by Different SQL Statements in InnoDB介绍了各种SQL语句在执行过程中会产生哪些锁。(不能说文档写的不好,但是看完确实还是有些疑问的)

按照常规的理解,锁就是互斥锁(或者读写锁),占有了锁就可以访问某些数据或者执行某段代码,拿不到锁就阻塞或者放弃。而死锁,是多线程并安执行时,各个线程获取锁的顺序不一致而导致的。这个理解是正确的,按照这个思路,在大部分情况下都能够应付程序中的问题。

然而,innodb中的锁类型太多,加锁的逻辑看起来也很诡异,导致了innodb的死锁总是令人困惑。这篇笔记来仔细讨论一下有关innodb锁的两个不好理解的地方。

意向锁

意向锁在Intention Locks中有定义:

Intention locks are table-level locks that indicate which type of lock (shared or exclusive) a transaction requires later for a row in a table.

这里明确说明意向锁是表级锁,同时给出了一个冲突矩阵:

X IX S IS
X 冲突 冲突 冲突 冲突
IX 冲突 兼容 冲突 兼容
S 冲突 冲突 兼容 兼容
IS 冲突 兼容 兼容 兼容

关于这个冲突矩阵,文档中描述如下:

Table-level lock type compatibility is summarized in the following matrix.

也就是说,这里的冲突指的是表级锁之间的冲突,其中X,S指的是表锁里面的X锁和S锁。可以测试一下:

// 建立一个表
mysql> CREATE TABLE child (id int(11) NOT NULL, PRIMARY KEY(id)) ENGINE=InnoDB;

// session 1 加上IX
mysql> start transaction;
mysql> select * from child for update;

// session 2 尝试锁表
mysql> start transaction;
mysql> lock tables child read;

这里session2会阻塞住,直到session1执行commit或者rollback。这说明意向锁确实会影响表锁。那么可以确认:

  1. 意向锁是表级锁。
  2. 意向锁和表锁相互作用,规则如上面图表中所示。
注意:lock tables这样的锁表动作是由innodb的上面的mysql层发起的,意向锁提供给mysql层一种检查是否能够顺利锁表的标志。
也就是说,当一个表上存在意向锁时,这个表里面可能正在进行一些操作,此时不能锁表。这个兼容冲突规律在上面的冲突矩阵中描述。

此外还有一个值得关注的问题:意向锁会不会影响行锁呢,文档中的描述是:

Intention locks do not block anything except full table requests (for example, LOCK TABLES ... WRITE). The main purpose of intention locks is to show that someone is locking a row, or going to lock a row in the table.

也就是说,意向锁不影响行锁。这个并不好验证,因为任何能够产生意向锁的语句都会同时产生行级锁,当一个事务获取行锁阻塞时,无法确切地知道是因为其他事务的意向锁还是行锁。

插入意向锁

接下来看下插入意向锁,文档定义如下:

An insert intention lock is a type of gap lock set by INSERT operations prior to row insertion. This 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. Suppose that there are index records with values of 4 and 7. Separate transactions that attempt to insert values of 5 and 6, respectively, each lock the gap between 4 and 7 with insert intention locks prior to obtaining the exclusive lock on the inserted row, but do not block each other because the rows are nonconflicting.

这段描述非常令人困惑,它基本介绍了插入意向锁的几个特点:

  1. 插入意向锁是一种间隙锁
  2. 意向锁不会导致多个插入操作相互阻塞

但是,它没有说明插入意向锁有什么作用,为什么需要这个锁。如果两个并行的事务同时执行insert语句,但是insert到不同的位置,它们不会阻塞对方的执行。那么获取插入意向锁的作用是什么呢?这个问题目前从文档中看不到任何解释。

先来看下另外一个问题:插入意向锁到底是意向锁还是间隙锁?很明显,从名字上看,插入意向锁是意向锁,同时文档中又说它是间隙锁,那么现在只能认为它既是意向锁又是间隙锁。我们可以分别看下它是否有这两种锁的特点。

意向锁的特点是作用在表级别,当某个事务持有意向锁时,其他事务无法进行锁表的操作。测试确认一下插入意向锁是否有这个效果。

// session 1 获取插入意向锁:
mysql> start transaction;
mysql> insert into child values(10);

// session 2 锁表:
mysql> start transaction;
mysql> lock tables child read;

session2阻塞,直到当session 1提交后,session 2 锁表成功。这说明插入意向锁确实能够影响表锁。
意向锁还有一个特点是,和行级锁相互无影响。但是从文档中描述来看,插入意向锁似乎并没有这个特点。文档中展示了一个例子:当间隙锁存在时,试图向间隙中插入数据会阻塞住,等待获取插入间隙锁。这可以看出,插入意向锁会受到行级锁的影响,这一点和意向锁不同。

对于间隙锁,它和插入意向锁有明显的不同。第一,间隙锁的作用是阻止其他事务向间隙中插入数据,而插入意向锁并不阻止其他事务向间隙中内插入数据。第二,从上面的例子中可以看到,插入间隙锁和普通间隙锁是冲突的,而根据文档中对间隙锁的描述,间隙锁之间是兼容的:

The reason conflicting gap locks are allowed is that if a record is purged from an index, the gap locks held on the record by different transactions must be merged.

总结一下,插入意向锁有这些特点:

  • 会影响lock tables操作,这一点和意向锁相同
  • 受到行级锁的影响,这和意向锁不同
  • 和普通间隙锁冲突,这和间隙锁不同

文档中说插入意向锁是一种间隙锁,这里的间隙锁应该只是字面意义上的间隙,就是一段范围。至于为什么插入意向锁要锁一段范围,可能的一个原因是范围锁相对于行锁来说粒度更粗,更容易被上层msyql检查到。(意向锁的作用)

insert操作的加锁逻辑

文档中描述insert操作的加锁:

INSERT sets an exclusive lock on the inserted row. This lock is an index-record lock, not a next-key lock (that is, there is no gap lock) and does not prevent other sessions from inserting into the gap before the inserted row.
Prior to inserting the row, a type of gap lock called an insert intention gap lock is set.
也就说,在执行insert的过程中,会加两个锁,先是插入意向锁,然后是x锁。我们来讨论下加锁失败的情形。

获取插入意向锁失败
前面提到过,当插入的位置存在间隙锁时,执行insert的事务会阻塞在获取插入意向锁的动作上。文档中有个实例。
除此之外,当其他事务占有了待插入数据的S或者X锁时,当前事务也会阻塞在获取插入意向锁上。(这个行为使得插入意向锁看起来很像是普通的X锁)

获取X锁失败
模拟出获取X锁失败的情况并不简单,比如按照下面的设想来操作:

// session 1 获取数据上的x锁
mysql> start transaction;
mysql> select * from child where id=1 for update;

// session 2 执行插入
mysql> start transaction;
mysql> insert ino child values(1);

你会发现按照这样的操作,session2会阻塞在获取插入意向锁上。so怎么样才能让事务顺利的获取到插入意向锁呢,方法是使用两个事务向同一个地方插入数据,按照插入意向锁的定义,它们不会阻塞并发的插入操作。
当两个事务都成功获取了插入意向锁后,它们会发现获取x锁时出现了冲突。这里出现了最麻烦的情况:当一个事务已经获取了要插入数据上的X锁,另一个事务会得到一个duplicate error,然后这个事务会去请求获取这个数据上的S锁
文档中介绍了插入操作的这个特点:

If a duplicate-key error occurs, a shared lock on the duplicate index record is set. This use of a shared lock can result in deadlock should there be multiple sessions trying to insert the same row if another session already has an exclusive lock.
这个特点导致并发插入同一条数据时很容易出现死锁,文档中有两个死锁的示例。

至于为什么在duplicate error后会去请求S锁,文档并没有给出解释,可能只能去阅读源码来找原因了。除此之外,当要插入的数据已经存在时,也会影响insert的行为。比如同时执行两个插入操作:

// session 1
start transaction;
insert into child values(1);

// session 2
start transaction;
insert into child values(2);

当id为2的数据在表中已经存在时,两个session都会获取锁成功,并立即返回duplicate error。然而当id为2的数据不存在时,session2会阻塞在获取S锁上,等待session1释放数据上的X锁,如果session1 commit,session2得到duplicate error,如果session1 rollback,session2插入成功。

根据insert的这些行为,大概推测一下它的加锁逻辑:

  1. 当待插入数据上有X锁,或者S锁时,获取插入意向锁阻塞
  2. 获取到意向锁后,先去检查数据库中是否已经有重复数据,如果有,则返回duplicate error
  3. 如果在数据库中没有发现重复数据,则尝试获取X锁,成功则到第6步,
  4. 如果获取X锁失败(视作duplicate error),则去尝试获取S锁,
  5. 获取S锁成功后,再去获取X锁
  6. 写入数据到数据库。

按照这个思路,能够解释目前看到的现象。不过有个疑问;在第4步为什么不直接等待X锁,而是当作duplicate error,转而去请求S锁。

总结

innodb的锁过于复杂,这看上去好像不是经过良好的设计而产生的结果,更可能是在逐步的升级迭代中因为各种需求慢慢演变成现在这个样子。 想要完全弄明白这些锁的原理,可能必须要阅读源码。目前我只看了innodb storage engine 中的介绍,总体感受是,能够弄明白一些现象,但是还有很多疑惑。(要看源码还要下功夫)

你可能感兴趣的:(关于innodb中锁的讨论)