如何[MYSQL] REDUNDANT行格式的数据解析?
- 内容介绍
- 文章标签
- 相关推荐
哎,又是MySQL REDUNDANT格式,这玩意儿还没死绝吗?
蚌埠住了! 说实话,看到REDUNDANT这三个字我就头疼。都什么年代了大家不都早就用DY不结盟IC或者COMPRESSED了吗?这REDUNDANT格式简直就是数据库界的古董, 就像是你家阁楼里那个不知道哪年买的、积满灰尘的旧收音机。你不想用它,但是它就在那儿,而且有时候你还真得把它打开听听里面到底在放什么歌。特别是当我们需要去解析一些老旧系统的.ibd文件, 或者做一些数据恢复的时候,这玩意儿就像幽灵一样冒出来。
很多人问我,怎么去解析这种格式的数据?网上的文章一搜一大把,但是说实话,大部分都写得跟教科书似的,看着就困。而且, 现在的工具大多都只支持DY不结盟IC和COMPRESSED比如我们之前写的那个ibd2sql工具,之前就不支持REDUNDANT。这就像是你有一把瑞士军刀,后来啊发现上面唯独缺了开啤酒瓶的那根针,气人不气人?

所以今天我们就来硬啃这块骨头。不管它多老,多难吃,我们都要把它给嚼碎了咽下去。我们要写一个脚本来解析它, 说白了就是... 而且要写得简单粗暴,毕竟人生苦短,何必在那些花里胡哨的代码上浪费时间呢?
这破格式到底长啥样?
抄近道。 先别急着写代码,我们得先搞清楚这REDUNDANT到底是个什么鬼东西。在InnoDB 1.0.X版本之前,也就是MySQL 5.1那会儿,这玩意儿还是默认格式之一。它的设计目标好像并不是为了省空间,而是为了... 嗯,为了兼容?或者是为了显得数据很丰富?
你看啊, DY不结盟IC格式多聪明,它有个专门的nullable bitmap而且只记录变长字段的长度,像INT这种固定长度的字段,它根本懒得去记长度。但是REDUNDANT就不一样了它通通都记录长度!不管你是变长的还是固定的,它都要记一笔。而且, 它也不再根据某个字段是否达到128字节来决定是用1个字节还是2个字节来表示长度了它是通通使用1或者2个字节。 我破防了。 这看起来是不是有点浪费?简直太浪费了!但是毕竟人家名字都叫REDUNDANT了能不冗余么?这就好比你去买包子,明明只要两个馅,它非给你塞五个,撑死你。
而且,这REDUNDANT格式对于BLOB数据的处理也很奇葩。新的DY不结盟IC格式, 如果数据太长,直接在行里存个20字节的指针,剩下的全扔到溢出页去,多干净利落。但是REDUNDANT呢? 不堪入目。 它非要先把前768个字节的前缀存在索引记录里剩下的才去溢出页。这就像是你搬家,明明可以叫个货车把东西全拉走,它非要你先背个大包,手里再提个箱子,累死你。
我们来看看它的Record Header,也就是记录头。这玩意儿比DY不结盟IC格式多了1个字节。它不再记录行是MIN、MAX、叶子还是非叶子了取而代之的是一些奇奇怪怪的标志。比如它多了一个记录var字段是用1字节还是2字节表示的标志,还多了个本行有多少字段的计数。最要命的是下一字段的位置变成了绝对位置,所以它是无符号的。这解析起来稍微不注意就会踩坑,琢磨琢磨。。
这里有个表格, 大家感受一下这几种格式的区别,简直是惨不忍睹的对比:
| 特性 | REDUNDANT | COMPACT | DY不结盟IC |
|---|---|---|---|
| 出现年代 | MySQL 5.0 之前 | MySQL 5.0+ | MySQL 5.7+ |
| 存储效率 | 低 | 中 | 高 |
| NULL值处理 | CHAR的NULL占空间 | 不占空间 | 不占空间 |
| 溢出页处理 | 存768字节前缀 | 存768字节前缀 | 只存20字节指针 |
| 索引前缀限制 | 768字节 | 767字节 | 3072字节 |
看到了吧?这就是为什么我们讨厌REDUNDANT。但是没办法, 我舒服了。 为了解析数据,我们还得去研究它的每一个字节。
直接开演, 写个脚本怼上去
说了这么多废话,那我们就来解析解析REDUNDANT格式吧。为了方便使用,我们就单独写个脚本来解析。但是 有部分数据类型存储比较复杂,比如decimaljson还有那些溢出页。如果单独写的话,脚本会变得超级长,长得让人想吐。所以对于那部分数据,我们决定偷个懒,直接把它们置为null。
当然不是直接硬编码为null那样太low了。我们的设计思路是:先尝试引入ibd2sql的包, 如果引入成功,就用它的高级功能来解析;如果引入失败, 就这样吧... 那就老老实实返回null。这样我们的脚本健壮性就高一些,显得我们很有水平。这就像是你去相亲,先看对方有没有钱,没有钱就... 咳咳,跑题了。
出道即巅峰。 我们来看看这个脚本的核心逻辑。先说说 我们得定义一些类,比如IBDREADER用来读文件,ROWREAD用来读行数据。
try:
HAVE_IBD2SQL = True # 部分数据懒得解析了, 直接沿用ibd2sql去实现. 不行的话,就取为null
from import first_blob
from _json import jsonob
from _page import page
from _page_index import char_decode
from import COLLID_TO_CHAR
from _type import innodb_type_isvar
class MINI_PAGE:
def __init__:
super.__init__
_data = b''
def read:
return _data
except:
HAVE_IBD2SQL = False
看到了吗?这就是所谓的“优雅降级”。如果ibd2sql不在我们也不慌, 我是深有体会。 大不了少解析几个字段。反正主要的数据都能拿出来就行。
接下来就是最恶心的部分了:解析Record Header。在REDUNDANT里 这个头信息包含了row_version_flagmin_recdeletedownedheap_non_fieldsbyte1_flagnext_record等等。 整一个... 特别是这个byte1_flag它决定了我们是用1个字节还是2个字节去读后面的偏移量。而且,这个偏移量还是绝对位置,不是相对位置,这点一定要记住了不然算出来的数据全是错的。
还有一个坑,就是NULL值的判断。在REDUNDANT里它是用偏移量的最高位来表示是否为NULL的。如果byte1_flag是1, 那就看第1个bit;如果是0,那就看第15个bit。如果那个bit是1,就表示这字段是NULL。但是 即使它是NULL,它也占用了长度信息的位置,我们还得去读那个长度,只不过读出来的数据我们要扔掉。这设计,简直反人类,我的看法是...。
实战演练,搞个表测测
光说不练假把式。我们得建个表, 把所有常见的字段类型都塞进去,然后改成ROW_FORMAT=REDUNDANT再往里面插点乱七八糟的数据,再说说用我们的脚本来解析,看看能不能还原回去。
这是建表和插入数据的SQL,大家可以直接拿去跑:,奥利给!
-- 创建测试表
CREATE TABLE `t20241206_test_redundant` (
`id` int NOT NULL DEFAULT '0',
`int_col` int DEFAULT NULL,
`tinyint_col` tinyint DEFAULT '1',
`smallint_col` smallint DEFAULT NULL,
`mediumint_col` mediumint DEFAULT NULL,
`bigint_col` bigint DEFAULT NULL,
`float_col` float DEFAULT NULL,
`double_col` double DEFAULT NULL,
`decimal_col` decimal DEFAULT NULL,
`date_col` date DEFAULT NULL,
`datetime_col` datetime DEFAULT NULL,
`timestamp_col` timestamp NULL DEFAULT NULL,
`time_col` time DEFAULT NULL,
`year_col` year DEFAULT NULL,
`char_col` char CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL,
`varchar_col` varchar CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL,
`binary_col` binary DEFAULT NULL,
`varbinary_col` varbinary DEFAULT NULL,
`bit_col` bit DEFAULT NULL,
`enum_col` enum CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL,
`set_col` set CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL,
`josn_type` json DEFAULT NULL
) ENGINE=InnoDB ROW_FORMAT=REDUNDANT;
-- 插入测试数据
INSERT INTO t20241206_test_redundant values;
这表建得够全了吧?什么json decimalenumset全都有。特别是那个josn_type在REDUNDANT里解析起来还是挺麻烦的,别犹豫...。
然后我们用脚本去跑。这里有个小技巧, 主要原因是我们这个脚本是个Mini版本,不支持压缩、加密之类的,页也不支持溢出页、json、decimal之类的。所以我们直接遍历整个数据文件,只要PAGE符合要求,我们就强行解析。这种方式虽然粗暴,但是有效,避免了顺着叶子节点走遇到坏块就迷路的问题。
命令大概是这样的:
python3 ibd2sql_mini_for_redundant.py /data/mysql_3314/mysqldata/db1/t20241206_test_redundant.ibd
如果是5.7的表或者分区表, 那就麻烦一点,还得加个参数,指定一个8.0里的同样结构的表, 说真的... 为了获取表结构信息。毕竟5.7没有SDI,我们得从别的地方把字典信息搞过来。
python3 ibd2sql_mini_for_redundant.py /data/mysql_3308/mysqldata/db1/t20241206_test_redundant.ibd /data/mysql_3314/mysqldata/db2/t20241206_test_redundant.ibd
跑完之后它会吐出一堆INSERT语句。我们把这些语句存起来 或者直接管道导入到另一个数据库里然后校验一下checksum。如果数据一致,那就说明我们解析对了;如果不一致,那就... 呃,那就再改改代码呗,害...。
代码里的那些坑和噪音
醉了... 我必须得吐槽一下这个脚本里的某些逻辑。比如那个read_nullandsize函数, 它既要读长度,又要判断是否为NULL,还要计算偏移量。在REDUNDANT里 偏移量是绝对位置,所以我们要用当前的值减去上一个字段的结束位置,才能得到当前字段的长度。这逻辑绕得人头晕。
还有那个read_filed函数,里面一堆if-elif-else。主要原因是REDUNDANT对各种类型的存储方式跟DY不结盟IC有点细微差别,特别是时间类型。比如datetime它把年月合在一起存了我们还得把它拆开。这活儿真不是人干的。
KTV你。 不过好在try...except大法好。对于那些设计得不那么科学的类, 或者我们懒得处理的类型,我们直接except住返回个None或者0x...。只要不报错崩掉,就是好脚本。这就是所谓的“鲁棒性”,懂不懂?
这里再插个表格, 看看我们这个脚本到底支持哪些字段,哪些是摆设:
| 数据类型 | 支持状态 | 备注 |
|---|---|---|
| INT, TINYINT, SMALLINT... | 完美支持 | 这种简单的要是搞不定,我就删库跑路 |
| VARCHAR, CHAR | 支持 | 如果有ibd2sql包,字符集转换更准 |
| DATE, DATETIME, TIME | 支持 | 位运算拆得我手都酸了 |
| DECIMAL | 半残 | 必须有ibd2sql包,否则NULL |
| JSON | 半残 | 同上,这玩意儿太复杂了 |
| BLOB/TEXT | 半残 | 只能读前768字节,除非有包支持 |
再说说的一点废话
这个REDUNDANT格式虽然老,虽然坑多,但是只要我们耐心点,把它的那些奇怪的规则摸透了还是能把数据给抠出来的。这个脚本虽然写得有点烂,有点随意,甚至有点“噪音”,但是它实用啊!这就够了。
大家在使用的时候, 记得把那个ibd2sql的库装好,不然decimal和json的数据你就别想了。还有,如果是5.7的表, 搞起来。 记得找个8.0的空表结构来辅助,不然SDI解析不出来脚本会直接报错退出的。别怪我没提醒你。
好了废话不多说代码都在上面了自己拿去跑吧。跑不通再来骂我。反正数据是能解析出来的, 不错。 这就完事了。这年头,能把REDUNDANT数据救回来的工具也不多了且用且珍惜吧。
哎,又是MySQL REDUNDANT格式,这玩意儿还没死绝吗?
蚌埠住了! 说实话,看到REDUNDANT这三个字我就头疼。都什么年代了大家不都早就用DY不结盟IC或者COMPRESSED了吗?这REDUNDANT格式简直就是数据库界的古董, 就像是你家阁楼里那个不知道哪年买的、积满灰尘的旧收音机。你不想用它,但是它就在那儿,而且有时候你还真得把它打开听听里面到底在放什么歌。特别是当我们需要去解析一些老旧系统的.ibd文件, 或者做一些数据恢复的时候,这玩意儿就像幽灵一样冒出来。
很多人问我,怎么去解析这种格式的数据?网上的文章一搜一大把,但是说实话,大部分都写得跟教科书似的,看着就困。而且, 现在的工具大多都只支持DY不结盟IC和COMPRESSED比如我们之前写的那个ibd2sql工具,之前就不支持REDUNDANT。这就像是你有一把瑞士军刀,后来啊发现上面唯独缺了开啤酒瓶的那根针,气人不气人?

所以今天我们就来硬啃这块骨头。不管它多老,多难吃,我们都要把它给嚼碎了咽下去。我们要写一个脚本来解析它, 说白了就是... 而且要写得简单粗暴,毕竟人生苦短,何必在那些花里胡哨的代码上浪费时间呢?
这破格式到底长啥样?
抄近道。 先别急着写代码,我们得先搞清楚这REDUNDANT到底是个什么鬼东西。在InnoDB 1.0.X版本之前,也就是MySQL 5.1那会儿,这玩意儿还是默认格式之一。它的设计目标好像并不是为了省空间,而是为了... 嗯,为了兼容?或者是为了显得数据很丰富?
你看啊, DY不结盟IC格式多聪明,它有个专门的nullable bitmap而且只记录变长字段的长度,像INT这种固定长度的字段,它根本懒得去记长度。但是REDUNDANT就不一样了它通通都记录长度!不管你是变长的还是固定的,它都要记一笔。而且, 它也不再根据某个字段是否达到128字节来决定是用1个字节还是2个字节来表示长度了它是通通使用1或者2个字节。 我破防了。 这看起来是不是有点浪费?简直太浪费了!但是毕竟人家名字都叫REDUNDANT了能不冗余么?这就好比你去买包子,明明只要两个馅,它非给你塞五个,撑死你。
而且,这REDUNDANT格式对于BLOB数据的处理也很奇葩。新的DY不结盟IC格式, 如果数据太长,直接在行里存个20字节的指针,剩下的全扔到溢出页去,多干净利落。但是REDUNDANT呢? 不堪入目。 它非要先把前768个字节的前缀存在索引记录里剩下的才去溢出页。这就像是你搬家,明明可以叫个货车把东西全拉走,它非要你先背个大包,手里再提个箱子,累死你。
我们来看看它的Record Header,也就是记录头。这玩意儿比DY不结盟IC格式多了1个字节。它不再记录行是MIN、MAX、叶子还是非叶子了取而代之的是一些奇奇怪怪的标志。比如它多了一个记录var字段是用1字节还是2字节表示的标志,还多了个本行有多少字段的计数。最要命的是下一字段的位置变成了绝对位置,所以它是无符号的。这解析起来稍微不注意就会踩坑,琢磨琢磨。。
这里有个表格, 大家感受一下这几种格式的区别,简直是惨不忍睹的对比:
| 特性 | REDUNDANT | COMPACT | DY不结盟IC |
|---|---|---|---|
| 出现年代 | MySQL 5.0 之前 | MySQL 5.0+ | MySQL 5.7+ |
| 存储效率 | 低 | 中 | 高 |
| NULL值处理 | CHAR的NULL占空间 | 不占空间 | 不占空间 |
| 溢出页处理 | 存768字节前缀 | 存768字节前缀 | 只存20字节指针 |
| 索引前缀限制 | 768字节 | 767字节 | 3072字节 |
看到了吧?这就是为什么我们讨厌REDUNDANT。但是没办法, 我舒服了。 为了解析数据,我们还得去研究它的每一个字节。
直接开演, 写个脚本怼上去
说了这么多废话,那我们就来解析解析REDUNDANT格式吧。为了方便使用,我们就单独写个脚本来解析。但是 有部分数据类型存储比较复杂,比如decimaljson还有那些溢出页。如果单独写的话,脚本会变得超级长,长得让人想吐。所以对于那部分数据,我们决定偷个懒,直接把它们置为null。
当然不是直接硬编码为null那样太low了。我们的设计思路是:先尝试引入ibd2sql的包, 如果引入成功,就用它的高级功能来解析;如果引入失败, 就这样吧... 那就老老实实返回null。这样我们的脚本健壮性就高一些,显得我们很有水平。这就像是你去相亲,先看对方有没有钱,没有钱就... 咳咳,跑题了。
出道即巅峰。 我们来看看这个脚本的核心逻辑。先说说 我们得定义一些类,比如IBDREADER用来读文件,ROWREAD用来读行数据。
try:
HAVE_IBD2SQL = True # 部分数据懒得解析了, 直接沿用ibd2sql去实现. 不行的话,就取为null
from import first_blob
from _json import jsonob
from _page import page
from _page_index import char_decode
from import COLLID_TO_CHAR
from _type import innodb_type_isvar
class MINI_PAGE:
def __init__:
super.__init__
_data = b''
def read:
return _data
except:
HAVE_IBD2SQL = False
看到了吗?这就是所谓的“优雅降级”。如果ibd2sql不在我们也不慌, 我是深有体会。 大不了少解析几个字段。反正主要的数据都能拿出来就行。
接下来就是最恶心的部分了:解析Record Header。在REDUNDANT里 这个头信息包含了row_version_flagmin_recdeletedownedheap_non_fieldsbyte1_flagnext_record等等。 整一个... 特别是这个byte1_flag它决定了我们是用1个字节还是2个字节去读后面的偏移量。而且,这个偏移量还是绝对位置,不是相对位置,这点一定要记住了不然算出来的数据全是错的。
还有一个坑,就是NULL值的判断。在REDUNDANT里它是用偏移量的最高位来表示是否为NULL的。如果byte1_flag是1, 那就看第1个bit;如果是0,那就看第15个bit。如果那个bit是1,就表示这字段是NULL。但是 即使它是NULL,它也占用了长度信息的位置,我们还得去读那个长度,只不过读出来的数据我们要扔掉。这设计,简直反人类,我的看法是...。
实战演练,搞个表测测
光说不练假把式。我们得建个表, 把所有常见的字段类型都塞进去,然后改成ROW_FORMAT=REDUNDANT再往里面插点乱七八糟的数据,再说说用我们的脚本来解析,看看能不能还原回去。
这是建表和插入数据的SQL,大家可以直接拿去跑:,奥利给!
-- 创建测试表
CREATE TABLE `t20241206_test_redundant` (
`id` int NOT NULL DEFAULT '0',
`int_col` int DEFAULT NULL,
`tinyint_col` tinyint DEFAULT '1',
`smallint_col` smallint DEFAULT NULL,
`mediumint_col` mediumint DEFAULT NULL,
`bigint_col` bigint DEFAULT NULL,
`float_col` float DEFAULT NULL,
`double_col` double DEFAULT NULL,
`decimal_col` decimal DEFAULT NULL,
`date_col` date DEFAULT NULL,
`datetime_col` datetime DEFAULT NULL,
`timestamp_col` timestamp NULL DEFAULT NULL,
`time_col` time DEFAULT NULL,
`year_col` year DEFAULT NULL,
`char_col` char CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL,
`varchar_col` varchar CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL,
`binary_col` binary DEFAULT NULL,
`varbinary_col` varbinary DEFAULT NULL,
`bit_col` bit DEFAULT NULL,
`enum_col` enum CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL,
`set_col` set CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL,
`josn_type` json DEFAULT NULL
) ENGINE=InnoDB ROW_FORMAT=REDUNDANT;
-- 插入测试数据
INSERT INTO t20241206_test_redundant values;
这表建得够全了吧?什么json decimalenumset全都有。特别是那个josn_type在REDUNDANT里解析起来还是挺麻烦的,别犹豫...。
然后我们用脚本去跑。这里有个小技巧, 主要原因是我们这个脚本是个Mini版本,不支持压缩、加密之类的,页也不支持溢出页、json、decimal之类的。所以我们直接遍历整个数据文件,只要PAGE符合要求,我们就强行解析。这种方式虽然粗暴,但是有效,避免了顺着叶子节点走遇到坏块就迷路的问题。
命令大概是这样的:
python3 ibd2sql_mini_for_redundant.py /data/mysql_3314/mysqldata/db1/t20241206_test_redundant.ibd
如果是5.7的表或者分区表, 那就麻烦一点,还得加个参数,指定一个8.0里的同样结构的表, 说真的... 为了获取表结构信息。毕竟5.7没有SDI,我们得从别的地方把字典信息搞过来。
python3 ibd2sql_mini_for_redundant.py /data/mysql_3308/mysqldata/db1/t20241206_test_redundant.ibd /data/mysql_3314/mysqldata/db2/t20241206_test_redundant.ibd
跑完之后它会吐出一堆INSERT语句。我们把这些语句存起来 或者直接管道导入到另一个数据库里然后校验一下checksum。如果数据一致,那就说明我们解析对了;如果不一致,那就... 呃,那就再改改代码呗,害...。
代码里的那些坑和噪音
醉了... 我必须得吐槽一下这个脚本里的某些逻辑。比如那个read_nullandsize函数, 它既要读长度,又要判断是否为NULL,还要计算偏移量。在REDUNDANT里 偏移量是绝对位置,所以我们要用当前的值减去上一个字段的结束位置,才能得到当前字段的长度。这逻辑绕得人头晕。
还有那个read_filed函数,里面一堆if-elif-else。主要原因是REDUNDANT对各种类型的存储方式跟DY不结盟IC有点细微差别,特别是时间类型。比如datetime它把年月合在一起存了我们还得把它拆开。这活儿真不是人干的。
KTV你。 不过好在try...except大法好。对于那些设计得不那么科学的类, 或者我们懒得处理的类型,我们直接except住返回个None或者0x...。只要不报错崩掉,就是好脚本。这就是所谓的“鲁棒性”,懂不懂?
这里再插个表格, 看看我们这个脚本到底支持哪些字段,哪些是摆设:
| 数据类型 | 支持状态 | 备注 |
|---|---|---|
| INT, TINYINT, SMALLINT... | 完美支持 | 这种简单的要是搞不定,我就删库跑路 |
| VARCHAR, CHAR | 支持 | 如果有ibd2sql包,字符集转换更准 |
| DATE, DATETIME, TIME | 支持 | 位运算拆得我手都酸了 |
| DECIMAL | 半残 | 必须有ibd2sql包,否则NULL |
| JSON | 半残 | 同上,这玩意儿太复杂了 |
| BLOB/TEXT | 半残 | 只能读前768字节,除非有包支持 |
再说说的一点废话
这个REDUNDANT格式虽然老,虽然坑多,但是只要我们耐心点,把它的那些奇怪的规则摸透了还是能把数据给抠出来的。这个脚本虽然写得有点烂,有点随意,甚至有点“噪音”,但是它实用啊!这就够了。
大家在使用的时候, 记得把那个ibd2sql的库装好,不然decimal和json的数据你就别想了。还有,如果是5.7的表, 搞起来。 记得找个8.0的空表结构来辅助,不然SDI解析不出来脚本会直接报错退出的。别怪我没提醒你。
好了废话不多说代码都在上面了自己拿去跑吧。跑不通再来骂我。反正数据是能解析出来的, 不错。 这就完事了。这年头,能把REDUNDANT数据救回来的工具也不多了且用且珍惜吧。

![如何[MYSQL] REDUNDANT行格式的数据解析?](/imgrand/BAhzlnvB.webp)