[Reserved] MySQL statement lock analysis

a

The establishment of a hero of the Three Kingdoms storage herotable:

CREATE TABLE hero (    number INT,    name VARCHAR(100),    country varchar(100),    PRIMARY KEY (number),    KEY idx_name (name)) Engine=InnoDB CHARSET=utf8;

Then insert a few records to the table:

INSERT INTO hero VALUES    (1, 'l刘备', '蜀'),    (3, 'z诸葛亮', '蜀'),    (8, 'c曹操', '魏'),    (15, 'x荀彧', '魏'),    (20, 's孙权', '吴');

Then now herotable have two indices (a secondary index, a clustered index), the following schematic:

15332916-0cbc9f205c25baab
image

Locking statement analysis

In fact, ah, "XXX statements that add what lock" itself is a false proposition, a statement need to add locks restricted by many conditions, for example:

  • Transaction isolation level

  • Index used when statement is executed (for example, a clustered index, a unique secondary index, Common secondary index)

  • Query (for example =, =<, >=etc.)

  • Specific types of statements executed

Before locking procedure to continue a detailed analysis of the statement, we must have a global concept: 加锁just solved during the execution of concurrent transactions cause 脏写, 脏读, 不可重复读, 幻读a solution to these problems ( MVCCregarded as a solution 脏读, 不可重复读, 幻读of these issues solutions), we must realize that 加锁the starting point is to solve these problems, to solve problems in different situations are not the same, which leads to additional lock is not the same, but do not lock to lock, easy to go around themselves. Of course, sometimes because of MySQLthe specific implementation resulting from locking under some scenarios some not so good understanding, which we have to rote learning -

We here the statement is divided into 3 categories: general SELECTstatements, locking read statements, INSERTstatements, we are to look at.

Ordinary SELECT statement

General SELECTstatement:

  • READ UNCOMMITTEDUnder isolation level, unlocked, directly read the latest version of the record, may occur 脏读, 不可重复读and 幻读problems.

  • READ COMMITTEDUnder isolation level, unlocked, each ordinary execution SELECTwill generate a while statement ReadView, this solves the 脏读problem, but does not solve 不可重复读and 幻读problems.

  • REPEATABLE READUnder isolation level, unlocked, only the first implementation of a common SELECTgenerated when a statement is ReadView, so to 脏读, 不可重复读and 幻读problems are solved.

    But here there is a small episode:

    # 事务T1,REPEATABLE READ隔离级别下mysql> BEGIN;Query OK, 0 rows affected (0.00 sec)mysql> SELECT * FROM hero WHERE number = 30;Empty set (0.01 sec)# 此时事务T2执行了:INSERT INTO hero VALUES(30, 'g关羽', '魏'); 并提交mysql> UPDATE hero SET country = '蜀' WHERE number = 30;Query OK, 1 row affected (0.01 sec)Rows matched: 1  Changed: 1  Warnings: 0mysql> SELECT * FROM hero WHERE number = 30;+--------+---------+---------+| number | name    | country |+--------+---------+---------+|     30 | g关羽   | 蜀      |+--------+---------+---------+1 row in set (0.01 sec)
    

    In the REPEATABLE READlower isolation level, T1the first execution of an ordinary SELECTgenerated when a statement ReadViewafter T2the herotable, insert a new record will be submitted, ReadViewdoes not prevent the T1execution UPDATEor DELETEstatement (because of changes to the newly inserted record T2has been submitted, changes the record does not cause obstruction), but this way this new record trx_idto hide the column becomes T1a 事务id, then T1re-use an ordinary SELECTcould see this when recording the statement to query this record, it these records are returned to the client. Because the existence of this particular phenomenon, you can think InnoDBof MVCCand can not completely ban phantom reads.

  • SERIALIZABLEUnder isolation level, we need to discuss two cases:

  • In the System variables autocommit=0when, that is, to disable automatic submission, ordinary SELECTstatement will be converted to SELECT ... LOCK IN SHARE MODEsuch a statement, that is, before reading records need to get the record S锁, the specific circumstances and lock REPEATABLE READas lower isolation level, then we behind analysis.

  • In the System variables autocommit=1when, that is, to enable automatic submission, ordinary SELECTstatement is not locked, just use MVCCto generate a ReadViewto read the record.

    Why not lock it? Because enable auto commit means a transaction contains only one statement, a statement it was doing anything 不可重复读, 幻读this kind of problem.

Locking read statements

We put together to discuss the following four kinds of statements:

  • A statement:SELECT ... LOCK IN SHARE MODE;

  • Statement II:SELECT ... FOR UPDATE;

  • Statement three:UPDATE ...

  • Statement Four:DELETE ...

We say 语句一and 语句二are MySQLprescribed two kinds 锁定读of syntax, while 语句三and 语句四due to the need to locate in the implementation of changes to be recorded and to record locking, can also be considered a 锁定读.

READ UNCOMMITTED / READ COMMITTED isolation level under

In the READ UNCOMMITTEDlock statement under way and READ COMMITTEDconsistent means locking statements under isolation level, so we put together to say. It is noteworthy that, using 加锁the time to resolve problems caused by concurrent transactions, in fact 脏读and 不可重复读in any isolation level will not happen (because the 读-写operation requires a queue be).

In the case of using the primary key query equivalence
  • Use SELECT ... LOCK IN SHARE MODEto lock the record, he says:

    SELECT * FROM hero WHERE number = 8 LOCK IN SHARE MODE;
    

    This statement is executed only when it needs to access a clustered index numbervalue of 8the record, so just give it a plus S型正经记录锁like, as shown:

    15332916-4281a8f6d70d537b
    image
  • Use SELECT ... FOR UPDATEto lock the record, he says:

    SELECT * FROM hero WHERE number = 8 FOR UPDATE;
    

    This statement is executed only when it needs to access a clustered index numbervalue of 8the record, so just give it a plus X型正经记录锁like, as shown:

    15332916-f3a0822ad14ea7b5
    image

    Tips: In order to distinguish between S and X locks lock, then we added put in a schematic view of a recording stained blue S lock, the lock records plus X dyed purple.

  • Use UPDATE ...to lock the record, he says:

    UPDATE hero SET country = '汉' WHERE number = 8;
    

    This UPDATEstatement does not update the secondary index column, means locking said top and SELECT ... FOR UPDATEstatements consistent.

    如果UPDATE语句中更新了二级索引列,比方说:

    UPDATE hero SET name = 'cao曹操' WHERE number = 8;
    

    该语句的实际执行步骤是首先更新对应的number值为8的聚簇索引记录,再更新对应的二级索引记录,所以加锁的步骤就是:

  1. number值为8的聚簇索引记录加上X型正经记录锁(该记录对应的)。

  2. 为该聚簇索引记录对应的idx_name二级索引记录(也就是name值为'c曹操'number值为8的那条二级索引记录)加上X型正经记录锁

画个图就是这样:

15332916-92e990b5334985ff
image

小贴士: 我们用带圆圈的数字来表示为各条记录加锁的顺序。

  • 使用DELETE ...来为记录加锁,比方说:

    DELETE FROM hero WHERE number = 8;
    

    我们平时所说的“DELETE表中的一条记录”其实意味着对聚簇索引和所有的二级索引中对应的记录做DELETE操作,本例子中就是要先把number值为8的聚簇索引记录执行DELETE操作,然后把对应的idx_name二级索引记录删除,所以加锁的步骤和上边更新带有二级索引列的UPDATE语句一致,就不画图了。

对于使用主键进行范围查询的情况
  • 使用SELECT ... LOCK IN SHARE MODE来为记录加锁,比方说:

    SELECT * FROM hero WHERE number <= 8 LOCK IN SHARE MODE;
    

    这个语句看起来十分简单,但它的执行过程还是有一丢丢小复杂的:

  1. 先到聚簇索引中定位到满足number <= 8的第一条记录,也就是number值为1的记录,然后为其加锁。

  2. 判断一下该记录是否符合索引条件下推中的条件。

    我们前边介绍过一个称之为索引条件下推Index Condition Pushdown,简称ICP)的功能,也就是把查询中与被使用索引有关的查询条件下推到存储引擎中判断,而不是返回到server层再判断。不过需要注意的是,索引条件下推只是为了减少回表次数,也就是减少读取完整的聚簇索引记录的次数,从而减少IO操作。而对于聚簇索引而言不需要回表,它本身就包含着全部的列,也起不到减少IO操作的作用,所以设计InnoDB的大叔们规定这个索引条件下推特性只适用于二级索引。也就是说在本例中与被使用索引有关的条件是:number <= 8,而number列又是聚簇索引列,所以本例中并没有符合索引条件下推的查询条件,自然也就不需要判断该记录是否符合索引条件下推中的条件。

  3. 判断一下该记录是否符合范围查询的边界条件

    因为在本例中是利用主键number进行范围查询,设计InnoDB的大叔规定每从聚簇索引中取出一条记录时都要判断一下该记录是否符合范围查询的边界条件,也就是number <= 8这个条件。如果符合的话将其返回给server层继续处理,否则的话需要释放掉在该记录上加的锁,并给server层返回一个查询完毕的信息。

    对于number值为1的记录是符合这个条件的,所以会将其返回到server层继续处理。

  4. 将该记录返回到server层继续判断。

    server层如果收到存储引擎层提供的查询完毕的信息,就结束查询,否则继续判断那些没有进行索引条件下推的条件,在本例中就是继续判断number <= 8这个条件是否成立。噫,不是在第3步中已经判断过了么,怎么在这又判断一回?是的,设计InnoDB的大叔采用的策略就是这么简单粗暴,把凡是没有经过索引条件下推的条件都需要放到server层再判断一遍。如果该记录符合剩余的条件(没有进行索引条件下推的条件),那么就把它发送给客户端,不然的话需要释放掉在该记录上加的锁。

  5. 然后刚刚查询得到的这条记录(也就是number值为1的记录)组成的单向链表继续向后查找,得到了number值为3的记录,然后重复第2345这几个步骤。

小贴士: 上述步骤是在MySQL 5.7.21这个版本中验证的,不保证其他版本有无出入。

但是这个过程有个问题,就是当找到number值为8的那条记录的时候,还得向后找一条记录(也就是number值为15的记录),在存储引擎读取这条记录的时候,也就是上述的第1步中,就得为这条记录加锁,然后在第3步时,判断该记录不符合number <= 8这个条件,又要释放掉这条记录的锁,这个过程导致number值为15的记录先被加锁,然后把锁释放掉,过程就是这样:

15332916-93ee7178f345547d
image

这个过程有意思的一点就是,如果你先在事务T1中执行:

# 事务T1BEGIN;SELECT * FROM hero WHERE number <= 8 LOCK IN SHARE MODE;

然后再到事务T2中执行:

# 事务T2BEGIN;SELECT * FROM hero WHERE number = 15 FOR UPDATE;

是没有问题的,因为在T2执行时,事务T1已经释放掉了number值为15的记录的锁,但是如果你先执行T2,再执行T1,由于T2已经持有了number值为15的记录的锁,事务T1将因为获取不到这个锁而等待。

我们再看一个使用主键进行范围查询的例子:

SELECT * FROM hero WHERE number >= 8 LOCK IN SHARE MODE;

这个语句的执行过程其实和我们举的上一个例子类似。也是先到聚簇索引中定位到满足number >= 8这个条件的第一条记录,也就是number值为8的记录,然后就可以沿着由记录组成的单向链表一路向后找,每找到一条记录,就会为其加上锁,然后判断该记录符不符合范围查询的边界条件,不过这里的边界条件比较特殊:number >= 8,只要记录不小于8就算符合边界条件,所以判断和没判断是一样一样的。最后把这条记录返回给server层server层再判断number >= 8这个条件是否成立,如果成立的话就发送给客户端,否则的话就结束查询。不过InnoDB存储引擎找到索引中的最后一条记录,也就是Supremum伪记录之后,在存储引擎内部就可以立即判断这是一条伪记录,不必要返回给server层处理,也没必要给它也加上锁(也就是说在第1步中就压根儿没给这条记录加锁)。整个过程会给number值为81520这三条记录加上S型正经记录锁,画个图表示一下就是这样:

15332916-9c79738b752975d4
image
  • 使用SELECT ... FOR UPDATE语句来为记录加锁:

    SELECT ... FOR UPDATE语句类似,只不过加的是X型正经记录锁

  • 使用UPDATE ...来为记录加锁,比方说:

    UPDATE hero SET country = '汉' WHERE number >= 8;
    

    这条UPDATE语句并没有更新二级索引列,加锁方式和上边所说的SELECT ... FOR UPDATE语句一致。

    如果UPDATE语句中更新了二级索引列,比方说:

    UPDATE hero SET name = 'cao曹操' WHERE number >= 8;
    

    这时候会首先更新聚簇索引记录,再更新对应的二级索引记录,所以加锁的步骤就是:

  1. number值为8的聚簇索引记录加上X型正经记录锁

  2. 然后为上一步中的记录索引记录对应的idx_name二级索引记录加上X型正经记录锁

  3. number值为15的聚簇索引记录加上X型正经记录锁

  4. 然后为上一步中的记录索引记录对应的idx_name二级索引记录加上X型正经记录锁

  5. number值为20的聚簇索引记录加上X型正经记录锁

  6. 然后为上一步中的记录索引记录对应的idx_name二级索引记录加上X型正经记录锁

画个图就是这样:

15332916-76c552a96d912b5a
image

如果是下边这个语句:

UPDATE hero SET namey = '汉' WHERE number <= 8;

则会对number值为138聚簇索引记录以及它们对应的二级索引记录加X型正经记录锁,加锁顺序和上边语句中的加锁顺序类似,都是先对一条聚簇索引记录加锁后,再给对应的二级索引记录加锁。之后会继续对number值为15的聚簇索引记录加锁,但是随后InnoDB存储引擎判断它不符合边界条件,随即会释放掉该聚簇索引记录上的锁(注意这个过程中没有对number值为15的聚簇索引记录对应的二级索引记录加锁)。具体示意图就不画了。

  • 使用DELETE ...来为记录加锁,比方说:

    DELETE FROM hero WHERE number >= 8;
    

    DELETE FROM hero WHERE number <= 8;
    

    这两个语句的加锁情况和更新带有二级索引列的UPDATE语句一致,就不画图了。

对于使用二级索引进行等值查询的情况

小贴士: 在READ UNCOMMITTED和READ COMMITTED隔离级别下,使用普通的二级索引和唯一二级索引进行加锁的过程是一样的,所以我们也就不分开讨论了。

  • 使用SELECT ... LOCK IN SHARE MODE来为记录加锁,比方说:

    SELECT * FROM hero WHERE name = 'c曹操' LOCK IN SHARE MODE;
    

    这个语句的执行过程是先通过二级索引idx_name定位到满足name = 'c曹操'条件的二级索引记录,然后进行回表操作。所以先要对二级索引记录加S型正经记录锁,然后再给对应的聚簇索引记录加S型正经记录锁,示意图如下:

    15332916-2d91fd2e74a1df88
    image

    这里需要再次强调一下这个语句的加锁顺序:

  1. 先对name列为'c曹操'二级索引记录进行加锁。

  2. 再对相应的聚簇索引记录进行加锁

小贴士: 我们知道idx_name是一个普通的二级索引,到idx_name索引中定位到满足name= 'c曹操'这个条件的第一条记录后,就可以沿着这条记录一路向后找。可是从我们上边的描述中可以看出来,并没有对下一条二级索引记录进行加锁,这是为什么呢?这是因为设计InnoDB的大叔对等值匹配的条件有特殊处理,他们规定在InnoDB存储引擎层查找到当前记录的下一条记录时,在对其加锁前就直接判断该记录是否满足等值匹配的条件,如果不满足直接返回(也就是不加锁了),否则的话需要将其加锁后再返回给server层。所以这里也就不需要对下一条二级索引记录进行加锁了。

现在要介绍一个非常有趣的事情,我们假设上边这个语句在事务T1中运行,然后事务T2中运行下边一个我们之前介绍过的语句:

UPDATE hero SET name = '曹操' WHERE number = 8;

这两个语句都是要对number值为8的聚簇索引记录和对应的二级索引记录加锁,但是不同点是加锁的顺序不一样。这个UPDATE语句是先对聚簇索引记录进行加锁,后对二级索引记录进行加锁,如果在不同事务中运行上述两个语句,可能发生一种贼奇妙的事情 ——

  • 事务T2持有了聚簇索引记录的锁,事务T1持有了二级索引记录的锁。

  • 事务T2在等待获取二级索引记录上的锁,事务T1在等待获取聚簇索引记录上的锁。

两个事务都分别持有一个锁,而且都在等待对方已经持有的那个锁,这种情况就是所谓的死锁,两个事务都无法运行下去,必须选择一个进行回滚,对性能影响比较大。

  • 使用SELECT ... FOR UPDATE语句时,比如:

    SELECT * FROM hero WHERE name = 'c曹操' FOR UPDATE;
    

    这种情况下与SELECT ... LOCK IN SHARE MODE语句的加锁情况类似,都是给访问到的二级索引记录和对应的聚簇索引记录加锁,只不过加的是X型正经记录锁罢了。

  • 使用UPDATE ...来为记录加锁,比方说:

    与更新二级索引记录的SELECT ... FOR UPDATE的加锁情况类似,不过如果被更新的列中还有别的二级索引列的话,对应的二级索引记录也会被加锁。

  • 使用DELETE ...来为记录加锁,比方说:

    SELECT ... FOR UPDATE的加锁情况类似,不过如果表中还有别的二级索引列的话,对应的二级索引记录也会被加锁。

对于使用二级索引进行范围查询的情况
  • 使用SELECT ... LOCK IN SHARE MODE来为记录加锁,比方说:

    SELECT * FROM hero FORCE INDEX(idx_name)  WHERE name >= 'c曹操' LOCK IN SHARE MODE;
    

    小贴士: 因为优化器会计算使用二级索引进行查询的成本,在成本较大时可能选择以全表扫描的方式来执行查询,所以我们这里使用FORCE INDEX(idx_name)来强制使用二级索引idx_name来执行查询。

    这个语句的执行过程其实是先到二级索引中定位到满足name >= 'c曹操'的第一条记录,也就是name值为c曹操的记录,然后就可以沿着这条记录的链表一路向后找,从二级索引idx_name的示意图中可以看出,所有的用户记录都满足name >= 'c曹操'的这个条件,所以所有的二级索引记录都会被加S型正经记录锁,它们对应的聚簇索引记录也会被加S型正经记录锁。不过需要注意一下加锁顺序,对一条二级索引记录加锁完后,会接着对它相应的聚簇索引记录加锁,完后才会对下一条二级索引记录进行加锁,以此类推~ 画个图表示一下就是这样:

    15332916-95fce94d10c566a3
    image

    再来看下边这个语句:

    SELECT * FROM hero FORCE INDEX(idx_name) WHERE name <= 'c曹操' LOCK IN SHARE MODE;
    

    这个语句的加锁情况就有点儿有趣了。前边说在使用number <= 8这个条件的语句中,需要把number值为15的记录也加一个锁,之后又判断它不符合边界条件而把锁释放掉。而对于查询条件name <= 'c曹操'的语句来说,执行该语句需要使用到二级索引,而与二级索引相关的条件是可以使用索引条件下推这个特性的。设计InnoDB的大叔规定,如果一条记录不符合索引条件下推中的条件的话,直接跳到下一条记录(这个过程根本不将其返回到server层),如果这已经是最后一条记录,那么直接向server层报告查询完毕。但是这里头有个问题呀:先对一条记录加了锁,然后再判断该记录是不是符合索引条件下推的条件,如果不符合直接跳到下一条记录或者直接向server层报告查询完毕,这个过程中并没有把那条被加锁的记录上的锁释放掉呀!!!。本例中使用的查询条件是name <= 'c曹操',在为name值为'c曹操'的二级索引记录以及它对应的聚簇索引加锁之后,会接着二级索引中的下一条记录,也就是name值为'l刘备'的那条二级索引记录,由于该记录不符合索引条件下推的条件,而且是范围查询的最后一条记录,会直接向server层报告查询完毕,重点是这个过程中并不会释放name值为'l刘备'的二级索引记录上的锁,也就导致了语句执行完毕时的加锁情况如下所示:

    15332916-8a315138f404e5fd
    image

    这样子会造成一个尴尬情况,假如T1执行了上述语句并且尚未提交,T2再执行这个语句:

    SELECT * FROM hero WHERE name = 'l刘备' FOR UPDATE;
    

    T2中的语句需要获取name值为l刘备的二级索引记录上的X型正经记录锁,而T1中仍然持有name值为l刘备的二级索引记录上的S型正经记录锁,这就造成了T2获取不到锁而进入等待状态。

    小贴士: 为啥不能释放不符合索引条件下推中的条件的二级索引记录上的锁呢?这个问题我也没想明白,人家就是这么规定的,如果有明白的小伙伴可以加我微信 xiaohaizi4919 来讨论一下哈~ 再强调一下,我使用的MySQL版本是5.7.21,不保证其他版本中的加锁情景是否完全一致。

  • 使用SELECT ... FOR UPDATE语句时:

    SELECT ... FOR UPDATE语句类似,只不过加的是X型正经记录锁

  • 使用UPDATE ...来为记录加锁,比方说:

    UPDATE hero SET country = '汉' WHERE name >= 'c曹操';
    

    小贴士: FORCE INDEX只对SELECT语句起作用,UPDATE语句虽然支持该语法,但实质上不起作用,DELETE语句压根儿不支持该语法。

    假设该语句执行时使用了idx_name二级索引来进行锁定读,那么它的加锁方式和上边所说的SELECT ... FOR UPDATE语句一致。如果有其他二级索引列也被更新,那么也会为对应的二级索引记录进行加锁,就不赘述了。不过还有一个有趣的情况,比方说:

    UPDATE hero SET country = '汉' WHERE name <= 'c曹操';
    

    我们前边说的索引条件下推这个特性只适用于SELECT语句,也就是说UPDATE语句中无法使用,那么这个语句就会为name值为'c曹操''l刘备'的二级索引记录以及它们对应的聚簇索引进行加锁,之后在判断边界条件时发现name值为'l刘备'的二级索引记录不符合name <= 'c曹操'条件,再把该二级索引记录和对应的聚簇索引记录上的锁释放掉。这个过程如下图所示:

    15332916-36620361a57cf61f
    image
  • Use DELETE ...to lock the record, he says:

    DELETE FROM hero WHERE name >= 'c曹操';
    

    with

    DELETE FROM hero WHERE name <= 'c曹操';
    

    If these two statements using the secondary index to be 锁定读, then they are locked and updating the index column with two UPDATEstatements consistent, not drawing up.

Case full table scan

for example:

SELECT * FROM hero WHERE country  = '魏' LOCK IN SHARE MODE;

Since countryunindexed column, so only use a full table scan approach to the implementation of this query, the storage engine reads each record a clustered index, it will lock a record for this article S型正常记录锁, and then return to server层, if the server层judge country = '魏'this condition is satisfied, if set up, it is sent to the client, otherwise it can release the lock on the record, draw a map like this:

15332916-f48ec22db730cae9
image

Use SELECT ... FOR UPDATEcase for locking the top similar, but added that X型正经记录锁, not go into details.

For UPDATE ...and DELETE ...statements, the record in traversing the clustered index, clustered index will be recorded for the add X型正经记录锁, then:

  • If the clustered index record does not satisfy the conditions for direct lock on the record released.

  • If the clustered index record satisfies the criteria, it will record the corresponding secondary index plus X型正经记录锁( DELETElock statement on all secondary index column, UPDATEthe statement will only update the secondary index record corresponding to the secondary index column lock ).

To be continued

The next chapter continues to nag at REPEATABLE READ isolation level, lock case all kinds of statements, as well as lock cases INSERT statement, so stay tuned. If you have any questions about the text, please contact the author: xiaohaizi4919 (serious micro-channel, pull the calf Do not add)

Reproduced in: https: //www.jianshu.com/p/2604bbeb7414

Guess you like

Origin blog.csdn.net/weixin_34067102/article/details/91337815