MySQL 核心模块揭秘 | 19 期 | 锁模块里有什么?什么样?

InnoDB 中管理表锁和行锁的锁模块,也就是传说中的锁子系统,在内存里是什么样的?

作者:操盛春,爱可生技术专家,公众号『一树一溪』作者,专注于研究 MySQL 和 OceanBase 源码。

爱可生开源社区出品,原创内容未经授权不得随意使用,转载请联系小编并注明来源。

本文基于 MySQL 8.0.32 源码,存储引擎为 InnoDB。

1. 引言

前面三篇文章,我们分别介绍了 InnoDB 表锁、行锁,以及它们的锁结构。

表锁结构和行锁结构是锁模块的基础组成部分,它们就像一块砖,哪里需要哪里搬。

然而,要盖房子,光有砖不行,还得有钢筋、水泥等材料,这些材料就由锁模块结构提供。

锁模块结构只有一个对象(lock_sys),在 InnoDB 中是全局唯一的。

2. 锁模块结构

锁模块结构类型为 lock_sys_t,去掉注释以及两个无关紧要的属性之后,简化如下:

struct lock_sys_t {locksys::Latches latches;hash_table_t *rec_hash;hash_table_t *prdt_hash;hash_table_t *prdt_page_hash;Lock_mutex wait_mutex;srv_slot_t *waiting_threads;srv_slot_t *last_slot;bool rollback_complete;std::chrono::steady_clock::duration n_lock_max_wait_time;os_event_t timeout_event;
}

单从属性数量上看,锁模块结构并不复杂,甚至可以说比较简单。

其实,锁模块的复杂性,不在于表锁结构、行锁结构,也不在于锁模块结构,而是在于各个事务、各种加锁场景相互交错导致的错综复杂的加锁结果。

例如,一个事务等待获得另一个事务持有的锁,虽然会出现或长或短的等待链,但也不算太坏的情况。更坏的情况是出现了环形的等待链,也就是出现了死锁。

如果出现死锁,我们又需要被动复现死锁,以解释形成死锁的原因,那简直头大了。

为了不滑入复杂的深渊,我们就此打住,先来介绍锁模块结构的属性。

锁模块结构中有三个类型为 hash_table_t 的属性,分别是 rec_hashprdt_hashprdt_page_hash

其中,prdt_hash、prdt_page_hash 由谓词锁使用。我们并不打算介绍谓词锁,忽略这两个属性,也就顺理成章了。

n_lock_max_wait_time 属性的值是 MySQL 本次启动以来,行锁的最长等待时间。通过以下命令可以查询到这个属性的值:

show status like 'innodb_row_lock_time_max';+--------------------------+-------+
| Variable_name            | Value |
+--------------------------+-------+
| Innodb_row_lock_time_max | 50157 |
+--------------------------+-------+

rollback_complete 属性,用于 MySQL 启动过程中,标识从 undo 日志中恢复出来的、需要回滚的事务是否已全部回滚完成。

如果 rollback_complete = false,说明从 undo 日志中恢复出来的、需要回滚的事务还没有全部回滚完成,InnoDB 会遍历读写事务链表(trx_sys->rw_trx_list),释放这些事务加的表锁和行锁。

这些事务全部回滚完成之后,rollback_complete 会被修改为 true。

前面介绍了锁模块结构中两个比较简单的属性,剩下的其它属性,我们分为几个小节一一介绍。

2.1 谁来管理行锁结构?

上一篇文章,我们介绍过,事务对多条记录加行锁,满足条件时,可以共用一个行锁结构。

虽然共用能减少行锁结构的数量,但是,同一时刻,InnoDB 中可能还是有很多行锁结构。

这么多行锁结构,要怎么组织,用到时才能方便、快速的找到呢?

这就需要用到锁模块结构的 rec_hash 属性了。

rec_hash 属性是个哈希表,它的类型为 hash_table_t,创建锁模块对象(lock_sys)之后分配内存:

void lock_sys_create(ulint n_cells)
{...// 创建锁模块对象,分配内存lock_sys = static_cast<lock_sys_t *>(ut::zalloc_withkey(...));...// 创建哈希表(rec_hash),分配内存lock_sys->rec_hash =ut::new_<hash_table_t>(n_cells);...
}

lock_sys_create() 由 srv_start() 调用:

dberr_t srv_start(bool create_new_db) {...lock_sys_create(srv_lock_table_size);...
}

变量 srv_lock_table_size 在 innodb_init_params() 中赋值,它的值会传递给 lock_sys_create() 的参数 n_cells。

static int innodb_init_params() {...srv_lock_table_size = 5 * (srv_buf_pool_size / UNIV_PAGE_SIZE);...
}

srv_buf_pool_size 是 buffer pool 的大小,UNIV_PAGE_SIZE 是一个数据页的大小,它们的单位都是字节。

以 buffer pool 大小为 128M、数据页大小为 16K 为例,变量 srv_lock_table_size 的值计算如下:

// 128M = 134217728 字节
// 16K  = 16384 字节
srv_lock_table_size = 5 * (134217728 / 16384) = 40960

变量 srv_lock_table_size 的值(40960)最终会传递给 lock_sys_create() 的参数 n_cells。用 40960 替换 n_cells 之后如下:

void lock_sys_create(ulint n_cells)
{...lock_sys->rec_hash = ut::new_<hash_table_t>(40960);...
}

以上代码说明 buffer pool 大小为 128M,数据页大小为 16K 时,锁模块结构的 rec_hash 属性有 40960 个格子。

每个格子都有编号,从 0 开始,一直到 40959。

这些格子并不是用来存储行锁结构,而是用来管理行锁结构,它们的作用相当于线头,找到了线头就能牵出一根线。

创建行锁结构之后,会先根据行锁结构中那些记录所属数据页的页号和表空间 ID,计算得到哈希值,再根据哈希值计算得到格子的编号。

多个行锁结构可能计算得到相同的哈希值,从而得到相同的编号,对应到同一个格子,这些行锁结构通过各自的 hash 属性形成一个行锁结构链表。如果我们把这个链表看成一根线,这个格子就是这根线的线头。

计算出格子编号之后,行锁结构会插入到格子对应的行锁结构链表的最前面。

想要找到某个行锁结构,也需要根据同样的规则,计算得到格子编号,再根据编号找到格子,最后遍历这个格子对应的行锁结构链表,以找到目标行锁结构。

2.2 谁来保护表锁和行锁结构?

前面我们介绍了 rec_hash 是个哈希表,分为很多格子,每个格子管理一个行锁结构链表。同一个链表的所有行锁结构,计算得到的哈希值相同。

事务加行锁时,会优先考虑共用已有的行锁结构,这就要先找到一个可以共用的行锁结构。

首先,需要找到 rec_hash 的某个格子。

然后,遍历这个格子对应的行锁结构链表,并根据共用条件,判断某个行锁结构是否可以共用。

事务加行锁时,如果生成了新的行锁结构,需要找到 rec_hash 的某个格子,把行锁结构插入到这个格子对应的行锁结构链表的最前面。

事务提交或回滚时,释放所有行锁,需要找到每个锁结构在哪个格子对应的行锁结构链表中,并从链表中删除这个行锁结构。

事务加表锁时,会遍历这个表对象的 locks 链表,以判断可以立即获得表锁,还是需要进入等待状态。

事务提交或回滚时,释放所有表锁,需要从每个表对象的 locks 链表中删除这个表锁结构。

多个事务执行上面这些操作,可能会同时读写 rec_hash 中某个格子对应的行锁结构链表,也可能同时读写某个表对象的 locks 链表。

为了避免并发操作同时读写同一个行锁结构链表、或者同时读写同一个表对象的 locks 链表出现冲突,需要有个什么东西,来限制同一时刻只有一个事务读写某个行锁结构链表、或者某个表对象的 locks 链表。

于是,就有了锁模块结构的 latches 属性,它的类型为 locksys::Latches。

class Latches {private:...Unique_sharded_rw_lock global_latch;Page_shards page_shards;Table_shards table_shards;...
}

latches 也是一个对象,有三个属性,分别为 global_latchpage_shardstable_shards

事务提交或回滚时,释放所有行锁和表锁会用到 global_latch。

事务加行锁时,会用到 page_shards。

事务加表锁时,会用到 table_shards。

page_shards、table_shards 的类型分为 Page_shardsTable_shards,定义如下:

static constexpr size_t SHARDS_COUNT = 512;class Page_shards {...Padded_mutex mutexes[SHARDS_COUNT];...
}class Table_shards {...Padded_mutex mutexes[SHARDS_COUNT];...
}

Page_shards 的 mutexes 属性是个数组,有 512 个元素。

有新的行锁结构需要加入某个行锁结构链表,或者需要遍历某个行锁结构链表以找到目标行锁结构时,会根据行锁结构中那些记录所属数据页的页号和表空间 ID,计算得到哈希值,再根据哈希值计算得到数组下标,到 mutexes 数组中拿到下标对应的互斥量,就可以保护需要读写的行锁结构链表了。

Table_shards 的 mutexes 属性也是个数组,同样有 512 个元素。

某个表对象的 locks 链表需要保护时,会直接用表 ID 对 512 取模(table_id % 512),得到的结果作为数组下标,到 mutexes 数组中拿到下标对应的互斥量,就可以保护这个表对象的 locks 链表了。

2.3 锁等待了怎么办?

锁模块结构中,有三个属性和锁等待相关,分别是 wait_mutexwaiting_threadslast_slot,它们的初始化代码如下:

void lock_sys_create(ulint n_cells)
{ulint lock_sys_sz;// 锁模块结构占用的内存大小// 加上 waiting_threads 指向的内存区域的大小// 因为这两部分要一起分配内存lock_sys_sz = sizeof(*lock_sys) + srv_max_n_threads * sizeof(srv_slot_t);...void *ptr = &lock_sys[1];lock_sys->waiting_threads = static_cast<srv_slot_t *>(ptr);// 初始化时// last_slot 和 waiting_threads 指向同一个位置lock_sys->last_slot = lock_sys->waiting_threads;mutex_create(LATCH_ID_LOCK_SYS_WAIT, &lock_sys->wait_mutex);...
}

waiting_threads 属性是个指针,它指向一片内存区域,这片内存区域分为 srv_max_n_threads 个 slot,每个 slot 存放一个 srv_slot_t 对象。

srv_max_n_threads 在 innodb_init_params() 中赋值,硬编码为 102400。

也就是说,waiting_threads 属性指向的内存区域,最多可以存放 102400 个 srv_slot_t 对象。

如果某个事务不能立即获得锁(表锁或行锁),就会在这片内存区域中找到一个空闲的 slot,构造一个包含该事务以及锁信息的 srv_slot_t 对象放入这个 slot,并标记这个 slot 为已使用状态。

last_slot 属性也是个指针,初始化时,和 waiting_threads 属性指向相同的内存地址。

随着不断有事务进入锁等待状态、以及处于锁等待状态的事务获得锁,last_slot 会不断变化。

不过,不管怎么变化,last_slot 始终遵循一个原则,就是它指向的那个 slot,以及之后的所有 slot 都处于空闲状态。

为什么需要 last_slot?

因为后台线程检查锁等待是否超时,会从后往前遍历 waiting_threads 属性指向的内存区域。

如果没有 last_slot,每次遍历都需要从最后一个 slot 开始,到第一个 slot 为止,检查每个 slot 对应的锁等待是否超时。

然而,通常情况下,waiting_threads 属性指向的内存区域中的 102400 个 slot,其中大部分都是空闲的。

空闲 slot 没有被正在等待锁的事务占用,实际上不需要检查锁等待是否超时。

如果没有 last_slot,每次检查锁等待是否超时,都要遍历所有 slot,显然很浪费时间。

为了提升检查锁等待超时的效率,只需要遍历已使用状态的 slot 就可以了,这就需要有个东西来标识哪个范围内的 slot 是已使用状态,于是,就有了 last_slot。

有一点需要说明,如果某个事务曾经进入过锁等待状态,占用了某个 slot。某一轮检查锁等待超时之前,这个事务获得了锁,又会把它占用的那个 slot 重置为空闲状态。

所以,last_slot 之前的那些 slot,并不全部是已使用状态,也有一些是空闲的,但是这个数量应该不会很多,遍历这些少量的空闲 slot,也不会浪费太多时间。

介绍完 waiting_threads、last_slot,终于轮到 wait_mutex 属性了。

从属性名上看,wait_mutex 属性显然是个互斥量。

多个事务同时读写 last_slot 属性,可能造成冲突,这就需要有个东西来保证同一时刻只有一个线程读写 last_slot 属性,于是就有了 wait_mutex

2.4 那就发个锁等待通知

事务想要加锁(表锁或行锁),如果发生了锁等待,新出现的锁等待,和原来那些锁等待搅和在一起,有可能会出现死锁。

为了及时发现死锁,事务进入锁等待状态之前,会触一个事件,通知后台线程出现了锁等待。

这个事件就保存在锁模块结构的 timeout_event 属性中。

监听 timeout_event 事件的后台线程收到通知之后,就会开始检查是否发生了死锁。如果检查发现了死锁,就及时解决。

3. 总结

锁模块结构的 rec_hash 属性是个哈希表,分为很多小格子,每个格子管理一个行锁结构链表。

latches 属性用于保证同一时刻只有一个线程读写 rec_hash 属性的同一个格子对应的行锁结构链表,以及同一时刻只有一个线程读写同一个表对象的 locks 链表。

waiting_threads 属性指向一片分为 102400 个 slot 的内存区域,每个等待获得锁的事务会占用其中一个 slot。

last_slot 属性用于减少检查锁等待超时需要遍历的 slot 数量,提升效率。

wait_mutex 属性用于保证同一时刻只有一个线程读写 last_sot 属性。

timeout_event 属性用于发生锁等待时,通知后台线程及时检查是否出现了死锁。

更多技术文章,请访问:https://opensource.actionsky.com/

关于 SQLE

SQLE 是一款全方位的 SQL 质量管理平台,覆盖开发至生产环境的 SQL 审核和管理。支持主流的开源、商业、国产数据库,为开发和运维提供流程自动化能力,提升上线效率,提高数据质量。

✨ Github:https://github.com/actiontech/sqle

📚 文档:https://actiontech.github.io/sqle-docs/

💻 官网:https://opensource.actionsky.com/sqle/

👥 微信群:请添加小助手加入 ActionOpenSource

🔗 商业支持:https://www.actionsky.com/sqle

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/web/23011.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

LabVIEW开发EOL功能测试系统

LabVIEW开发EOL功能测试系统 介绍了一种基于LabVIEW开发的EOL功能测试系统方案&#xff0c;涵盖软件架构、工作流程、模块化设计、低耦合性、易于修改与维护、稳定性及硬件选型。系统通过高效的CAN通信实现对电机控制器的全面测试&#xff0c;确保运行可靠并支持未来的升级需求…

危机公关之负面信息优化技巧解析

当今时代&#xff0c;网络发布信息没有任何门槛&#xff0c;任何人可以通过互联网发布信息&#xff0c;这使负面信息产生的可能性大大提高&#xff0c;企业形成危机的可能性也大大提高。针对网络上的负面信息处理得当可能并不会对品牌造成伤害&#xff0c;处理不当就很可能给企…

QT之可拖动布局研究

1. 背景 最开始只用到了最基本的水平布局 、垂直布局。它的好处就是窗口整体缩放后&#xff0c;控件也自动等比例缩放。 但是比如水平布局之中的控件宽度比例、垂直布局之中的控件高度比例都是固定的。 平时也不怎么开发界面&#xff0c;最近有个需求&#xff0c;想界面上的…

Atlassian企业日技术分享:AI在ITSM中的创新实践与应用、Jira服务管理平台AI功能介绍

2024年5月17日&#xff0c;Atlassian中国合作伙伴企业日活动在上海成功举办。活动以“AI协同 创未来——如何利用人工智能提升团队协作&#xff0c;加速产品交付”为主题&#xff0c;深入探讨了AI技术在团队协作与产品交付中的创新应用与实践&#xff0c;吸引了众多业内专家、企…

深圳比创达电子EMC|EMC与EMI一站式解决方案:攻克电磁兼容难题

在当今这个科技日新月异、电子产品层出不穷的时代&#xff0c;电磁兼容&#xff08;EMC&#xff09;与电磁干扰&#xff08;EMI&#xff09;问题愈发凸显其重要性。为了确保电子设备的正常运行&#xff0c;减少电磁干扰对环境和人体的影响&#xff0c;EMC与EMI一站式解决方案成…

【回眸】Linux内核(十)system()函数与popen()函数

前言 system()函数的作用是执行一个shell脚本或者shell指令 popen与system()函数类似,不同点是popen()函数可以获取运行的shell脚本或者命令的输出结果 system() 函数参数 #include <stdlib.h> int system(const char *comand) 参考示例代码: #include <stdio.…

2023年全国消费品“增品种、提品质、创品牌”三品战略发展成果报告

来源&#xff1a;赛迪&欧特欧 近期历史回顾&#xff1a; 2023工业无线电磁环境白皮书——有色金属制造行业.pdf 2024出海企业人才发展实践指南.pdf 2024年全球电子商务市场.pdf 宝钢低碳钢铁技术策划及开发-钟勇.pdf 2023-2024年度中国智能制造产业发展报告.pdf 2024精准医…

【AI大模型】Function Calling

目录 什么是Function Calling 示例 1&#xff1a;调用本地函数 Function Calling 的注意事项 支持 Function Calling 的国产大模型 百度文心大模型 MiniMax ChatGLM3-6B 讯飞星火 3.0 通义千问 几条经验总结 什么是Function Calling Function Calling 是一种函数调用机…

【C++ | 构造函数】类的构造函数详解

&#x1f601;博客主页&#x1f601;&#xff1a;&#x1f680;https://blog.csdn.net/wkd_007&#x1f680; &#x1f911;博客内容&#x1f911;&#xff1a;&#x1f36d;嵌入式开发、Linux、C语言、C、数据结构、音视频&#x1f36d; ⏰发布时间⏰&#xff1a;2024-06-06 0…

HCIA-RS基础-VLAN配置

目录 前言创建拓扑创建VLAN查看创建的VLAN配置trunk口并放行VLAN配置access接口查看所有vlan基本信息测试网络连通性命令合集 前言 VLAN定义&#xff1a;VLAN是一种将局域网内的设备从逻辑上划分成一个个网段&#xff0c;从而实现虚拟工作组的新兴数据交换技术。VLAN优点&…

【面试笔记】嵌入式软件工程师,汽车电子软件相关

文章目录 1. C语言基础1.1 const1.2 static1.3 回调函数的用法1.4 宏定义1.5 编译、链接过程1.6 堆与栈的区别&#xff1f;1.7 简单的字符串算法题&#xff0c;C语言实现1.7.1 给定一个字符串&#xff0c;按顺序筛选出不重复的字符组成字符串&#xff0c;输出该字符串1.7.2 给定…

Python3 迭代器和生成器

前言 本文主要介绍Python中的迭代器和生成器&#xff0c;主要内容包括 迭代器概述、生成器简介。 文章目录 前言一、迭代器简介二、生成器简介 一、迭代器简介 在 Python 中&#xff0c;迭代器(iterator)是一个实现了迭代器协议&#xff08;Iterator Protocol&#xff09;的…

opencv进阶 ——(十一)基于RMBG实现生活照生成寸照

实现步骤 1、检测人脸&#xff0c;可以使用opencv自带的级联分类器或者dlib实现人脸检测 2、放大人脸范围&#xff0c;调整到正常寸照尺寸 3、基于RMGB算法得到人像掩码 4、生成尺寸相同的纯色背景与当前人像进行ALPHA融合即可 alpha融合实现 void alphaBlend(cv::Mat&…

1 机器人软件开发学习所需通用技术栈(一)

机器人软件工程师技术路线&#xff08;如有缺失&#xff0c;欢迎补充&#xff09; 1. 机器人软件开发工程师技术路线 1.1 基础知识 C/C编程&#xff1a;掌握C/C语言基础&#xff0c;包括数据结构、算法、内存管理等。操作系统&#xff1a;了解Linux或Windows等操作系统的基本…

2.1 初识Windows程序

Windows程序设计是一种面向对象的编程。Windows操作系统以数据结构的形式定义了大量预定义的对象作为操作系统的数据类型。Windows动态链接库提供了各种各样的API接口函数供Windows应用程序调用。一个Windows应用程序是运行在Windows操作系统之上的。这些API接口函数的调用所实…

【Vue】路由的基本使用

文章目录 一、固定5个固定的步骤二、代码示例三、两个核心步骤四、完整代码 vue-router插件作用 修改地址栏路径时&#xff0c;切换显示匹配的组件 说明 Vue 官方的一个路由插件&#xff0c;是一个第三方包 官网 https://v3.router.vuejs.org/zh/ VueRouter的使用&#xff0…

TCP/IP协议介绍——三次握手四次挥手

TCP/IP&#xff08;Transmission Control Protocol/Internet Protocol&#xff0c;传输控制协议/网际协议&#xff09;是指能够在多个不同网络间实现信息传输的协议簇。TCP/IP协议不仅仅指的是TCP 和IP两个协议&#xff0c;而是指一个由FTP、SMTP、TCP、UDP、IP等协议构成的协议…

CSS学习|css三种导入方式、基本选择器、层次选择器、结构伪类选择器、属性选择器、字体样式、文本样式

第一个css程序 css程序都是在style标签中书写 打开该网页&#xff0c;可以看到h1标签中的我是标题被渲染成了红色 可以在同级目录下创建一个css目录&#xff0c;专门存放css文件&#xff0c;可以和html分开编写 然后在html页面中&#xff0c;利用link标签以及css文件地址&…

大模型基架:Transformer如何做优化?

大模型的基础模式是transformer&#xff0c;所以很多芯片都实现先专门的transformer引擎来加速模型训练或者推理。本文将拆解Transformer的算子组成&#xff0c;展开具体的数据流分析&#xff0c;结合不同的芯片架构实现&#xff0c;分析如何做性能优化。 Transformer结构 tr…

go的反射和断言

在go中对于一个变量&#xff0c;主要包含两个信息变量类型&#xff08;type&#xff09;和变量值&#xff08;value&#xff09; 可以通过reflect包在运行的时候动态获取变量信息&#xff0c;并能够进行操作 对于Type可以通过reflect.TypeOf()获取到变量的类型信息 reflect.Ty…