当前位置: 首页 > news >正文

Debezium日常分享系列之:深入解析SQL Server事务日志

Debezium日常分享系列之:深入解析SQL Server事务日志

  • 准备 SQL Server
    • 配置 SQL Server 容器
    • 在虚拟机中安装 SQL Server
  • 创建测试数据库
  • SQL Server 事务日志内部机制
    • 虚拟日志文件 (VLFs)
    • VLF 偏移量与状态管理
  • 块、记录与 LSN
  • SQL Server 数据结构
  • 分区与分配单元
  • 页:基本存储单元
  • 探索表与页结构
  • 使用DBCC IND探索数据页
  • 使用 DBCC PAGE 读取页面
  • 结论

SQL Server 通过 变更表 提供变更数据捕获(CDC)功能——这是一组特殊的系统表,用于记录选定“普通”表中的数据修改。若需实时监控变更,需定期查询这些变更表。这正是 Debezium 目前的工作方式:它以配置的间隔轮询 SQL Server 的变更表,并将结果转换为连续的 CDC 记录流。这种方式效果良好,但我们能否更进一步?

捕获的表由 SQL Server Agent 填充,该代理读取事务日志、提取变更,并将其存储到变更表中。理论上,我们可以绕过中间环节,直接解析事务日志。类似 OpenLogReplicator 的工具正是以这种方式处理 Oracle 数据库的 CDC。让我们深入 SQL Server 内部,探索其工作原理及记录存储方式。

在本文中,我们将:

  • 准备一个本地 SQL Server 实例用于实验
  • 探究 SQL Server 事务日志的内部结构
  • 理解记录如何存储在磁盘上

准备 SQL Server

我们将本地部署 SQL Server,以便深入分析其日志文件。您有两种主要选择:Docker 容器或虚拟机安装。以下分别介绍这两种方法。

配置 SQL Server 容器

使用容器非常简单,只需启动即可:

docker run -it --rm --name sqlserver \-e "ACCEPT_EULA=Y" \-e "MSSQL_SA_PASSWORD=Password!" \-p 1433:1433 mcr.microsoft.com/mssql/server:2022-latest

当容器运行后,您可以直接从宿主机连接,或附加到容器内部使用内置的 mssql-tools 工具。

docker exec -it sqlserver /bin/bash
/opt/mssql-tools18/bin/sqlcmd -S localhost -U SA -P Password! -C -No

在虚拟机中安装 SQL Server

请按照适用于 RHEL 或 Fedora 的 官方指南 进行操作。添加适当的软件仓库、安装及配置服务器的步骤均简单明了:

curl -o /etc/yum.repos.d/mssql-server.repo https://packages.microsoft.com/config/rhel/9/mssql-server-2022.repo
dnf install -y mssql-server
/opt/mssql/bin/mssql-conf setup

您可能还需要安装 SQL Server 客户端工具:

curl -o /etc/yum.repos.d/mssql-release.repo https://packages.microsoft.com/config/rhel/9/prod.repo
dnf install -y mssql-tools18 unixODBC-devel

并本地连接:

/opt/mssql-tools18/bin/sqlcmd -S localhost -U SA -P Password! -C -No

如果要从虚拟机外部连接,请打开端口 1433:

firewall-cmd --zone=public --add-port=1433/tcp --permanent
firewall-cmd --reload

创建测试数据库

服务器启动并运行后,我们可以在其中创建测试数据库和表:

CREATE DATABASE TestDB;
GOUSE TestDB;CREATE TABLE products (id INT PRIMARY KEY,name NVARCHAR(255) NOT NULL,description NVARCHAR(512),weight FLOAT
);INSERT INTO products VALUES(1,'scooter','Small 2-wheel scooter',3.14),(2,'car battery','12V car battery',8.1),(3,'12-pack drill bits','12-pack of drill bits with sizes ranging from #40 to #3',0.8);
GO

SQL Server 事务日志内部机制

在 SQL Server 中,所有持久性变更(包括插入、更新、删除或架构变更)均会被写入事务日志。该日志确保在系统崩溃时能够恢复事务。默认情况下,数据库文件存储在 /var/opt/mssql/ 目录中。事务日志文件以 .ldf 为扩展名,存放在 /var/opt/mssql/data 路径下。该目录还包含 *.mdf 文件——即存储数据本身和架构信息的主数据文件。例如,我们的示例数据库文件为 TestDB.mdf 和 TestDB_log.ldf。

若因故无法在此路径找到相关文件,可通过运行以下查询直接从 SQL Server 获取物理文件位置:

SELECT name, physical_name
FROM sys.master_files
WHERE database_id = DB_ID('TestDB');
GO

SQL 服务器应该给出如下结果:

name                           physical_name
------------------------------ ------------------------------
TestDB                         /var/opt/mssql/data/TestDB.mdf
TestDB_log                     /var/opt/mssql/data/TestDB_log

虚拟日志文件 (VLFs)

事务日志并非一个庞大的单一空间,而是被划分为多个虚拟日志文件 (VLFs)。SQL Server 根据日志大小决定创建多少 VLFs。您可以通过调用 sys.dm_db_log_info 存储过程,获取 VLF 的序列号及其他数据库相关信息:

SELECT file_id, vlf_sequence_number, vlf_begin_offset, vlf_size_mb
FROM sys.dm_db_log_info(DB_ID('TestDB'));

以下是一个示例输出,包含 VLF 序列号(每个文件唯一)、起始偏移量(在文件中的起始位置)以及大小(以 MB 为单位):

file_id vlf_sequence_number  vlf_begin_offset     vlf_size_mb
------- -------------------- -------------------- ------------------------2                   42                 8192       1.92999999999999992                    0              2039808       1.92999999999999992                    0              4071424       1.92999999999999992                    0              6103040       2.1699999999999999

VLF 偏移量与状态管理

除了 VLF 序列号外,另一个关键点是 VLF 偏移量。实际上,第一个 VLF 并非从日志文件的起始位置开始,而是位于文件开头的 8192 字节(8KB)之后——这部分空间被保留为日志文件头。

VLF 文件头中记录了该 VLF 是否处于活动状态。VLF 以循环方式使用,未活动的 VLF 可被新创建的 VLF 复用。但若 VLF 处于活动状态,则无法复用。这种情况通常发生在以下场景:

  • 包含未提交事务的记录
  • 记录尚未归档或未被系统其他组件处理(如未复制到变更表、未同步至其他服务器等)
    VLF 从活动状态转为非活动状态的过程称为日志截断(log truncation)。

VLF 复用的具体时机和规则还取决于数据库的恢复模式:

  • SIMPLE 恢复模式:检查点(checkpoint)操作后,VLF 最终会被复用。
  • FULL 恢复模式:检查点操作不会导致事务日志截断。

您可以通过以下 SQL 命令查询数据库当前的恢复模式:

SELECT name, recovery_model,recovery_model_desc
FROM sys.databases
WHERE name = N'TestDB';
GO

磁盘上 VLF 的物理结构与上述列表中的 VLF 行相同。这也可以从 VLF 偏移量 vlf_begin_offset 中看出。但是,VLF 的逻辑结构可能有所不同。VLF 的顺序由 vlf_sequence_number 决定。0 表示 VLS 尚未使用。

使用 DBCC 工具运行以下命令可以获得与上述查询非常相似的输出:

DBCC LOGINFO('TestDB')

本文后面将会提到有关 DBCC 实用程序的更多信息。

块、记录与 LSN

每个 VLF(虚拟日志文件)被划分为块(blocks)——这是日志中最小的物理写入单元。块的大小范围从 512 字节到 60 KB,以 512 字节为增量逐步增长,直至达到最大尺寸。块是实际写入磁盘的单位,通常在事务提交(commit)或检查点(checkpoint)时执行写入。

块中存储的是实际的事务日志记录(log records)。日志记录是对数据库执行的原子性变更操作,例如插入(insert)、更新(update)、事务提交(TX commit)等。这些记录在块中的存储顺序与它们在数据库中的执行顺序一致,因此同一个块内可能混杂不同事务的记录。此外,当块被写入磁盘时,其中可能包含未提交事务的记录。

SQL Server 事务日志文件的整体结构如下图所示:

在这里插入图片描述

块及其内部记录均被分配了唯一的序列号:

  • 块序列号:长度为 4 字节
  • 记录序列号:长度为 2 字节
  • 若将这两者与 VLF 序列号结合,即可通过三元组(VLF序列号 + 块序列号 + 记录序列号)唯一标识每条记录。
<VLF sequence number>:<block sequence number>:<record sequence number>

这一唯一记录标识符被称为日志序列号(Log Sequence Number),通常简称为 LSN。Debezium 正是利用此编号来存储偏移量(offsets)——它记录了 Debezium 已处理的最新变更以及最新提交的事务。

SQL Server 数据结构

在前一章中,我们探讨了 SQL Server 事务日志的结构。如前所述,事务日志会记录针对数据库执行的所有操作。但由于日志最终可能被截断,其包含的信息可能会丢失。为确保数据持久性和性能,实际数据及相关元数据(如索引)会永久存储在磁盘上。下面我们详细分析 SQL Server 如何组织这些数据。

分区与分配单元

每个表或索引存储在一个或多个分区(partition)中,单个表或索引最多可包含 15,000 个分区。存储于单个分区中的表或索引子集称为 HOBT(Heap 或 B-tree 的缩写)。

  • 堆(Heap):指无聚集索引的表,其数据未按索引结构组织。
  • 聚集索引表:数据按索引结构组织,实际存储的是索引行。

分区包含存储实际数据行的数据页(data pages),主要分为三类:

  • 行内数据页(In-row data pages):存储固定长度数据或可单页容纳的数据。
  • 行溢出数据页(Row-overflow data pages):当变长数据类型(如 varchar、nvarchar)超出单页容量时存储于此。
  • LOB 数据页(LOB data pages):存储大型对象(如 XML 或二进制数据)。
    同一分区中类型相同的数据页会被归类到分配单元(allocation units)中。

页:基本存储单元

在 SQL Server 中,页(Page) 是最基础的存储单元,存储于前文提到的 .mdf 文件中。每个 .mdf 文件被划分为 8 KB 的块,每个块即为一页。

页的结构

  • 页头(Header)占用 96 字节,存储元数据,包括:
    • 页号(Page number)
    • 页类型(Page type)
    • 页内剩余空间(Free space)
    • 所属分配单元(Allocation unit)的编号等
  • 数据行(Data Rows)
    • 页头之后存储实际数据行。
    • 每行在页中的位置由 行偏移量(Row Offset) 决定:
      • 行偏移量为 2 字节,表示该行起始位置距离页开头的字节数。
      • 所有偏移量以逆序存储在页末尾,形成行偏移数组(Row Offset Array)。

设计优势

  • 这种设计使 SQL Server 能够高效地:
    • 查找、插入或移动页内行数据
    • 无需重写整个页

页的具体结构如下图所示:
在这里插入图片描述
变长数据和大对象(LOB)通常无法完整存储在一个页面中,且由于行数据不能跨页存储,这些大型值会被存放在行内数据分配单元之外。它们会被置于由行溢出或LOB分配单元管理的页面中。

页面本身会被分组为所谓的"区段"(extents),这是SQL Server管理磁盘空间的基本单位。每个区段由8个页面组成,总计64KB。区段分为两种类型:统一区段(所有8个页面属于同一对象)和混合区段(页面可能属于不同对象)。SQL Server通过特殊的分配映射来跟踪区段使用情况:全局分配映射(GAM)记录区段是否已分配,共享全局分配映射(SGAM)则标识仍包含空闲页面的混合区段。

关于这些机制的更深入探讨超出了本文范围,您可以通过官方页面与区段架构指南了解更多细节。

探索表与页结构

从实际应用角度,可以通过系统视图 sys.indexes、sys.tables 和 sys.columns 查询索引、表及列的信息。分区与分配单元的详细信息则存储在 sys.partitions、sys.allocation_units 以及 sys.system_internals_allocation_units 中。这些视图可通过 OBJECT_ID 函数关联查询。例如,若要统计 products 表包含的分区数量,可执行以下查询:

SELECT object_id, partition_id, partition_number, hobt_id
FROM sys.partitions
WHERE object_id = OBJECT_ID(N'products');

输出可能如下所示:

object_id   partition_id         partition_number hobt_id
----------- -------------------- ---------------- --------------------901578250    72057594045726720                1    72057594045726720

然后可以使用生成的partition_id来获取有关此表的分配的信息:

SELECT *
FROM sys.allocation_units
WHERE container_id = (SELECT partition_idFROM sys.partitionsWHERE object_id = OBJECT_ID(N'products')
);

查询给出了以下结果:

allocation_unit_id   type type_desc                                                    container_id         data_space_id total_pages          used_pages           data_pages
-------------------- ---- ------------------------------------------------------------ -------------------- ------------- -------------------- -------------------- --------------------72057594052476928    1 IN_ROW_DATA                                                     72057594045726720             1                    9                    2                    1

通过关联这些系统视图,我们可以收集关于表和索引结构的详细信息。但需要注意的是,这些视图不会直接暴露实际存储数据的页面内容。

使用DBCC IND探索数据页

SQL Server提供了DBCC工具集来检查单个数据页的结构和内容。虽然其中许多功能未被官方文档记录,但在社区资源中有详细描述。

DBCC IND ( [ database_name | database_id ], table_name, index_id )

index_id 可以从 sys.indexes 中获取,或者 -1 显示所有索引和 IAM(索引分配图),-2 仅显示 IAM。

对于我们的产品表:

DBCC IND ('TestDB', 'products', -1)

结果

PageFID PagePID     IAMFID IAMPID      ObjectID    IndexID     PartitionNumber PartitionID          iam_chain_type       PageType IndexLevel NextPageFID NextPagePID PrevPageFID PrevPagePID
------- ----------- ------ ----------- ----------- ----------- --------------- -------------------- -------------------- -------- ---------- ----------- ----------- ----------- -----------1         231   NULL        NULL   901578250           1               1    72057594045726720 In-row data                10       NULL           0           0           0           01         336      1         231   901578250           1               1    72057594045726720 In-row data                 1          0           0           0           0           0

第一条记录代表 IAM 页,而第二条记录对应于包含表行的实际数据页。这里的关键值是 PageFID 和 PagePID,我们将在下一个命令 DBCC PAGE 中使用它们。

使用 DBCC PAGE 读取页面

DBCC 实用程序的另一个未记录的函数是 DBCC PAGE,它会转储所请求页面的内容。在运行 DBCC PAGE 之前,必须启用跟踪标志 3604 才能将输出发送到客户端:

DBCC TRACEON(3604);

DBCC PAGE 命令的语法是

DBCC PAGE ( [ database_name | database_id ], file_number, page_number, print_option )

其中 file_number 和 page_number 分别是 DBCC IND 中的 PageFID 和 PagePID。print_option 控制详细级别,其值可以是 0-3。添加 WITH TABLERESULTS 可将输出格式化为表格形式。

对于我们的数据页表:

DBCC PAGE ('TestDB', 1, 336, 0)

我们得到

PAGE: (1:336)BUFFER:BUF @0x00000009000FDA40bpage = 0x000000101A8B0000          bPmmpage = 0x0000000000000000       bsort_r_nextbP = 0x0000000000000000bsort_r_prevbP = 0x0000000000000000 bhash = 0x0000000000000000          bpageno = (1:336)bpart = 4                           bstat = 0x10b                       breferences = 0berrcode = 0                        bUse1 = 22094                       bstat2 = 0x0blog = 0x1cc                        bsampleCount = 0                    bIoCount = 0resPoolId = 0                       bcputicks = 0                       bReadMicroSec = 294bDirtyPendingCount = 0              bDirtyContext = 0x000000100C4947A0  bDbPageBroker = 0x0000000000000000bdbid = 5                           bpru = 0x00000010047A0040PAGE HEADER:Page @0x000000101A8B0000m_pageId = (1:336)                  m_headerVersion = 1                 m_type = 1m_typeFlagBits = 0x0                m_level = 0                         m_flagBits = 0x8000m_objId (AllocUnitId.idObj) = 222   m_indexId (AllocUnitId.idInd) = 256 Metadata: AllocUnitId = 72057594052476928Metadata: PartitionId = 72057594045726720                                Metadata: IndexId = 1Metadata: ObjectId = 901578250      m_prevPage = (0:0)                  m_nextPage = (0:0)pminlen = 16                        m_slotCnt = 3                       m_freeCnt = 7761m_freeData = 425                    m_reservedCnt = 0                   m_lsn = (42:208:29)m_xactReserved = 0                  m_xdesId = (0:0)                    m_ghostRecCnt = 0m_tornBits = 0                      DB Frag ID = 1Allocation StatusGAM (1:2) = ALLOCATED               SGAM (1:3) = NOT ALLOCATED          PFS (1:1) = 0x40 ALLOCATED   0_PCT_FULLDIFF (1:6) = CHANGED                ML (1:7) = NOT MIN_LOGGED

页头部分包含一些有用的元数据,例如:

  • m_lsn - 此页面最后一次更改的日志序列号,在恢复过程中非常有用。
  • m_slotCnt - 页面上的行数(槽位)
  • pminlen - 固定长度数据的大小
  • m_freeData - 可用空间的起始偏移量
  • m_prevPage / m_nextPage - 指向相邻页面的指针(此处为空,因为数据量太小,无法跨越多个页面)

将 print_option 设置为 1 或 3 后,DBCC PAGE 还会在 DATA 部分显示行内容的字节表示。您可以轻松识别其中的变量字符串列。为简洁起见,此处仅显示第一行:

Slot 0, Offset 0x60, Length 81, DumpStyle BYTERecord Type = PRIMARY_RECORD        Record Attributes =  NULL_BITMAP VARIABLE_COLUMNSRecord Size = 81
Memory Dump @0x000000096A0260600000000000000000:   30001000 01000000 1f85eb51 b81e0940 04000002  0........ëQ¸. @....0000000000000014:   00270051 00730063 006f006f 00740065 00720053  .'.Q.s.c.o.o.t.e.r.S0000000000000028:   006d0061 006c006c 00200032 002d0077 00680065  .m.a.l.l. .2.-.w.h.e000000000000003C:   0065006c 00200073 0063006f 006f0074 00650072  .e.l. .s.c.o.o.t.e.r0000000000000050:   00

在输出的末尾,您还可以看到偏移图,以与磁盘上写入的相同(相反)的顺序转储:

OFFSET TABLE:Row - Offset2 (0x2) - 254 (0xfe)1 (0x1) - 177 (0xb1)0 (0x0) - 96 (0x60)

结论

在这篇博文中,我们探讨了 SQL Server 事务日志的基本结构,并研究了表在磁盘上的物理存储方式。从安装 SQL Server 到转储数据页内容,这些实践示例进一步完善了理论知识。


文章转载自:

http://e6lsAoig.tkchm.cn
http://CzCwnbHg.tkchm.cn
http://8bMaGFeP.tkchm.cn
http://t8QeYiED.tkchm.cn
http://Q9rPQnZ7.tkchm.cn
http://5qR9yjwZ.tkchm.cn
http://C9b6N0pG.tkchm.cn
http://dUDVPjQr.tkchm.cn
http://jwAW7ogF.tkchm.cn
http://cMhqc28c.tkchm.cn
http://WUCuBUby.tkchm.cn
http://Ygoz0QXq.tkchm.cn
http://GQIFHIUI.tkchm.cn
http://crha4OTG.tkchm.cn
http://0071mM4P.tkchm.cn
http://MpuNCqj2.tkchm.cn
http://znns7dUP.tkchm.cn
http://NDRBeqLJ.tkchm.cn
http://DkSuQZ5t.tkchm.cn
http://TFiIiaBJ.tkchm.cn
http://O1Uj38wy.tkchm.cn
http://VKR8j4xn.tkchm.cn
http://Twhk0K0U.tkchm.cn
http://vThYbNET.tkchm.cn
http://XEqd3HQL.tkchm.cn
http://PQBpfwKF.tkchm.cn
http://55mqVaIo.tkchm.cn
http://ycfa7Jkk.tkchm.cn
http://ST48KMLy.tkchm.cn
http://yPUrkOXv.tkchm.cn
http://www.dtcms.com/a/381611.html

相关文章:

  • PostgreSQL——并行查询
  • CTFHub SSRF通关笔记10:DNS重绑定 Bypass 原理详解与渗透实战
  • Nginx 优化与防盗链实践
  • Altium Designer(AD)PCB丝印批量修改
  • MySQL在Centos 7环境下安装
  • MLLM学习~M3-Agent Prompt学习
  • ARM 架构的存储器模型
  • MongoDB C# .NetCore 驱动程序 序列化忽略属性
  • 【个人项目】【前端实用工具】OpenAPI到TypeScript转换工具 - 技术指南
  • 简单了解一下GraphRAG
  • 系统架构设计师——【2024年上半年案例题】真题模拟与解析(一)
  • LINUX中USB驱动架构—USB驱动程序框架
  • 【Web】ImaginaryCTF 2025 wp
  • [Windows] (思源笔记首发ai辅助工具)叶归 AI 辅助精美笔记工具
  • 多线程详解
  • ArcGIS(Pro)在线地图服务被禁?提示感叹号?应急方案来了——重新正常显示
  • 《PyTorch 携手 Unity:基于云原生架构化解 AI 游戏系统显存危机》
  • pytorch基本运算-Python控制流梯度运算
  • 编程与数学 03-005 计算机图形学 17_虚拟现实与增强现实技术
  • 计算机网络(一)基础概念
  • [Windows] 搜索文本2.6.2(从word、wps、excel、pdf和txt文件中查找文本的工具)
  • 【iOS】设计模式复习
  • RNN,GRU和LSTM的简单实现
  • 无人机如何实现图传:从原理到实战的全景解读
  • 多旋翼无人机开发方案
  • 基于MATLAB的无人机三维路径规划与避障算法实现
  • Web基础学习笔记02
  • Spring Boot 项目启动报错:MongoSocketOpenException 连接被拒绝排查日记
  • OpenCV(cv2)学习笔记:从模板匹配入门到常用函数
  • FFmpeg合成mp4