表的数据与结构如下:
mysql> select * from unlockt;
+—-+——+——+
| a | b | c |
+—-+——+——+
| 1 | 1 | 1 |
| 2 | 2 | 2 |
| 5 | 5 | 5 |
| 7 | 7 | 7 |
| 8 | 8 | 8 |
| 9 | 9 | 9 |
| 10 | 10 | 10 |
| 11 | 11 | 11 |
+—-+——+——+
8 rows in set (0.01 sec)
CREATE TABLE \”unlockt\” (
\”a\” int(11) NOT NULLAUTO_INCREMENT,
\”b\” int(11) DEFAULTNULL,
\”c\” int(11) DEFAULTNULL,
PRIMARY KEY (\”a\”),
UNIQUE KEY \”b\”(\”b\”)
) ENGINE=InnoDB
事务1:
mysql> 事务1 begin;
mysql> 事务2 begin;
事务1> delete from unlockt where a=2;
Query OK, 1 row affected (0.01 sec)
事务1> delete from unlockt where a=7;
Query OK, 1 row affected (0.00 sec)
事务2> delete from unlockt where a=5;
Query OK, 1 row affected (0.01 sec)
事物2> delete from unlockt where a=8;
Query OK, 1 row affected (0.01 sec)
事务1> insert into unlockt values(2,2,2); lock wait。。。。。。。,最后执行成功。
Query OK, 1 row affected (18.69 sec)
事务2> insert into unlockt values(5,5,5);
ERROR 1213 (40001): Deadlock found when trying to get lock; try restartingtransaction
事物2回滚。
估计大多数人会比较好奇。因为这两个事务操作的是完全不同的行,且删除是通过主键索引加锁的,为什么就出现死锁了? 猜测&想知道原因?那好,我们来慢慢剖析其中的原因吧!
我们先来打印一下死锁信息,如下:
LATEST DETECTED DEADLOCK
————————
2016-01-07 16:57:02 0x7f6f4d14b700
*** (1) TRANSACTION:
TRANSACTION 13204, ACTIVE 82 sec inserting
mysql tables in use 1, locked 1
LOCK WAIT 6 lock struct(s), heap size 1160, 7 row lock(s), undo logentries 3
MySQL thread id 2, OS thread handle 140116011566848, query id 194localhost root update
insert into unlockt values(2,2,2)
*** (1) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 56 page no 4 n bits80 index b of table \”xcytest\”.\”unlockt\” trx id 13204lock mode S waiting
Record lock, heap no 9 PHYSICAL RECORD: n_fields 2;compact format; info bits 32
0: len 4; hex 80000005; asc ;;
1: len 4; hex 80000005; asc ;;
以上是事务1的锁信息,等待索引B上的某行的S锁, 等待的行的heap no(将其理解为数据页内的行的唯一号就行) 为9,有两列(索引列加主键值),列值为
0: len 4; hex80000005; asc ;; 第一列,索引列,长度为4( int类型字段的长度),值为80000005,也就是十进制5。
1: len 4; hex80000005; asc ;;第二列,主键值,也是为5。
此时,事务2已经执行deletefrom unlockt where a=5的sql,但未提交,所以这行(包括跟这个行有关索引记录)被事务2锁住,出现等待完全是合乎情理。
—–但这行,跟事务1执行insert into unlockt values(2,2,2) 有毛关系呢? 完全不相关的行。Xxx……, 亲怎么看?
还是继续把死锁信息中事务2看完吧,上述问题先搁置,把本篇仔细读完,会有非常清8000晰地答案。
*** (2) TRANSACTION:
TRANSACTION 13209, ACTIVE 51 sec inserting, thread declared inside InnoDB5000
mysql tables in use 1, locked 1
6 lock struct(s), heap size 1160, 6 row lock(s), undo log entries 3
MySQL thread id 3, OS thread handle 140116011300608, query id 195localhost root update
insert into unlockt values(5,5,5)
*** (2) HOLDS THE LOCK(S):
RECORD LOCKS space id 56 page no 4 n bits80 index b of table \”xcytest\”.\”unlockt\” trx id 13209 lock_mode X locks rec but not gap
Record lock,heap no 9 PHYSICAL RECORD: n_fields 2; compactformat; info bits 32
0: len 4; hex 80000005; asc ;;
1: len 4; hex 80000005; asc ;;
事务2持有b列索引上b=5,a=5的记录的排它锁,正是该锁堵住了事务1的insert 语句。
*** (2) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 56 page no 4 n bits 80 index b of table\”xcytest\”.\”unlockt\” trx id 13209 lockmode S waiting
Record lock, heap no 8 PHYSICAL RECORD: n_fields 2; compact format; infobits 32
0: len 4; hex 80000007;asc ;;
1: len 4; hex 80000007;asc ;;
*** WE ROLL BACK TRANSACTION (2)
事务2执行insertinto unlockt values(5,5,5)却等待b列索引上的(b=7,a=7)的记录的共享锁,但这个锁被事务1的delete 语句排它持有,因此形成环,造成死锁。 事务2被驱除。
喜欢思维发散的朋友可能会有以下两个疑问:
-
这个unlockt表上的a列为主键,b列上有唯一性索引,为啥冲突只出现在b列索引上。
-
还是之前的问题,为毛insert语句会去请求下一行的共享锁?
带着这些问题,我们来继续追根溯源的分析吧。
上面有两类操作,第一类是delete 操作,我们来分析一下delete的操作过程,数据库内部执行的过程描述起来,可以非常简单,也可以巨复杂,为了便于大家理解,只描述跟本主题有关系的大致过程:
对于delete from unlockt where a=5 这样的操作,过程如下:
第一步:因为a为主键,则我们会通过主键查找来找到对应的行,因为我们要对这行做delete操作,所以给其加排它锁(LOCK_X位图值为3 ),同时结合锁的类型(lock_rec_not_gap 位图值为1024) ,所以加锁的mode为1027,即LOCK_X|LOCK_REC_NOT_GAP的组合。当然,如果是在read_committed的事务隔离级别下,则没有这个LOCK_REC_NOT_GAP的属性。因为是主键,所以加的是lock_rec_not_gap锁。暂时点到为止,再继续扩展就没有主题了,把要说的事情也会变得巨复杂,会晕。
第二步:在主键(在mysql中,主键索引就是表)上找到行之后,就开始加锁,并在加锁成功后,就可以进行修改,在mysql的内部,一个delete操作,其实演变为一个update操作,将这个记录标记为delete。
在此,再提一个问题:因为这个表还有索引,索引也需要修改,为什么不去给索引上的记录进行加锁就开始修改表(主键)呢?如果不加锁,怎么保证索引上的记录在修改前后不发生变化?在mysql的数据库中,普通索引跟主键索引(表)都是索引,为什么在主键上进行delete需要加锁,而没有在普通索引上加锁,就开始修改主键(表)上的行了?
回答:因为所有的修改都是主键(表)先行的,先修改表,然后再修改索引,既然已经锁住了表上的记录了,也就没有其他的会话可以在你锁住主键之后去修改与该记录的相关索引上的节点(在你之前的会话是可以滴)。因此,在这个情况下,是不需要锁住普通索引上的相关记录的。
第三步:去修改所有的索引,将索引上与这行相关的记录给标记为delete,对索引修改之前,加锁的函数为lock_sec_rec_modify_check_and_lock,该函数会给索引优先加隐含锁,(隐含锁也就是不加锁)加锁成功后,会通过这个函数page_update_max_trx_id修改该行所在页的最大事务ID。
第四步:修改完成,如果是自动commit,则进行提交。否则,等待提交。
上面是整个基于主键的单行delete操作的加锁情况—只加一个行锁,行锁的对象是被修改的记录。对于普通索引上的记录,是不显式加锁的。
是否又有疑问?对于普通索引上被修改的记录,不加锁不?
回答: 是的,一般不加显式锁,如果不加显式锁,则在锁的hash表中找不到关于普通索引上相关的锁。也就是通过show engine innodbstatus \\G 命令,看到的锁的总数只包括该事务的显式锁,因此,在一般情况下,事务删除一行,通过该命令只会看到一个行锁。
疑问? 那别的会话也要修改普通索引上的这行该怎么办?不冲突了么?
别人想要修改之前,如果要先加索引上的锁(通过普通索引查找去做更新的操作,先锁住普通索引,然后再去锁表),加锁函数为lock_sec_rec_read_check_and_lock,会检查普通索引行上有没有其他的事务占有这行,(对于表(主键),因为每个行上都有两个对用户不可见的列,一个存放的是事务的id, 一个是回滚段上的地址,用于事务回滚,但普通索引因为没有这两个不可见的列,所以要多绕一步),如果有,也就是将隐含锁转换为显式锁(实现函数为lock_rec_convert_impl_to_expl_for_trx),在普通索引上,判断某行是否有活跃事务的流程如下,具体实现函数为lock_sec_rec_some_has_impl,判断规则如下:
Step 1 . max_trx_id = page_get_max_trx_id(page);获取所在的数据页的最大事务号。(每个页都记录着一个最大事务号)。
Step 2 . max_trx_id < trx_rw_min_trx_id() ,将获得的最大事务号跟数据库中的最小事务号比较,如果数据库中最小的事务号,则认为该页上的所有事务都已经提交,则无活跃事务。
Step 3。 !lock_check_trx_id_sanity(max_trx_id, rec, index,offsets)判断该页时一个合法的页,就是进行数据页合法性检查。 /* Thepage is corrupt: try to avoid a crash by returning 0 */
Step 4。如果以上条件都不满足,则进行主键(表)上进行判断,判断索引行对对应的主键行上有没有事务。下面这个函数就是去主键上检查是否有活跃事务,因为单纯从索引页上无法判断该行是否有活跃事务。
/* In this case it is possible thatsome transaction has an implicit
x-lock. We have to look in theclustered index. */
} else {
trx =row_vers_impl_x_locked(rec, index, offsets);
如果有,则返回事务id 。否则,不需要进行隐含锁转换。
下面是将详细解析隐含锁转换为显式锁的函数 lock_rec_convert_impl_to_expl(主键跟非主键之间有区别,区别上面解释)
通过判断事务id是否活跃(即是否提交)来判断当前行是否被其他会话占有。如果被其他的会话占有,则将隐含锁转换为对方占有的显式行锁,然后再判断是否可以继续加锁这行,判断动作也就是进行锁冲突检查。这也是当事务A与事务B执行两个delete之后,去查锁的情况,分别只会有两个行锁。然后在分别执行insert操作之后,出现死锁。从死锁输出看到,会发现事务A有7 row lock(s),而事务B有6 row lock(s)。 最后一个insert操作,突然多出来的5个行锁,除了insert本身带来的锁之外,还有2个来自于之前delete操作在普通索引上的隐含锁的转换。
疑问?为什么同样的操作A有7个行锁,事务B只有6个行锁?
回答:那是因为事务A的第7个行锁处于等待中,而事务B在申请把第7个行锁加入等待队列之前,会进行死锁检查,这时,发现已经出现死锁现象。此时,这个锁还没有创建。因此比事务A少了一个行锁。
说了这么多,还是没有说出死锁的原因,确实,上面的这些跟死锁现象有间接关系,但无直接关系。死锁的直接原因在后面。 上面的关于delete的加锁机制,权当是让大家了解mysql的内部实现吧。如果熟悉mysql的delete加锁的机制的,可以跳过。
从上面的实验现象中,当事务A执行到insert into unlock values(2,2,2)的时候,出现了锁等待。因为有事务出现锁等待是死锁的前提条件。所以,找到事务A为什么出现等待的原因,也就是找到了死锁产生的直接原因。
是否需要把insert into unlock values( 2,2,2) 的加锁过程分析一遍呢?因为这个insert语句,是造成死锁的直接原因,咱们还是不厌其烦的,仔细去了解insert的语句的加锁过程吧。
Insert的执行过程与delete不太一样,delete常规过程是先找到数据,然后加锁,然后修改行标志位为delete(delete过程实际就是update的过程)。 Insert的过程有两个大步骤,分别为往表里插入数据以及往索引里面插入数据(如果有索引)。插入之前,先根据sql语句传入的主键值(例如语句insert into unlock values(11,11,11),则传入的主键值为11),然后进行游标查找(这个查找是不加锁的),找到需要插入的位置(也就是某行),返回的游标里面,会登记是否找到匹配的关键字。如果有唯一性约束(或者主键),如果有匹配键值,则需要判断是否健值冲突。如果没有,则不需要判断是否健值冲突。在表上判断健值冲突的函数为row_ins_duplicate_error_in_clust, 利用插入位置的行跟要插入的行的值(11,11,11)进行比较,看是否主键冲突。因为要比较数据,为了防止被比较的数据在比较过程中发生修改,则需要对其进行加锁,通过函数row_ins_set_shared_rec_lock对插入位置的行加共享锁,如果加锁成功,则进行数据比较,否则进行锁等待。如果数据比较不冲突,则通过冲突检测,进行数据的插入。下面是对插入位置的行加锁的stack.
(注:限于文章字数限制,省掉函数调用栈)
当加锁成功之后,返回到函数row_ins_clust_index_entry_low,根据之前的游标查找,可以知道是否在表中有个相同的key的行,但标记为delete 状态。 因此,如果存在,则会调用row_ins_clust_index_entry_by_modify再次进行锁检查(以及加锁),然后将原来在表中的行修改为正常的行。当然,如果新行比之前的行的长度要长,被删除行的原有空间放不下新行,则在剩余中间中分配空间给新行,被删除的老行仍然存在,但老行会纪录新行的地址,新行做为老行的下一行。
对于表(主键)的插入过程解析到此为止,我们可以看到,在主键(表)上,会对插入位置进行加锁,如果该行是标记为删除的,则直接将这行修改。 下面是执行modify之前再次检查锁的栈。 在执行modify 的时候,会再次对行进行加锁,但传入的impl参数为true, 该参数的作用是,如果没有必要创建显式锁,则不需要创建锁结构,加的是隐含锁。
(gdb) bt
#0 lock_rec_lock (impl=true, mode=1027, block=0x7f63dfe96fa0, heap_no=
………………………………………………………
at/data/mysql/mysql-5.7.10/storage/innobase/btr/btr0cur.cc:3553
#3 0x0000000001ae190b in btr_cur_update_in_place(flags=0, cursor=0x7f63a36ac780, offsets=0x7f63a36ac880, update=0x7f62dc018420,
cmpl_info=0,thr=0x7f62dc017030, trx_id=15918, mtr=0x7f63a36acba0) at/data/mysql/mysql-5.7.10/storage/innobase/btr/btr0cur.cc:3863
………………………………………..
上面说了在表上(主键索引上)插入的加锁过程,以及简单的函数调用过程。当在主键插入完成之后,则需要在索引上插入。 因为表unlockt的b列上有唯一性索引,所以同样需要进行健值冲突检测。下面是对二级索引插入的栈,通过栈我们可以看到,是通过函数row_ins_scan_sec_index_for_duplicate来检查是否健值冲突。(主键冲突的检测的算法跟唯一性索引完全不一样,这也是上述实验中,insert为什么只等待唯一性索引上的锁的原因,锁冲突发生在唯一性索引上的原因)
(gdb) bt
#0 lock_rec_lock (impl=false,mode=2, block=0x7f63dfe972c8, heap_no=11, index=0x7f6304018150,thr=0x7f62dc017030)
………………………………………………..
#3 0x00000000019b3e8a in row_ins_scan_sec_index_for_duplicate(flags=0, index=0x7f6304018150,entry=0x7f62dc018160, thr=0x7f62dc017030,
s_latch=false, mtr=0x7f63a36ac460,offsets_heap=0x7f62dc0182d8)
。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。
。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。
我们来仔细检查该函数,通过分析该函数,我们就可以知道,为什么上面实验的sql的insert 语句会请求下一行的共享锁。 对代码不感兴趣的朋友请直接跳过代码,在代码段后面,会直接给出答案。
/***************************************************************//**
Scans a unique non-clustered index at a given index entry to determine
whether a uniqueness violation has occurred for the key value of theentry.
Set shared locks on possible duplicate records.
@return DB_SUCCESS, DB_DUPLICATE_KEY, or DB_LOCK_WAIT */
static__attribute__((nonnull, warn_unused_result))
dberr_t
row_ins_scan_sec_index_for_duplicate(
/*=================================*/
ulint flags, /*!< in: undo logging andlocking flags */
dict_index_t* index, /*!< in: non-clusteredunique index */
dtuple_t* entry, /*!< in: index entry */
que_thr_t* thr, /*!< in: query thread */
bool s_latch,/*!< in: whether index->lock isbeing held */
mtr_t* mtr, /*!< in/out:mini-transaction */
mem_heap_t*offsets_heap)
/*!< in/out: memory heapthat can be emptied */
{
。。。。。。。。。。。。。。。。。。。。。
cmp = cmp_dtuple_rec(entry, rec, offsets);
if (cmp == 0 && !index->allow_duplicates) {
if (row_ins_dupl_error_with_rec(rec, entry,
index, offsets)) {
err = DB_DUPLICATE_KEY;
thr_get_trx(thr)->error_info = index;
/* If the duplicate is onhidden FTS_DOC_ID,
state so in the errorlog */
if (index == index->table->fts_doc_id_index
&& DICT_TF2_FLAG_IS_SET(
index->table,
DICT_TF2_FTS_HAS_DOC_ID)) {
ib::error() << \”DuplicateFTS_DOC_ID\”
\” value ontable \”
<< index->table->name;
}
goto end_scan;
}
} else {
ut_a(cmp < 0 || index->allow_duplicates);
goto end_scan;
}
} while (btr_pcur_move_to_next(&pcur, mtr));
end_scan:
/* Restore old value */
dtuple_set_n_fields_cmp(entry, n_fields_cmp);
DBUG_RETURN(err);
}
在代码中,有这么一行cmp =cmp_dtuple_rec(entry, rec, offsets);这个就是将在唯一性索引上找到的行跟要插入的key值进行比较,如果相等则cmp会等于0,会进入下面代码段。
if (cmp == 0 && !index->allow_duplicates) {
if (row_ins_dupl_error_with_rec(rec, entry,
index, offsets)) {
err =DB_DUPLICATE_KEY;
。。。。。。。。。。。。。。。。。。。。。。
goto end_scan;
}
} else {
ut_a(cmp < 0 || index->allow_duplicates);
goto end_scan;
}
} while(btr_pcur_move_to_next(&pcur, mtr));
首先进行(row_ins_dupl_error_with_rec(rec,entry,index, offsets)),因为有cmp等于0 ,所以找到的行的字段值必定跟entry相等,该函数判断的是——该行是否已经删除,如果已经删除,则不冲突,返回false , 如果不是删除的行,则冲突,最终给用户返回唯一性索引冲突的错误。如果cmp等于0 ,但又不冲突,则会在进行下一行对比,也就是该函数(btr_pcur_move_to_next(&pcur,mtr))的作用,在比对下一行之前,会对下一行进行加锁。
直至插入位置查找完成,即cmp<0 ,即要插入的key 比找到的行的key 小。Insert语句要插入的行的插入位置,要么就是找到的行的位置(该行已经删除),要么就是在找到行的前面插入。
现在我们回过头来看执行事务A,与事务B的操作过程: a=5 的行位于a=2的下一行,中间没有a=3,a=4的行:
事务1:
mysql> 事务1 begin;
mysql> 事务2 begin;
事务1> deletefrom unlockt where a=2;
Query OK, 1 row affected (0.01 sec)
事务1> deletefrom unlockt where a=7;
Query OK, 1 row affected (0.00 sec)
事务2> deletefrom unlockt where a=5;
Query OK, 1 row affected (0.01 sec)
事物2> delete from unlockt where a=8;
Query OK, 1 row affected (0.01 sec)
事务1> insert intounlockt values(2,2,2); lock wait。。。。。。。,最后执行成功。
Query OK, 1 row affected (18.69 sec)
事务2> insertinto unlockt values(5,5,5);
ERROR 1213 (40001): Deadlock found when trying to get lock; try restartingtransaction
当事务A执行insert into unlockt values(2,2,2); 的时候,在主键上会找到被自己事务之前delete的行,然后对这行进行数据修改,接下来,再去修改b列索引,首先b列索引上通过b=2,找到已经被删除的b=2的行,在函数row_ins_scan_sec_index_for_duplicate中被过滤,原因是认为其一个无效的行,通过该行无法判断是否还有其他的行b=2 , 所以继续检测一下一条记录,下一个记录为b=5 ,被事务2删除但没有提交,所以无法加锁,因此出现了锁等待。。。。。
再回到之前提的问题:
-
这个unlockt表上的a列为主键,b列上有唯一性索引,为啥冲突只出现在b列索引上。
在主键上,发现有删除的记录,直接修改。
-
还是之前的问题,为毛insert 语句会去请求下一行的共享锁?
这个刚才已经回答,是因为要插入的记录之前被自己的事务删除导致其在二级索引上,需要找下一行来做是否重复判断。
最后,再提最后一个问题,如果不需要做测试就知道答案,证明亲就完全理解上面的所有知识,以及让知识升华了。
问题如下:表还是一样的表,数据还是一样的数据,依然是两个事务,各自做两个delete加一个Insert,详细过程如下:
mysql> select *from unlockt;
+—-+——+——+
| a | b | c |
+—-+——+——+
| 1 | 1 | 1 |
| 2 | 2 | 2 |
| 5 | 5 | 5 |
| 7 | 7 | 7 |
| 8 | 8 | 8 |
| 9 | 9 | 9 |
| 10 | 10 | 10 |
| 11 | 11 | 11 |
+—-+——+——+
8 rows in set (0.01 sec)
CREATE TABLE \”unlockt\” (
\”a\” int(11) NOT NULLAUTO_INCREMENT,
\”b\” int(11) DEFAULTNULL,
\”c\” int(11) DEFAULTNULL,
PRIMARY KEY (\”a\”),
UNIQUE KEY \”b\”(\”b\”)
) ENGINE=InnoDB
事务1:
mysql> 事务1 begin;
mysql> 事务2 begin;
事务1> delete from unlockt where a=2;
Query OK, 1 row affected (0.01 sec)
事务1> delete from unlockt where a=5; (第一个实验中事务1是删除a=7的行,事务1仅仅这个操作有变化)
Query OK, 1 row affected (0.00 sec)
事务2> delete from unlockt where a=7; (第一个实验中事务2是删除a=5的行,事务2仅仅这个操作有变化)
Query OK, 1 row affected (0.01 sec)
事物2> delete from unlockt where a=8;
Query OK, 1 row affected (0.01 sec)
事务1> insert into unlockt values(2,2,2); 结果为如何? 是否会等待?或者最终回滚?
事务2> insert into unlockt values(5,5,5); 结果为如何? 是否会等待?或者最终回滚?
如果不知道最终结果如何,请进行测试。如果测试后对结果依然不是非常理解,联系作者,请求帮助,呵呵呵!!!
在最后,再发散一个问题,如果上述的表unlockt的b列索引是非唯一性索引,还会出现死锁吗 ?