前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >[ibd2sql] mysql做过online ddl(instant)的数据应该怎么解析?

[ibd2sql] mysql做过online ddl(instant)的数据应该怎么解析?

原创
作者头像
大大刺猬
发布2024-07-09 17:01:13
2340
发布2024-07-09 17:01:13
举报
文章被收录于专栏:大大刺猬

ibd2sql的诞生过程

  1. 最开始解析ibd文件的时候, 只是一个脚本, 方便了解ibd文件的结构的:MYSQL INNODB ibd文件详解 (1)-腾讯云开发者社区-腾讯云 (tencent.com)
  2. 既然都能解析结构了, 那就顺便提取下数据(ddl+dml):MYSQL INNODB ibd文件详解 (2) 提取DDL和DML-腾讯云开发者社区-腾讯云 (tencent.com) 这时候还只是3个脚本而已.
  3. 接着再更新下元数据信息:MYSQL INNODB ibd文件详解 (3) FIL_PAGE_SDI-腾讯云开发者社区-腾讯云 (tencent.com)
  4. v0.1 信息都解析得差不多了, 那就整一套完整的工具吧. 能解析基础的数据类型就行.(这时候只支持基础数据类型):使用ibd2sql解析ibd文件生成 DDL和DML-腾讯云开发者社区-腾讯云 (tencent.com)
  5. v0.2 然后支持了更多的数据类型
  6. v0.3 对于从5.7升级上来的库, sdi page不是固定的位置, 而是记录在第一页里面的: mysql 寻找SDI PAGE-腾讯云开发者社区-腾讯云 (tencent.com)
  7. v1.0 增加了debug功能, 主要是方便调试的:ibd2sql ibd2sql v1.0 发布 & ibd文件结构说明-腾讯云开发者社区-腾讯云 (tencent.com)
  8. v1.1 修复一些BUG v1.2支持空间坐标, 也算是完善了数据类型, v1.3 支持mysql 5.x v1.4 支持溢出页和子分区. 基本上算是全部支持了.
  9. v1.5 对做过ONLINE DDL的字段重新做了个解析,(之前遇到的相关BUG,都是临时修一下.), 也是本文的重点. 项目地址: https://github.com/ddcw/ibd2sql

简介

我们知道mysql在8.0.12引入了INSTANT(online ddl),可以快速的插入列.

在8.0.29 完善了INSTANT(使用row version flag代替instant flag), 使其支持drop列.

原理比较好理解, 就是只修改元数据信息. 读取数据的时候, 根据元数据信息判断改字段是否做过online ddl.

最开始只有个instant flag, 用来标记该行数据是否是在online ddl之后更新的数据(若为online ddl之后操作的, 则存在完整的数据, 若为online ddl之前操作的,则读取字段的默认值, nullable也不考虑该字段.)

该方式使用太有限了, 毕竟只支持快速add column, 如果要删除某一列, 还是得重建表. 于是在8.0.29又引入了row version, 用来标记该行数据的版本, 元数据里面也记录数据行版本. 如果行数据里面的row version比元数据里面的版本信息, 则行数据记录更完整(要考虑nullable和数据). 文字描述还是比较枯燥的, 我们来使用图和代码表示吧.

instant实现逻辑

先介绍几个概念:

对象

描述

INSTANT FLAG

标记是否存在instant 在record第1bit位置, 仅存在于8.0.12-8.0.28版本中

ROW VERSION FLAG

标记是否存在row version 在record第2bit位置, 存在于8.0.29及其以上版本

COLUMN COUNT

字段数量, 当存在INSTANT FLAG时存在, 使用1-2字节表示. 位于null bitmask和record header之间. 包含rowid,trx,rollptr.

ROW VERSION

数据行版本, 当存在ROW VERSION FLAG时存在, 使用1字节表示. 每次add/drop时+1, 最大64. 用来记录该行数据是否为最新的

VERSION_DROPPED

元数据中记录的该字段被删除后的版本

VERSION_ADDED

元数据中记录的该字段添加后的版本

代码逻辑参考

代码语言:python
代码运行次数:0
复制
# ROW VERSION/COLUMN_COUNT判断
if self.table.mysqld_version_id < 80029 and self.table.mysqld_version_id >=80012 and rheader.instant_flag:
  _COLUMN_COUNT = self._read_innodb_varsize()
elif if rheader.row_version_flag:
  ROW_VERSION = self._read_innodb_varsize()

# NULL BITMASK计算
for _phno,colno in self.table.column_ph:
 col = self.table.column[colno]
 if rheader.row_version_flag:
    if (ROW_VERSION >= col['version_added'] and (col['version_dropped'] == 0 or col['version_dropped'] > ROW_VERSION)) or (col['version_dropped'] > ROW_VERSION and ROW_VERSION >= col['version_added']):
       null_bitmask_count += 1 if col['is_nullable'] else 0
    else:
       null_bitmask_count += 1 if col['is_nullable'] else 0

# 读取索引(注意前缀索引问题)
                        if self.haveindex: #有索引的时候
                                for colno,prefix_key,order in self.table.index[self.idxno]['element_col']:
                                        col = self.table.column[colno]
                                        self.debug(f"\tREAD KEY COLNO:{colno} NAME:{col['name']}")

                                        #pk 不需要判断null
                                        if prefix_key == 0:
                                                _data[colno],_expage[colno] = self._read_field(col)
                                                col_count -= 1
                                                self.debug(f"\tREAD KEY NO:{colno} NAME:{col['name']} FINISH. VALUES: {_data[colno]}")
                                        else:
                                                _,__ = self._read_field(col)
                                                self.debug(f'前缀索引数据为: {_} . SKIP IT.' )

                        else:
                                self.debug("\tNO CLUSTER KEY.WILL READ 6 bytes(ROWID)",)
                                _row_id = self._read_uint(6) #ROW_ID
                                self.debug(f"\t ROW_ID:{_row_id}")
 
# 读取TRX&ROLLPTR
_row['trx'] = self._read_uint(6)
_row['rollptr'] = self._read_uint(7)

# 读取数据. 存在多种条件
      for _phno,colno in self.table.column_ph:
        if colno in _data: # KEY
          continue
        col = self.table.column[colno]
        self.debug(f"\tNAME: {col['name']}  VERSION_ADDED:{col['version_added']}  VERSION_DROPED:{col['version_dropped']} COL_INSTANT:{col['instant']} ROW VERSION:{ROW_VERSION}")
        if rheader.row_version_flag: # >=8.0.29 的online ddl
          if (ROW_VERSION >= col['version_added'] and (col['version_dropped'] == 0 or col['version_dropped'] > ROW_VERSION)) or (col['version_dropped'] > ROW_VERSION and ROW_VERSION >= col['version_added']):
            if col['is_nullable']:
              _nullable_count += 1
            if col['is_nullable'] and null_bitmask&(1<<_nullable_count):
              _data[colno],_expage[colno] = None,None
              self.debug(f"######## DDCW FLAG 1 ########")
            else:
              _data[colno],_expage[colno] = self._read_field(col)
              self.debug(f"######## DDCW FLAG 2 ########")
          elif ROW_VERSION < col['version_added']:
            _data[colno] = col['default'] if not col['instant_null'] else None
            _expage[colno] = None
            self.debug(f"######## DDCW FLAG 3 ########")
          #elif col['version_dropped'] <= ROW_VERSION:
          else:
            _data[colno],_expage[colno] = None,None
            self.debug(f"######## DDCW FLAG 4 ########")
        elif ( not rheader.instant_flag and col['instant']): # 8.0.12-28 的online ddl
          self.debug(f'mysql 8.0.12-28 版本的add column (instant) 读默认值 (实际版本:{self.table.mysqld_version_id}) ')
          _data[colno] = col['default'] if not col['instant_null'] else None
          _expage[colno] = None
          self.debug(f"######## DDCW FLAG 5 ########")
        elif not rheader.instant and col['version_dropped'] > 0: # 删除的字段就不读了(无instant情况下)
          _data[colno],_expage[colno] = None,None
          self.debug(f"######## DDCW FLAG 6 ########")
        #elif col['instant'] and rheader.instant_flag:
        else:
          #self.debug(f"NULLABLE {col['name']}:  is_nullable:{col['is_nullable']} null_bitmask:{null_bitmask} _nullable_count:{_nullable_count}")
          if col['is_nullable']:
            _nullable_count += 1
          if col['is_nullable'] and null_bitmask&(1<<_nullable_count):
            _data[colno],_expage[colno] = None,None
            self.debug(f"######## DDCW FLAG 7 ########")
          else:
            _data[colno],_expage[colno] = self._read_field(col)
            self.debug(f"######## DDCW FLAG 8 ########")

看起来读取数据的逻辑比较复杂, 主要是涉及到 ROW_VERSION,VERSION_DROPPED,VERSION_ADDED之间的判断. 即一个字段 可能是instant add的, 然后写入n行数据后, 又被instant drop了. 也就是那一列的数据可能得读元数据, 也可能是记录在data位置的. 要看row version, verion dropped/added之间的关系了.

逻辑如下(不考虑nullable)

较为复杂. 实际读取的时候,还得考虑nullable, 其实也就2种情况要考虑nullable.

  1. 非instant情况
  2. 行数据记录了instant之后的数据(即使已经被删除了)

演示

我们直接使用8.0.28和8.0.38来演示把. (5.7的不在本次演示范围内).

不放完整过程了, 太长了.(新增: 60个随机的online ddl)

8.0.28测试结果如下:

8.0.38如下

9.0.0的还会做vector的解析.

总结

当解析做过instant的字段时, 主要就是考虑row version和元数据版本的关系, 若row version版本更高, 基本上都不考虑, 因为行数据就是最高版本了.主要是考虑row version版本低的时候, 如果低于version_added, 就说明要读元数据里面的默认值了(毕竟做instant的时候,只修改了元数据信息). 如果低于version_dropped, 还是正常读, 只不过读出来的字段不要放到sql里面(毕竟已经在逻辑上被删除了).

instant的相关BUG基本上就算解决了.

ibd2sql下载地址: https://github.com/ddcw/ibd2sql

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • ibd2sql的诞生过程
  • 简介
    • instant实现逻辑
      • 代码逻辑参考
        • 逻辑如下(不考虑nullable)
        • 演示
          • 8.0.28测试结果如下:
            • 8.0.38如下
            • 总结
            相关产品与服务
            云数据库 MySQL
            腾讯云数据库 MySQL(TencentDB for MySQL)为用户提供安全可靠,性能卓越、易于维护的企业级云数据库服务。其具备6大企业级特性,包括企业级定制内核、企业级高可用、企业级高可靠、企业级安全、企业级扩展以及企业级智能运维。通过使用腾讯云数据库 MySQL,可实现分钟级别的数据库部署、弹性扩展以及全自动化的运维管理,不仅经济实惠,而且稳定可靠,易于运维。
            领券
            问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档