本文的目的是对 InnoDB 的锁模块做个简单的介绍,使读者对这块有初步的认识。
此外,我们在对MySQL 5.7做性能分析的时候发现lock_sys mutex成为热点瓶颈,官方在MySQL 8.0上对lock_sys锁也做了很多优化,本文针对一些重大的性能优化做一些介绍。
MySQL lock 与 latch区别(本文主要介绍lock)
struct lock_sys_t{ LockMutex mutex; hash_table_t* rec_hash; hash_table_t* prdt_hash; hash_table_t* prdt_page_hash; LockMutex wait_mutex; srv_slot_t* waiting_threads; srv_slot_t* last_slot; int n_waiting;};
struct lock_t { trx_t* trx; UT_LIST_NODE_T(lock_t) trx_locks; dict_index_t* index; lock_t* hash;
union { lock_table_t tab_lock; lock_rec_t rec_lock; };
uint32_t type_mode;}
struct lock_rec_t { ib_uint32_t space; ib_uint32_t page_no; ib_uint32_t n_bits;}
struct lock_table_t { dict_table_t* table; UT_LIST_NODE_T(lock_t) locks;}
lock_t::type_mode:可以区分锁类型表锁、行锁(gap锁,非gap锁等);锁是否处于lock_wait状态;共享锁,排他锁,自增锁等锁模式。
lock_sys_t,lock_t,lock_rec_t,lock_table_t关系如下图所示:
/*用于判断不同事务的锁之间是否可以兼容*/static const byte lock_compatibility_matrix[5][5] = { /** IS IX S X AI */ /* IS */ {TRUE, TRUE, TRUE, FALSE, TRUE}, /* IX */ {TRUE, TRUE, FALSE, FALSE, TRUE}, /* S */ {TRUE, FALSE, TRUE, FALSE, FALSE}, /* X */ {FALSE, FALSE, FALSE, FALSE, FALSE}, /* AI */ {TRUE, TRUE, FALSE, FALSE, FALSE}};
/*用于判断当前事务上是否已经存在满足强度的锁,如果存在,则不需要继续加锁*/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}};
以update为例解析加锁/解锁流程:
其中lock_table为表锁加锁,lock_rec_lock为行锁加锁,下面会详细解释表锁和行锁的加锁过程。
表锁加锁逻辑相对简单,步骤如下图所示:
如下图所示:
以下条件满足一个则快速加锁失败:if (lock_rec_get_next_on_page(lock) //page上有多个行锁 || lock->trx != trx //已经拥有该锁的事务不是当前事务 || lock->type_mode != (mode | LOCK_REC) //已有的锁和要加的锁锁模式是否一致 || lock_rec_get_n_bits(lock) <= heap_no) { //n_bits是否足够描述大小为 heap_no 的行 status = LOCK_REC_FAIL;}
加锁时若出现锁冲突,我们将该锁设置为lock_wait模式,并等待被唤醒重新加锁,以下为5种锁唤醒的场景。
事务提交时该事务将添加的所有行锁和表锁进行解锁,释放锁资源,lock_trx_release_locks,具体流程如下图所示:
行锁在一些特定场景中会发生变化,涉及锁分裂,迁移,继承等。
行锁类型:
LOCK_S、LOCK_X
GAP类型:
LOCK_GAP:只锁间隙
LOCK_REC_NO_GAP:只锁记录
LOCK_ORDINARY:锁记录和记录之前的间隙
LOCK_INSERT_INTENTION:插入意向锁,用于insert时检查锁冲突
每个行锁由锁类型和GAP类型组成,例如:LOCK_X|LOCK_ORDINARY 表示对记录和记录之前的间隙加排他锁,锁类型和GAP类型由type_mode控制。
插入的记录的间隙存在GAP锁,此时此GAP需分裂为两个GAP。
例:隔离级别RR
create table t1(c1 int primary key, c1 int unique)engine=innodb;
insert into t1 values(1,1)(2,2);
begin;# supremum 记录上加 LOCK_X|LOCK_GAP 锁住(1~)select * from t1 where c2=2 for update;# 发现插入(3,3)的间隙存在GAP锁,因此给(3,3)加LOCK_X | LOCK_GAP锁。这样依然锁住了(1~)insert into t1 values(3,3);
删除的记录前存在GAP锁,此GAP锁会继承到要删除记录的下一条记录上。
例:隔离级别RR
create table t1(c1 int primary key, c1 int unique)engine=innodb;
insert into t1 values(1,1)(2,2);
B树节点发生分裂,合并,删除都会引发锁的变化。锁迁移的原则是,B树结构变化前后,锁住的范围保证不变。
假设原节点A(infimum,1,3,supremum) 向右分裂为B(infimum,1,supremum), C(infimum,3,supremum)两个节点。
假设原节点A上记录3锁为LOCK_S|LOCK_ORIDNARY,supremum为LOCK_S|LOCK_GAP,实际锁住了(1~) 锁迁移过程大致为:
1)将3上的gap锁迁移到C节点3上
2)将A上supremum迁移继承到C的supremum上
3)将C上最小记录3的锁迁移继承到B的supremum上
迁移完成后锁的情况如下 B节点:suprmum LOCK_S|LOCK_GAP C节点:3 LOCK_S|LOCK_ORINARY, suprmum LOCK_S|GAP。
迁移后仍然锁住了范围(1~)。
以上述节点分裂的逆操作来讲述合并过程 B(infimum,1,supremum), C(infimum,3,supremum)两个节点,向左合并为A节点(infimum,1,3,supremum) 其中B,C节点锁情况如下:
B节点:suprmum LOCK_S|LOCK_GAP
C节点:3 LOCK_S|LOCK_ORINARY, suprmum LOCK_S|GAP
迁移流程如下(lock_update_merge_left):
1)将C节点锁记录3迁移到B节点
2)将B节点supremum迁移继承到A的supremum上
迁移后仍然锁住了范围(1~)。
如果删除节点存在左节点,则将删除节点符合条件的锁,迁移继承到左节点supremum上;否则将删除节点符合条件的锁,迁移继承到右节点最小用户记录上。参考lock_update_discard。
优化场景:发生锁等待时可以通过其它数据结构避免使用lock_sys mutex。
优化方式:
异步死锁优化前,死锁检测由用户线程同步进行;
优化后,用户线程无需死锁检测,lock_wait_timeout_thread线程除了检测锁超时,还需要进行死锁检测。
以行锁为例(表锁类似,将lock_sys->rec_hash换成table->locks)
优化前死锁检测步骤:
检测到死锁,选择当前事务或者m_wait_lock对应的事务进行回滚:
优化思路:
优化后死锁检测步骤
//死锁检测lock_wait_update_schedule_and_check_for_deadlocks-----lock_wait_snapshot_waiting_threads //遍历lock_sys->waiting_threads,将锁等待任务都放入infos数组, //数组每个元素都包括持锁事务,等锁事务等-----lock_wait_build_wait_for_graph //利用outgoing数组建立锁等待关系, outing数组中存储的是对应infos数组 //锁等待事务所在的数组下标。例如infos[0]={trx1,trx2},infos[1]={trx2,trx5}, //那么outgoing[0]=1;-----lock_wait_find_and_handle_deadlocks //通过infos和outgoing数组我们可以找出是否存在死锁,找到形成死锁检测的环,并进行事务回滚。
lock_sys_t--->lock_sys::Latches latches 代替 lock_sys_t--->LockMutex mutex;
针对不同的加锁场景,新增了不同的锁类型(全局读写锁和分片mutex锁)。
1.表锁加锁过程优化,lock_sys mutex -> 先全局共享锁,后对table所在分片加mutex。
2.行锁加锁过程优化,lock_sys mutex -> 先全局共享锁,后对page所在分片加mutex。
3.行锁/表锁解锁过程优化,lock_sys mutex -> 先全局共享锁,后对table/page所在分片加mutex。
解锁时,由于需要将事务的所有锁进行释放,简单的做法是先加全局共享锁,然后对事务的每个lock的shards加mutex进行lock释放。但存在一个约束:
为了遵守加锁规则,lock_sys相关的锁需在trx->mutex之前。因此在释放事务的每一个lock时需要先解锁trx->mutex,加锁shard->mutex,再加锁trx->mutex,释放锁资源后解锁shard->mutex。
4.lock_sys mutex -> Global_exclusive_latch_guard(全局排他锁)使用场景。
5.lock_sys mutex -> Global_exclusive_try_latch(全局排他锁)使用场景。
6.lock_sys mutex -> Shard_latches_guard(先加全局共享,再对两个page所在shards加mutex)。
为了避免死锁,对不同page shards加锁时需遵守加锁规则:shards->mutex地址大小顺序加锁。
Lock Manager是Innodb中较为复杂的一个模块,本文深入浅出,希望对大家有所帮助。
尤其是在MySQL 5.7及以下版本中遇到lock_sys锁存在热点瓶颈时,可以考虑升级到MySQL 8.0.21及以上版本。
腾讯数据库技术团队对内支持QQ空间、微信红包、腾讯广告、腾讯音乐、腾讯新闻等公司自研业务,对外在腾讯云上依托于CBS+CFS的底座,支持TencentDB相关产品,如CynosDB、CDB、CTSDB、MongoDB、CES等。腾讯数据库技术团队专注于持续优化数据库内核和架构能力,提升数据库性能和稳定性,为腾讯自研业务和腾讯云客户提供“省心、放心”的数据库服务。此公众号旨在和广大数据库技术爱好者一起推广和分享数据库领域专业知识,希望对大家有所帮助。
扫码关注腾讯云开发者
领取腾讯云代金券
Copyright © 2013 - 2025 Tencent Cloud. All Rights Reserved. 腾讯云 版权所有
深圳市腾讯计算机系统有限公司 ICP备案/许可证号:粤B2-20090059 深公网安备号 44030502008569
腾讯云计算(北京)有限责任公司 京ICP证150476号 | 京ICP备11018762号 | 京公网安备号11010802020287
Copyright © 2013 - 2025 Tencent Cloud.
All Rights Reserved. 腾讯云 版权所有