理解 MySQL 意向锁
准备
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_locks 和 trx->lock.table_locks.
MySQL为了支持多粒度的锁, 引入了意向锁,意向锁是一种可以与行锁共存的锁, 例如 SELECT ... FOR SHARE 设置了 IS 意向共享锁, 而 SELECT ... FOR UPDATE设置了IX意向排他锁. 意向锁的上锁原则如下:
-
当一个事务对一个表的某一行记录申请 record 共享锁(行锁), 需要先申请
IS意向共享锁(表锁). -
当一个事务对一个表的某一行记录申请 record 排他锁(行锁), 需要先申请
IX意向排他锁(表锁).
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 锁, 表级锁的申请过程如下:
/* 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 锁:
----------------
| 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 状态.
总结
- MySQL支持的意向锁之间互不排斥,除了 IS 与 S 锁兼容外,意向锁会与 共享锁/ 排他锁 互斥.
- IX,IS是表级锁,不会和行级的X,S锁发生冲突.