准备

MySQL内核版本: 8.0.17

理解lock和latch

latch

数据库中的 latch 和我们通常代码编程中保证并发多线程操作操作临界资源的锁意义一样,通过 latch 的中文翻译“闩”就可以理解,这是为了维护一段临界区域.

lock

而lock则是数据库MySQL中在事务使用的”锁”, 锁定的对象是表或者行. 关于MySQL的死锁可以查看另外一篇文章MySQL死锁检测.

锁的类型

  • 行锁

  • 意向锁

  • Gap锁

意向锁

表级别锁的兼容互斥矩阵:

X IX S IS
X Conflict Conflict Conflict Conflict
IX Conflict Compatible Conflict Compatible
S Conflict Conflict Compatible Compatible
IS Conflict Compatible Compatible Compatible

需要注意上图矩阵的X, IX, S, IS锁均为表锁,并不代表行锁.

锁的含义:

X: 排他锁
IX: 意向排他锁
S: 共享锁
IS: 意向共享锁

在一个事务trx_t中用结果trx_lock_t来存放事务申请的锁信息, 包括行锁和表锁, 即trx->lock.trx_lockstrx->lock.table_locks.

MySQL为了支持多粒度的锁, 引入了意向锁,意向锁是一种可以与行锁共存的锁, 例如SELECT ... FOR SHARE设置了IS意向共享锁, 而SELECT ... FOR UPDATE设置了IX意向排他锁. 意向锁的上锁原则如下:

  • 当一个事务对一个表的某一行记录申请 record 共享锁(行锁), 需要先申请IS意向共享锁(表锁).

  • 当一个事务对一个表的某一行记录申请 record 排他锁(行锁), 需要先申请IS意向排他锁(表锁).

X,IS是表级锁,不会和行级的X,S锁发生冲突, 只会和表级的X,S发生冲突. 行级别的X和S只与其它行锁存在普通的共享、排他规则. 而意向锁的意义是当需要向一张表添加表级X锁时,假如没有意向锁,需要遍历lock_sys->rec_hash判断是否与该X锁存在冲突的锁.

源码分析

我们以源码分析的方式来直观的理解意向锁的加锁过程,我们以 update 一条 record 获取 IX 锁为例:

在 IX 锁申请之前,会对当前表(dict_table_t)记录的锁信息的兼容情况进行判断(lock_table_other_has_incompatible()), 符合兼容矩阵的从而在row_upd_step()函数中调用lock_table()申请 IX 锁, 表级锁的申请过程如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
/* storage/innobase/lock/lock0lock.cc */

UNIV_INLINE
lock_t*
lock_table_create(
/*==============*/
dict_table_t* table, /*!< in/out: database table
in dictionary cache */
ulint type_mode,/*!< in: lock mode possibly ORed with
LOCK_WAIT */
trx_t* trx) /*!< in: trx */
{
lock_t* lock;

ut_ad(table && trx);
ut_ad(lock_mutex_own());
ut_ad(trx_mutex_own(trx));

/* 检查事务状态. */
check_trx_state(trx);

if ((type_mode & LOCK_MODE_MASK) == LOCK_AUTO_INC) {
++table->n_waiting_or_granted_auto_inc_locks;
}

if (type_mode == LOCK_AUTO_INC) {
/* 对于AUTOINC 锁可以直接复用. */
lock = table->autoinc_lock;

table->autoinc_trx = trx;

ib_vector_push(trx->autoinc_locks, &lock);

} else if (trx->lock.table_cached < trx->lock.table_pool.size()) {
/* 假如trx的table_pool有预先申请的table lock. */
lock = trx->lock.table_pool[trx->lock.table_cached++];
} else {
/* 否则通过内存分配一个table lock. */
lock = static_cast<lock_t*>(
mem_heap_alloc(trx->lock.lock_heap, sizeof(*lock)));

}

/* 设置lock相关的数据变量. */
lock->type_mode = ib_uint32_t(type_mode | LOCK_TABLE);
lock->trx = trx;

lock->un_member.tab_lock.table = table;

ut_ad(table->n_ref_count > 0 || !table->can_be_evicted);

/* 插入trx->lock的trx_locks. */
UT_LIST_ADD_LAST(trx->lock.trx_locks, lock);

ut_list_append(table->locks, lock, TableLockGetNode());

if (type_mode & LOCK_WAIT) {
/* 假如设置了LOCK_WAIT状态,需要设置lock.wait_lock. */
lock_set_lock_and_trx_wait(lock, trx);
}

/* 插入trx->lock的table_locks. */
lock->trx->lock.table_locks.push_back(lock);

MONITOR_INC(MONITOR_TABLELOCK_CREATED);
MONITOR_INC(MONITOR_NUM_TABLELOCK);

return(lock);
}

row_upd_step()完成申请IX意向排他锁后继续调用row_upd_clust_step(), 而row_upd_clust_step()调用lock_clust_rec_modify_check_and_lock()对修改的 record 申请 X 锁:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
 ----------------
| row_upd_step() | /* 申请 IX 锁. */
----------------
|
| ----------------
-> | ... |
----------------
|
| ----------------------
-> | row_upd_clust_step() |
----------------------
|
| ----------------------------------------
-> | lock_clust_rec_modify_check_and_lock() | /* 申请 record 的 X 锁. */
| ----------------------------------------
|
| ---------------
-> | ... |
---------------

例如此时某一个用户正在使用lock table语句锁表,依然会进入lock_table_other_has_incompatible()判断表级锁的兼容情况,假如产生冲突,该用户线程则会进入 wait 状态.

总结

  1. MySQL支持的意向锁之间互不排斥,除了 IS 与 S 锁兼容外,意向锁会与 共享锁/ 排他锁 互斥.
  2. IX,IS是表级锁,不会和行级的X,S锁发生冲突.