MySQL 日志篇:Redo 相关线程

在 MySQL 中,用户线程开启事务更改数据时,系统内部会生成相应的 Redo Record。为了保证事务的持久性,这些 Redo Record 需要以 Redo Log 的形式在事务提交之前写入磁盘 (也称为“落盘”)。

为了提高事务的吞吐率 (单位时间内系统处理的事务数量),在不违背 Redo Log 落盘必须先于事务提交的前提下,需要尽可能快地使 Redo Log 落盘。

MySQL 8.0 使用多个线程协同工作来完成 Redo Log 的落盘。本文将介绍这些 Redo 相关线程。如果还不了解 LSN、Log Buffer、Redo 文件格式等概念,推荐阅读 MySQL 日志篇:Redo Log Buffer 和 MySQL 日志篇:Redo 文件和自适应检查点。

Redo 相关线程总览

MySQL 8.0 中与 Redo Log 相关的线程如下图所示 (图片来自 MySQL 官方文档)。

其中:

  • log writerlog flusherlog write notifierlog flush notifier 是 Redo 相关线程 (这里没有画出 log checkpointerlog files governor 线程);
  • buf_ready_for_write_lsnwrite_lsnflushed_to_disk_lsn 是不同的 LSN,分别由特定的线程读取和更新;
  • Log Buffer 是在内存中存放 Redo Log 的容器,Recent Written Buffer 用来维护 Redo Log 写入 Log Buffer 的进度,Redo Log 最终被写入磁盘中的 Redo 文件;
  • write eventflush event 用来使线程等待或唤醒线程。

一个用户事务在更改数据时,MySQL 内部会创建若干个 mini-transaction (简称为 mtr) 在数据页上原子地执行更改动作。随之生成的 Redo Record 会存放在 mtr 的内部缓冲区中,mtr 提交时会将其内部缓冲区中的 Redo Record 一次性写入 Log Buffer,并向前推进 buf_ready_for_write_lsn

write_lsn 落后于 buf_ready_for_write_lsn 时,log writer 线程首先把 Log Buffer 中连续的 Redo Log 写入操作系统的 Page Cache,然后向前推进 write_lsn,最后唤醒 log flusherlog write notifier 线程。

flushed_to_disk_lsn 落后于 write_lsn 时,log flusher 线程通过执行 fsync() 函数强制将 Page Cache 中的数据页刷入磁盘,此时 Redo Log 才真正落盘。随后,log flusher 线程向前推进 flushed_to_disk_lsn 并唤醒 log flush notifier 线程。

最后,log write notifier/log flush notifier 线程会唤醒用户线程。用户线程被唤醒后比较自身事务的 commit_lsnwrite_lsn/flushed_to_disk_lsn,如果 commit_lsn 小于或等于 write_lsn/flushed_to_disk_lsn,那么事务对应的 Redo Log 已经写入 Page Cache/磁盘,此时可以提交事务。如果 commit_lsn 仍大于 write_lsn/flushed_to_disk_lsn,那么用户线程继续等待下一次唤醒。MySQL 使用 innodb_flush_log_at_trx_commit 参数控制是由 log write notifier 线程还是由 log flush notifier 线程唤醒用户线程,即事务提交的时机。

log writer 线程

下图从代码的角度展示了 log writer 线程的执行细节。

log writer 线程的执行流程如下:

  1. 调用 log_advance_ready_for_write_lsn() 函数向前推进 buf_ready_for_write_lsn,从而确定 Log Buffer 中已写完的连续区域 1 \textcolor{red}{^1} 1
  2. 调用 log_writer_wait_on_checkpoint() 函数检查 Redo 文件中是否有足够的空闲空间,若空闲空间不足则等待;
  3. 调用 compute_how_much_to_write() 函数计算本次要写的 Redo Log 的长度 2 \textcolor{red}{^2} 2,同时决定是否需要使用 Write-Ahead Buffer;
  4. 调用 prepare_full_blocks() 函数填入 Log Buffer 中每个 Redo Block 的头部和尾部 3 \textcolor{red}{^3} 3
  5. 若要使用 Write-Ahead Buffer,则调用 copy_to_write_ahead_buffer() 函数将 Redo Log 从 Log Buffer 复制到 Write-Ahead Buffer;
  6. 调用 write_blocks() 函数把 Log Buffer 或 Write-Ahead Buffer 中的 Redo Log 写入 Page Cache;
  7. 向前推进 write_lsn
  8. 调用 notify_about_advanced_write_lsn() 函数唤醒 log flusher 线程和 log writer notifier 线程;
  9. 调用 log_update_buf_limit() 函数向前推进 log.buf_limit_sn 4 \textcolor{red}{^4} 4
  10. 调用 update_current_write_ahead() 函数更新 log.write_ahead_end_offset

1 \textcolor{red}{1} 1:若遇到了 Recent Written Buffer 中的“空洞”或者已经向前推进了 4 KB 的距离,则停止向前推进 buf_ready_for_write_lsn

2 \textcolor{red}{2} 2:这里限制一次只能写一个 Redo 文件。若 Redo Log 的长度超出当前 Redo 文件的空闲空间大小,则多出的 Redo Log 被推迟到下次。

3 \textcolor{red}{3} 3:除了每个 Redo Block 头部的 first_rec_group 字段,该字段由用户线程填入。

4 \textcolor{red}{4} 4:用户线程把 mtr 中的 Redo Record 复制到 Log Buffer 时,实际要通过 log.buf_limit_sn 确定 Log Buffer 是否有足够的空闲空间。

这里不会再继续深入代码细节,有兴趣的小伙伴可以自行阅读 MySQL 源码。下面将聚焦于 log writer 线程使用的 Write-Ahead Buffer。

Write-Ahead Buffer 有点像 Log Buffer,但它们的职责不同:

  • Log Buffer 负责存放 Redo Log,用户线程作为“生产者”向 Log Buffer 写入 Redo Log,log writer 线程作为“消费者”从 Log Buffer 取出 Redo Log;
  • Write-Ahead Buffer 负责辅助 log writer 线程将 Redo Log 写入 Page Cache,用于写不完整的 Redo Block 以及解决 Read-on-Write 问题。

写不完整的 Redo Block

log writer 线程写 Redo Log 时,在 write_lsnbuf_ready_for_write_lsn 区间内的最后一个 Redo Block 可能还未被写满,如下图所示。

此时对于最后一个 Redo Block:

  1. 将其在 write_lsnbuf_ready_for_write_lsn 区间内的部分复制到 Write-Ahead Buffer 中;
  2. 用 0 填充该 Redo Block 剩余的部分;
  3. 填充该 Redo Block 的头部和尾部。

这种情况下,借助 Write-Ahead Buffer 可以写完 write_lsnbuf_ready_for_write_lsn 区间内的 Redo Log (即使最后一个 Redo Block 还未写满),从而尽快唤醒正在等待的用户线程。

解决 Read-On-Write 问题

Write-Ahead Buffer 的另一个作用是解决 Read-on-Write 问题。Read-on-Write 问题发生在写文件时:若要写的数据量小于 4 KB,并且对应的数据页还不在 Page Cache 中,则操作系统首先将该数据页从磁盘读入 Page Cache,然后用新数据覆盖对应区域,最后把该数据页从 Page Cache 写回磁盘。如下图所示,这会导致多出一次读操作。

只要出现以下情况之一,就不会出现 Read-on-Write 问题:

  • 情况一:要写的数据页已经在 Page Cache 中;
  • 情况二:要写的数据刚好是一个完整的数据页。

因此,解决 Read-on-Write 问题实际就是依照上述情况去写 Redo Log,这需要借助 Write-Ahead Buffer 和 log.write_ahead_end_offset 变量。

log writer 线程使用 log.write_ahead_end_offset 变量确认当前正在写的数据页。每次开始写一个新的 Redo 文件时,将 log.write_ahead_end_offset 置为 0;每次开始写一个新的数据页时,将 log.write_ahead_end_offset 加 4 KB。log writer 线程写 Redo Log 时,先调用 log.m_current_file.offset() 函数基于起始 LSN 5 \textcolor{red}{^5} 5 算出其在 Redo 文件中的偏移量,再根据偏移量、Redo Log 长度和 log.write_ahead_end_offset 决定如何写 Redo Log:

  • 若偏移量小于 log.write_ahead_end_offset,则当前数据页还未写完 (情况一,当前数据页已在 Page Cache 中):
    • 若偏移量加上 Redo Log 长度 ≤ \le log.write_ahead_end_offset
      • 且 Redo Log 长度 ≥ \ge 512,则本次只写完整的 Redo Block,最后一个未写满的 Redo Block 放到下次写;
      • 且 Redo Log 长度 < < < 512,则将 Redo Log 复制到 Write-Ahead Buffer 中,补齐到 512 字节后再写。
    • 若偏移量加上 Redo Log 长度已超过 log.write_ahead_end_offset,则本次只写满当前数据页,剩下的 Redo Log 放到下次写。
  • 若偏移量等于 log.write_ahead_end_offset,则当前数据页已经写完 (情况二,下一数据页不在 Page Cache 中):
    • 若偏移量加上 Redo Log 长度 ≥ \ge 4 KB,则本次只写 4 KB 大小的 Redo Log,剩下的 Redo Log 放到下次写;
    • 若偏移量加上 Redo Log 长度 < < < 4 KB,则将 Redo Log 复制到 Write-Ahead Buffer 中,补齐到 4 KB 后再写。

5 \textcolor{red}{5} 5:起始 LSN 等于 write_lsn - write_lsn % 512,即按 Redo Block 大小对齐。

上述使用 Write-Ahead Buffer 的两种情况如下图所示。

注意,写第一个 Redo Block 时,由于对应的数据页还不在 Page Cache 中,因此实际写了 4 KB 的数据。写第二个 Redo Block 时,由于对应的数据页已经在 Page Cache 中,因此实际写了 512 B 的数量。

log write notifier 线程

log write notifier 线程负责唤醒等待 Redo Log 写到 Page Cache 的用户线程。用户线程在提交事务前需要等待 write_lsn 追上 commit_lsn (commit_lsn 标识该事务对应的 Redo Log 的终止 LSN)。MySQL 基于 commit_lsn 把多个用户线程放到不同的 slot 中 6 \textcolor{red}{^6} 6,每个 slot 都有自己的 write event,位于相同 slot 中的用户线程都在该 slot 的 write event 上等待被唤醒。log write notifier 线程被 log writer 线程唤醒后,先根据老的 write_lsn 和新的 write_lsn 确定本次向前移动所经过的 slot,再唤醒这些 slot 中所有的用户线程。

6 \textcolor{red}{6} 6:将 commit_lsn 除以 512 后再对 slot 个数取模 (即, s l o t = c o m m i t _ l s n − 1 512 % s l o t _ c o u n t \mathrm{slot} = \frac{commit\_lsn-1}{512}\ \%\ \mathrm{slot\_count} slot=512commit_lsn1 % slot_count),就得到用户线程被放入的 slot 位置 (参见 log_compute_wait_event_slot() 函数)。

注意,用户线程可能被误唤醒。以下图中的红色实心圆标识的用户线程为例,其 commit_lsn 仍大于新的 write_lsn。因此,用户线程被唤醒后还要再次检查新的 write_lsn 是否追上自身的 commit_lsn,若没有则继续等待。

log flusher 线程比较简单,只是当 write_lsn 超过 flushed_to_disk_lsn 时在当前打开的 Redo 文件上执行 fsync() 函数。log flush notifier 线程和 log write notifier 线程类似,只是把 write_lsn 换成了 flushed_to_disk_lsn。这里不再赘述。


欢迎关注微信公众号:fightingZh

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

如若内容造成侵权/违法违规/事实不符,请联系一条长河网进行投诉反馈,一经查实,立即删除!

相关文章

JavaSE - 面向对象编程01

01 什么是面向对象编程(oop) 答&#xff1a;就是只关心对象之间的交互&#xff0c;而并不关心任务是怎样具体完成的。例如把一个大象放进冰箱需要几步&#xff1f;如果是面向对象编程只会思考冰箱和大象之间的交互&#xff0c;那么给出的答案就是&#xff1a;把冰箱门打开&…

不可错过的AIGC浪潮:提升效率与竞争力的必备神器

随着人工智能生成内容&#xff08;AIGC&#xff09;技术的迅猛发展&#xff0c;它在提升工作效率和改善生活质量方面展示了巨大的潜力。对职场人来说&#xff0c;了解AIGC如何改变各个行业&#xff0c;并探讨其未来发展中的风险和机遇&#xff0c;将有助于他们更好地利用这项技…

三相可控整流电路 (三相半波,三相桥式)

目录 1. 三相半波整流电路 2. 三相桥式全控整流电路 三相可控整流电路利用三相交流电源&#xff0c;通过可控硅&#xff08;晶闸管&#xff09;将交流电整流为直流电。主要有两种常见类型&#xff1a;三相半波整流电路和三相桥式全控整流电路。 1. 三相半波整流电路 三相半波…

Java数据存储结构——二叉查找树

文章目录 22.1.2二叉查找树22.1.2.1 概述22.1.2.1二叉查找树添加节点22.1.2.2二叉查找树查找节点22.1.2.3 二叉树遍历22.1.2.4 二叉查找树的弊端 22.1.2二叉查找树 22.1.2.1 概述 二叉查找树,又称二叉排序树或者二叉搜索树 二叉查找树的特点&#xff1a; 每一个节点上最多有…

你的绩效是不是常年都是B

原创不易&#xff0c;求赞&#xff0c;求关注&#xff0c;&#x1f64f;&#x1f64f;&#x1f64f;&#x1f64f;&#x1f64f;&#x1f64f;&#x1f64f;&#x1f64f; 目录 原创不易&#xff0c;求赞&#xff0c;求关注&#xff0c;&#x1f64f;&#x1f64f;&#x1f64…

PCL 点云生成DSM图 (高程模型图)

🙋 结果预览 🔗接上篇 Python 点云生成高程模型图(DSM) 一、代码实现 #include <pcl/io/pcd_io.h> //PCD读写类相关的头文件 #include

使用java程序对字符串进行加密

程序功能 程序的功能是对用户输入的字符串&#xff0c;使用常见的三种加密算法&#xff08;MD5、SHA-1 和 SHA-256&#xff09;进行加密&#xff0c;并输出每种算法加密后的结果。 主要步骤包括&#xff1a; 用户通过控制台输入一个字符串。 程序使用 MessageDigest 类&#x…

DFS:深搜+回溯+剪枝实战解决OJ问题

✨✨✨学习的道路很枯燥&#xff0c;希望我们能并肩走下来! 文章目录 目录 文章目录 前言 一 排列、子集问题 1.1 全排列I 1.2 子集I 1.3 找出所有子集的异或总和 1.4 全排列II 1.5 字母大小写全排列 1.6 优美的排列 二 组合问题 2.1 电话号码的数字组合 …

三菱模拟量入门接线与编程详解

当我们学会完基础后。下面就需要学习模拟量,希望小编的文章对读者有所帮助! 什么是模拟量? 模拟量是指一些连续变化的物理量(简单来说就是连续变化的量),在PLC中通常电压信号为0~10V,电流信号为4~20mA。 为什么要使用模拟量? 当我们需要检测如电压、电流、压力、速度、…

【计网】从零开始使用TCP进行socket编程 --- 客户端与服务端的通信实现

阵雨后放晴的天空中&#xff0c; 出现的彩虹很快便会消失。 而人心中的彩虹却永不会消失。 --- 太宰治 《斜阳》--- 从零开始使用TCP进行socket编程 1 TCP与UDP2 TCP服务器类2.1 TCP基础知识2.2 整体框架设计2.3 初始化接口2.4 循环接收接口与服务接口 3 服务端与客户端测试…

JS落叶动画代码分析

秋天到了&#xff0c;秋高气爽的季节。我们来做一个落叶动画吧&#xff01;来迎接秋天的到来 文字可以更换。 1.目录如下 在线演示&#xff1a;点击我在线演示 images两张照片&#xff0c;首先&#xff0c;你得要准备一个vscode编辑器。和一个chorme浏览器或edge浏览器。 …

PyTorch 激活函数及非线性变换详解

激活函数是深度学习模型的重要组成部分&#xff0c;它们引入非线性&#xff0c;从而使模型能够更好地拟合复杂的数据模式。本文将详细介绍激活函数的作用、常见类型、经典应用示例&#xff0c;并比较它们的优缺点。 激活函数的作用 激活函数的主要作用是引入非线性变换&#…

理解高并发

文章目录 1、如何理解高并发2、高并发的关键指标3、高并发系统设计的目标是什么&#xff1f;1_宏观目标2_微观目标1.性能指标2.可用性指标3.可扩展性指标 4、高并发的实践方案有哪些&#xff1f;1_通用的设计方法1.纵向扩展&#xff08;scale-up&#xff09;2.横向扩展&#xf…

ROS组合导航笔记2:使用外部定位系统

在上一单元中&#xff0c;我们了解了如何合并不同传感器的数据以生成机器人的姿势估计。因此&#xff0c;基本上&#xff0c;我们介绍了图表的以下部分&#xff0c;其中向 robot_localization 节点提供了不同的传感器&#xff0c;以便通过卡尔曼滤波器进行合并。 但是...图表的…

背包问题 总结详解

就是感觉之前 dp 的 blog 太乱了整理一下。 0-1 背包 例题:P1048 朴素算法 思路 对于一个物品&#xff0c;我们可以选&#xff0c;也可以不选。 我们用表示第 i 件物品的重量&#xff0c;表示第 i 件物品的价值。 考虑表示前 i 件物品放入容量为j的背包中的最大价值。 如…

【图像匹配】基于Harris算法的图像匹配,matlab实现

博主简介&#xff1a;matlab图像代码项目合作&#xff08;扣扣&#xff1a;3249726188&#xff09; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 本次案例是基于基于Harris算法的图像匹配&#xff0c;用matlab实现。 一、案例背景和算法介绍 …

Observability:日志管理的最佳实践 - 利用日志更快地解决问题

作者&#xff1a;来自 Elastic Luca Wintergerst•David Hope•Bahubali Shetti 在当今快速发展的软件开发环境中&#xff0c;高效的日志管理对于维护系统可靠性和性能至关重要。随着基础架构和应用程序组件的不断扩展和复杂化&#xff0c;运营和开发团队的职责也不断增加且越来…

yolov8区域入侵检测警报系统-pyside6可视化界面

yolov8区域入侵检测警报系统&#xff0c;是微智启软件工作室基于yolov8目标追踪和pyside6开发&#xff0c;在window的pycharm或者vscode里运行&#xff0c;可以应用于多个领域&#xff0c;检测统计物体个数以及入侵语音警报。 功能介绍 可以应用于 江河流域危险区域禁止游泳警…

利用AI技术提升ISP处理:图像质量的四大关键模块

随着智能手机和数码相机的飞速发展&#xff0c;图像质量成为了影响用户体验的关键因素之一。图像信号处理&#xff08;ISP&#xff0c;Image Signal Processing&#xff09;管道是将图像传感器捕捉到的原始数据转化为高质量输出的核心技术。然而&#xff0c;传统的ISP处理方法在…

螺丝、螺母、垫片等紧固件常用类型详细介绍

螺钉、螺母、垫片等紧固件介绍 螺钉 杯头内六角 首先介绍一下杯头内六角&#xff0c;杯头内六角是我们用的最常见的一种螺钉&#xff0c;如果你对选择螺钉没有什么想法&#xff0c;可以直接无脑选杯头内六角去使用。 比如说我们有一个零件加工了通孔&#xff0c;另一个零件加…