本文讨论的锁都是innodb 的行锁,不涉及譬如MDL LOCK/TABLE LOCK等锁,这也是最常见的。
一、LOCK SYSTEM的拆锁改进简述
在8.0种lock system和5.7显著的不同就是进行的锁的拆分,主要是分为2个方面
- 拆分锁为GLOBAL锁和shard锁。
- 对于shard锁来讲,一共有512个锁,通过page和heap no在LOCK SYSTEM获取锁的时候通常只需要上对应部分的shard锁
/** Number of page shards, and also number of table shards.
Must be a power of two */
static constexpr size_t SHARDS_COUNT = 512;
因此8.0在LOCK SYSTEM MUTEX热点锁竞争方面已经有了改进,因此在大批量加锁的情况下,本热点锁已经在实际环境中少见了。比如在5.7 我们大批量加锁的时候可能有如下:
而在实际运维中8.0貌似还没见到过,这可能就得益于这里的拆分。
二、对行进行加锁
如果某个page已经包含了锁信息,通常会使用lock_rec_lock_slow函数进行加锁判定,
1.首先需要通过本事务本次需要加的锁和已经持有的锁进行判定,如果已经有强度更高或者相同的锁,则不需要加锁了。(lock_rec_has_expl函数开始)判定的时候就是通过page no和heap no在LOCK SYSTEM的rec_hash中进行查找,而这里加锁就是前面说的拆分后的。其中主要通过Lock_iter::for_each通过lambda函数,先定位到rec_hash的某个链表,然后顺序访问通过函数lock_mode_stronger_or_eq进行比较。而函数lock_mode_stronger_or_eq就是进行锁强度的判定的,实际上就一句
return (lock_strength_matrix[mode1][mode2] != 0)
static const byte lock_strength_matrix[5][5] = {
/** IS IX S X AI */
/* IS */ {
TRUE, FALSE, FALSE, FALSE, FALSE},
/* IX */ {
TRUE, TRUE, FALSE, FALSE, FALSE},
/* S */ {
TRUE, FALSE, TRUE, FALSE, FALSE},
/* X */ {
TRUE, TRUE, TRUE, TRUE, TRUE},
/* AI */ {
FALSE, FALSE, FALSE, FALSE, TRUE}};
也就是通过兼容矩阵进行强度判定。
当然这里判定的时候除了兼容矩阵强度判定还必须是当前事务的锁才行,因此如下:
lock->trx == trx
要满足这一条才是前提。并且这里从语法来讲有个完美转发右值引入的方式,尽量节省内存消耗。
2.当然如果不满足上面的条件,则需要进行加锁,但是在加锁之前理所应当的需要判定是否需要等待。(lock_rec_other_has_conflicting函数开始)
实际上需要找到的是是否有事务持有锁,而堵塞了本事务需要获取的锁,那么也需要对LOCK SYSTEM的rec_hash进行迭代,同上面一样,也是通过Lock_iter::for_each进行的遍历,但是lambda函数不一样,这里主要调用的是lock_rec_has_to_wait函数进行判定,需要满足的条件主要是:
- 根据兼容矩阵判定lock_mode_compatible函数,其实也是一句话如下,
return (lock_compatibility_matrix[mode1][mode2]);
- 其次找到的锁的持有者不是本事务,如下,
trx != lock2->trx
这里对于SQL线程持有的锁会设置为高优先级,这个会特殊处理,对于高优先级来讲当持有锁的trx释放后,会优先持有锁,这个在后面会看到。
经过这个过程就能找到本次加锁需要等待的锁资源是哪个。
3.如果需要等待,也就是上面的步骤找到堵塞的锁,那么需要的事情比较多,主要集中在函数rec_lock.add_to_waitq中。
先是需要新建一个RecLock的辅助结构,主要是线程结构、事务结构、锁模式、索引名称、page no/heap no作为构造参数。
然后调用rec_lock.add_to_waitq,先将锁模式设置为 LOCK_WAIT,如下:
bool wait = m_mode & LOCK_WAIT
然后创建lock_t结构并且为其分配内存,在分配内存的时候,innodb有一个rec_pool的概念,在初始化的时候就已经初始化了REC_LOCK_CACHE(8)个lock_t结构,如果加锁不多那么分配内存这一块就直接从rec_pool中拿,否则需要实际分配内存,其构造主要是输入的space/page no/heap no/锁模型等信息,主要最后会调用lock_rec_set_nth_bit来设置lock_t结构的bit位,并且lock->trx->lock.n_rec_locks这个信息+1,这个信息是我们show engine看到的锁数量的信息。
接着需要做的就是将建立好的lock_t结构放入响应的链表或者属性中,调用的是RecLock::lock_add,这是我们如下
- lock_rec_insert_to_waiting:将锁放到相应LOCK SYSTEM的rec_hash某个链表(cell)的尾部(Insert lock record to the tail of the queue where the WAITING locks reside)
- locksys::add_to_trx_locks:将锁放入到事务相关的链表trx->lock.trx_locks的尾部
- lock_set_lock_and_trx_wait:将锁放入到本事务trx->lock.wait_lock中
上面将本次等待锁的信息建立好了,并且放入了响应的链表或者属性中,接下来就是要