InnoDB存储引擎存储结构详解-实战篇

Posted 5ycode

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了InnoDB存储引擎存储结构详解-实战篇相关的知识,希望对你有一定的参考价值。

背景

之前学习mysql的时候,了解了到了页,段的概念,页的结构是什么,都简单的了解下了,毕竟都是纸面看到的,也没有深入源码了解。总觉的悬在上面,直接通过数据库文件反编译也比较麻烦。媳妇介绍了一个工具innodb_ruby, 说它可以扒mysql数据的结构。这几天扒拉了下,蛮好用的,好多知识也和之前的对上了。

我的mysql的配置文件如下(本地开发单机环境,没做什么优化,也没开启binlog):

[root@localhost data]# cat /etc/my.cnf
[client]
#客户端默认连接字集集,若编译安装时已指定则不用填写
#character-set-server = utf8  
#客户端连接通信端口
port = 3306    
#客户端通信的用户密码端口等信息保存文件
#socket = /data/mysql/tmp/mysql.sock   
default-character-set=utf8

# The MySQL server
[mysqld]
# join_buffer_size = 128M
# sort_buffer_size = 2M
# read_rnd_buffer_size = 2M
#配置mysql的内存大小,一般数据库服务器的80%
innodb_buffer_pool_size = 2g
##mysql服务端监听端口
port  = 3306     
# 指定服务器id
server_id = 22
#mysql数据库存放目录
datadir = /data/mysql/data/ 
#socket = /data/mysql/tmp/mysql.sock       
#服务端pid进程文件,若丢失则重启Mysql重新生成,若重启失败,
#则可能由于mysqld进程未杀死,用pkill mysql后则能重启成功Mysql
pid-file =/data/mysql/mysqld.pid
#指定错误日志目录
log-error=/data/mysql/logs/
#slow log
#slow-query-log=1
#slow_query_log_file= "slow.log"
#long_query_time=10
#binlog设置
#log_bin=mysql-bin
#binlog-format=MIXED
#独立表空间设置
innodb-file-per-table=1
explicit_defaults_for_timestamp=true
lower_case_table_names=1
sql_mode=NO_ENGINE_SUBSTITUTION,STRICT_TRANS_TABLES
performance_schema_max_table_instances=400
table_definition_cache=400
table_open_cache=256
max_allowed_packet = 32M
# 设置默认引擎
default-storage-engine=INNODB
#设置默认字符
character-set-server=utf8 
collation-server=utf8_general_ci
#设置最大链接数
max_connections=500
[root@localhost data]# ll
总用量 188488
-rw-r-----. 1 mysql mysql       56 428 23:34 auto.cnf
-rw-------. 1 mysql mysql     1672 428 23:34 ca-key.pem
-rw-r--r--. 1 mysql mysql     1112 428 23:34 ca.pem
-rw-r--r--. 1 mysql mysql     1112 428 23:34 client-cert.pem
-rw-------. 1 mysql mysql     1676 428 23:34 client-key.pem
-rw-r-----  1 mysql mysql     1140 724 15:27 ib_buffer_pool
-rw-r-----. 1 mysql mysql 79691776 724 17:00 ibdata1
-rw-r-----. 1 mysql mysql 50331648 724 17:00 ib_logfile0
-rw-r-----. 1 mysql mysql 50331648 724 17:00 ib_logfile1
-rw-r-----  1 mysql mysql 12582912 724 22:12 ibtmp1
drwxr-x---  2 mysql mysql      114 723 11:18 innodb_space
drwxr-x---. 2 mysql mysql     4096 428 23:34 mysql
drwxr-x---. 2 mysql mysql     8192 428 23:34 performance_schema
drwxr-x---  2 mysql mysql     4096 622 22:26 portal
-rw-------. 1 mysql mysql     1676 428 23:34 private_key.pem
-rw-r--r--. 1 mysql mysql      452 428 23:34 public_key.pem
-rw-r--r--. 1 mysql mysql     1112 428 23:34 server-cert.pem
-rw-------. 1 mysql mysql     1680 428 23:34 server-key.pem

安装

可以参考:https://github.com/jeremycole/innodb_ruby

mac安装

使用ruby,安装innodb_ruby

sudo  gem install innodb_ruby
# 安装成功以后
yxkdeMacBook-Pro bin % where innodb_space
/usr/local/bin/innodb_space

centos 7 安装

centos上默认ruby是2.0无法安装的,所以要先升级ruby

#卸载操作系统原有的ruby
sudo yum remove ruby
#下载ruby
wget https://cache.ruby-lang.org/pub/ruby/2.5/ruby-2.5.0.tar.gz
#解压
tar -zxvf ruby-2.5.0.tar.gz
#进入目录
cd ruby-2.5.0
#创建安装目录
sudo mkdir -p /usr/local/ruby
#配置并指定安装位置
sudo ./configure --prefix=/usr/local/ruby
#编译与安装
sudo make && sudo make install
#建立软链接
sudo ln -s /usr/local/ruby/bin/ruby /usr/local/bin/ruby
#查看ruby版本
ruby -v
#安装 innodb_ruby
gem install innodb_ruby

使用innodb_space --help 获取到命令的帮助信息

例子:
  #使用ibdata1作为系统表空间,从系统表空间中加载表名或索引的元数据,自动生成描述索引的元数据字典
  innodb_space -s ibdata1 [-T table-name [-I index-name [-R record-offset]]] [options] <mode>
  #使用 *.ibd来读取表空间结构或索引结构
  innodb_space -f file-name.ibd [-r ./describer.rb -d DescriberClass] [options] <mode>
    使用指定表名的ibd文件 读取表空间结构或索引

支持以下参数:
  --help, -?  打印命令的帮助信息
  --trace, -t
    Enable tracing of all data read. Specify twice to enable even more
    tracing (including reads during opening of the tablespace) which can
    be quite noisy.

  --system-space-file, -s <arg>
    Load the system tablespace file or files <arg>: Either a single file e.g.
    'ibdata1', a comma-delimited list of files e.g. 'ibdata1,ibdata1', or a
    directory name. If a directory name is provided, it will be scanned for all
    files named 'ibdata?' which will then be sorted alphabetically and used to
    load the system tablespace.

  If using the --system-space-file option, the following options may also
  be used:

    --table-name, -T <name>
     指定表名,简写-T.

    --index-name, -I <name>
     指定索引名称

    --system-space-tables, -x
      Allow opening tables from the system space to support system spaces with
      tables created without innodb-file-per-table enabled.

    --data-directory, -D <directory>
      Open per-table tablespace files from <directory> rather than from the
      directory where the system-space-file is located.

  --space-file, -f <file>
    指定表空间文件

  --page, -p <page>
    指定操作的page

  --record, -R <offset>
    Operate on the record located at <offset> within the index page.

  --level, -l <level>
    level=0 为数据层或回表层,level=1或2 索引层级

  --list, -L <list>
    Operate on the list <list>.

  --fseg-id, -F <fseg_id>
     指定段的编号

  --require, -r <file>
    Use Ruby's 'require' to load the file <file>. This is useful for loading
    classes with record describers.

  --describer, -d <describer>
    Use the named record describer to parse records in index pages.

The following modes are supported:
  system-spaces : 打印系统中所有表的概要信息.
  data-dictionary-tables :打印所有的表
    Print all records in the SYS_TABLES data dictionary table.

  data-dictionary-columns : 打印所有的字段
    Print all records in the SYS_COLUMNS data dictionary table.

  data-dictionary-indexes
    Print all records in the SYS_INDEXES data dictionary table.

  data-dictionary-fields
    Print all records in the SYS_FIELDS data dictionary table.

  space-summary
    统计表空间中的所有页面,可以使用--page/-p指定页码 

  space-index-pages-summary
    统计表空间内的所有’INDEX‘类型的page,对分析page的填充率和每页的记录有用。
    除了INDEX类型的页面,ALLOCATED类型的页面也会被打印出来
    可以通过-p 指定开始的页号

  space-index-fseg-pages-summary
    与 space-index-pages-summary 相同,但是一次只能通过-F 指定一个段
    
  space-index-pages-free-plot
    Use Ruby's gnuplot module to produce a scatterplot of page free space for
    all 'INDEX' and 'ALLOCATED' pages in a tablespace. More aesthetically
    pleasing plots can be produced with space-index-pages-summary output,
    but this is a quick and easy way to produce a passable plot. A starting
    page number can be provided with the --page/-p argument.

  space-page-type-regions
    Summarize all contiguous regions of the same page type. This is useful to
    provide an overall view of the space and allocations within it. A starting
    page number can be provided with the --page/-p argument.

  space-page-type-summary
    按类型统计所有的page,可以通过-p 指定起始页 

  space-indexes
    统计所有索引的申请、使用,以及填充率

  space-lists
    Print a summary of all lists in a space.

  space-list-iterate
    Iterate through the contents of a space list.

  space-extents
    Iterate through all extents, printing the extent descriptor bitmap.

  space-extents-illustrate
    Iterate through all extents, illustrating the extent usage using ANSI
    color and Unicode box drawing characters to show page usage throughout
    the space.

  space-lsn-age-illustrate
    Iterate through all pages, producing a heat map colored by the page LSN
    using ANSI color and Unicode box drawing characters, allowing the user to
    get an overview of page modification recency.

  space-inodes-fseg-id
    Iterate through all inodes, printing only the FSEG ID.

  space-inodes-summary
    Iterate through all inodes, printing a short summary of each FSEG.

  space-inodes-detail
   遍历所有的inodes节点,打印每个段的详细报告

  index-level-summary
    Print a summary of all pages at a given level (provided with the --level/-l
    argument) in an index.

  index-fseg-internal-lists
  index-fseg-leaf-lists
    Print a summary of all lists in an index file segment. Index root page must
    be provided with --page/-p.

  page-dump
    dump 页面元数据

  page-account
    Account for a page's usage in FSEGs.

  page-validate
    Validate the contents of a page.

  page-directory-summary
    Summarize the record contents of the page directory in a page. If a record
    describer is available, the key of each record will be printed.

  page-records
    Summarize all records within a page.

  page-illustrate
    Produce an illustration of the contents of a page.



Innodb表空间辅助工具使用

前期准备- 表以及数据构建

CREATE DATABASE innodb_space;
use innodb_space;

# 用户表一
drop TABLE if exists `t_user_info`;
CREATE TABLE `t_user_info` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '用户id',
  `nick_name` varchar(30) NOT NULL COMMENT '用户昵称',
  `mobile` varchar(11) NOT NULL DEFAULT '0' COMMENT '用户手机号',
  `pwd` varchar(32) NOT NULL DEFAULT '0' COMMENT '加密后的登录密码',
  `salt` varchar(10) NOT NULL DEFAULT '0' COMMENT '密码盐值',
  `status` char(1) NOT NULL DEFAULT '1' COMMENT '用户状态,1启用,0禁用',
  `tenant_id` smallint(4) DEFAULT '1001' COMMENT '租户id',
  `create_time` datetime NOT NULL COMMENT '创建时间',
  `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`),
  UNIQUE KEY `unq_user` (`mobile`,`tenant_id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='用户表';

# 用户表二(和表已唯一的区别是id` bigint(11) )
drop TABLE if exists `t_user_info1`;
CREATE TABLE `t_user_info1` (
  `id` bigint(11) NOT NULL AUTO_INCREMENT COMMENT '用户id',
  `nick_name` varchar(30) NOT NULL COMMENT '用户昵称',
  `mobile` varchar(11) NOT NULL DEFAULT '0' COMMENT '用户手机号',
  `pwd` varchar(32) NOT NULL DEFAULT '0' COMMENT '加密后的登录密码',
  `salt` varchar(10) NOT NULL DEFAULT '0' COMMENT '密码盐值',
  `status` char(1) NOT NULL DEFAULT '1' COMMENT '用户状态,1启用,0禁用',
  `tenant_id` smallint(4) DEFAULT '1001' COMMENT '租户id',
  `create_time` datetime NOT NULL COMMENT '创建时间',
  `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`),
  UNIQUE KEY `unq_user` (`mobile`,`tenant_id`) USING BTREE
) ENGINE=InnoDB  DEFAULT CHARSET=utf8 COMMENT='用户表';



-- 格式化手机号
drop function if exists `format_mobile`;
CREATE  FUNCTION `format_mobile`(m varchar(3),n int) RETURNS varchar(11) CHARSET utf8
    NO SQL
begin        
  return concat(m,LPAD(concat(n,''),9,'0')) ;    
end
;
#随机状态
drop function if exists `rand_status`;
CREATE  FUNCTION `rand_status`() RETURNS varchar(1) CHARSET utf8
    NO SQL
begin        
  return floor(rand() *100)%2;    
end
;


# 生成随机字符串函数,最大32位
drop function if exists `rand_string`;
create function `rand_string`(n int) returns varchar(32) charset utf8 no sql
begin        
  declare rand_str varchar(32) default "";        
  declare i int default 0;
  while i < n do        
      set rand_str=substring(replace(uuid(),  '-',  ''), 1, n);
      set i= i+1;        
  end while;        
  return rand_str; 
end  
;

#生成测试数据的存储过程,动态表名需要预处理语句,为了快速插入,就直接改表名
drop procedure if exists `insert_data`;
create  procedure `insert_data`(in n int) deterministic
begin  
	declare i int default 1;
	declare start_create_time datetime default now();
	-- 自己根据要灌的数据算下,1秒一条记录 一年365*24*3600=31 536 000 一年约3000万
	declare sec int default -365*3*24*3600;
	set autocommit = 0;
	while (i <= n) do
		-- 每条记录+1秒
		set sec=sec+1;
		insert into t_user_info 
		(`nick_name`,`mobile`,`pwd`,`salt`,`status`,`create_time`,`update_time`) 
		values 
		(rand_string(30),format_mobile('13',i),rand_string(32),rand_string(5),rand_status() ,date_add(start_create_time,interval sec second),date_add(start_create_time,interval sec second)) ;
		if i%500=0 then
			-- 500条提交一次
			commit;
		end if;
		set i=i+1 ;
	end while ;
	-- 提交剩余的
	commit ;
	set autocommit = 1;
end 
;


CALL insert_data(50000000);
-- 修改下表名为t_user_info1
CALL insert_data(100000000);


/* 动态表名的模板
drop procedure if exists selectByTableName;
create procedure selectByTableName(in tableName varchar(50))
begin
    #定义语句
    set @stmt = concat('select * from ',tableName);
    #预定义sql语句,从用户变量中获取
    prepare stmt from @stmt;
    #执行sql语句
    execute stmt;
    #释放资源,后续还可以使用
    deallocate prepare stmt;
end;
*/


我两张表分别灌了5000w的数据,磁盘各占用7.7gb

[root@localhost innodb_space]# ll -h
-rw-r----- 1 mysql mysql   61 716 17:38 db.opt
-rw-r----- 1 mysql mysql 8.8K 723 11:18 t_user_info1.frm
-rw-r----- 1 mysql mysql 7.7G 724 17:00 t_user_info1.ibd
-rw-r----- 1 mysql mysql 8.8K 716 18:42 t_user_info.frm
-rw-r----- 1 mysql mysql 7.7G 716 22:22 t_user_info.ibd

概念说明

段、区、页

InnoDB 为了管理好页,提出了表空间(Tablespace)的概念。

  • 表空间是一个逻辑结构,它可以对应文件系统上一个或多个真实文件
  • 表空间可以看成是InnoDB存储引擎逻辑结构的最高层,所有的数据都存放在表空间中;
  • 表空间由段(segment)、区(extent)、页(page)组成
  • 表空间分为系统表空间和独立表空间
    • 系统表空间是InnoDB默认提供的一个表空间,也可以理解为共享表空间,在mysql数据目录下,名称是ibdata1
      • 存储表的最顶层索引(可以理解为起点指针),undo log、事务相关的信息、双写缓冲(double write buffer)
        • 对应的1和2区是双写缓冲区
      • 会随着mysql的运行一直不停的增大
      • 如果没有开启独立表空间,所有表的数据、索引,插入缓冲也放在这里
      • 表删除以后占用空间降不下来
    • 独立表空间,在my.cnf中开启了innodb_file_per_table=1 ,mysql会为每张表创建一个独立表空间(表名.ibd)
      • 存储表的数据、索引和插入缓冲
      • 表删除后占用空间直接删除
      • 在mysql5.6.6及以后的版本中,InnoDB默认会使用独立表空间
    • 其他表空间
      • 通用表空间
      • undo表空间
      • 临时表空间

段 (segment) 包含256区

  • 段(segment)是逻辑概念

  • 段中不要求区与区之间是相邻的

  • 分为索引段、数据段、回滚段等

    • 当我们创建数据表、索引的时候,就会相应的创建对应的段
    • 回滚段主要用于数据回滚和多版本控制。

区(extent)包含64 页

  • 区是页的集合,一个区包含64个连续的页,一个区默认大小为1MB
  • 区能保证上面的64个页在物理上是连续的(有利于IO)
  • InnoDB每次从磁盘一次申请4~5个区
  • 申请的64个页在使用的时候会确定类型,可能存储多张表的数据
  • 在区中有这些:
    • **FSP_HDR类型:**这个类型的页面是用来登记整个表空间的一些整体属性以及本组所有的区,也就是extent 0 ~ extent 255这256个区的属性。需要注意的一点是, 整个 表空间只有一个FSP_HDR类型的页面
    • **IBUF_BITMAP类型:**这个类型的页面是存储本组所有的区的所有页面关于INSERT BUFFER的信息
    • **INODE类型:**这个类型的页面存储了许多称为INODE的数据结构
    • **XDES类型:**全称是extent descriptor,用来登记本组256个区的属性,也就是说对于在extent 256区中的该类型页面存储的就是extent 256 ~ extent 511这些区的属性

页(page)默认16kb

  • 页是InnoDB管理的最小单位
  • 页包含(文件头:38字节,文件尾8字节,其他填充数据)
  • 每个页面分配一个32位整数页码,通常称为偏移量(offset),只是页面与空间开头的偏移量
  • InnoDB的数据限制为64TB,是有页码的限制

看下独立表空间的结构

[root@localhost data]# ll innodb_space/ -h
总用量 16G
-rw-r----- 1 mysql mysql   61 7月  16 17:38 db.opt
-rw-r----- 1 mysql mysql 8.8K 7月  23 11:18 t_user_info1.frm
-rw-r----- 1 mysql mysql 7.7G 7月  24 17:00 t_user_info1.ibd
-rw-r----- 1 mysql mysql 8.8K 7月  16 18:42 t_user_info.frm
-rw-r----- 1 mysql mysql 7.7G 7月  16 22:22 t_user_info.ibd

表的基础信息

-- 查询表所属的表空间id
SELECT * FROM information_schema.innodb_sys_tablespaces WHERE name LIKE '%user_info';

从结果上,可以看出来, 行格式为dynamic,一页的大小为16kb,表空间类型为single,文件大小

该表的表空间id是53,

-- 查询表的表id
SELECT * FROM information_schema.innodb_sys_tables WHERE name LIKE '%user_info';

该表的table_id为57

-- 查看表的索引id
SELECT * FROM information_schema.innodb_sys_indexes WHERE table_id=57

对应的索引id为71,72

以上信息我们先记着,后面会用到

开始使用

进度mysql的data目录,看自己mysql的配置文件,数据存储在哪里。

[root@localhost data]# ls
auto.cnf    client-cert.pem  ibdata1      ibtmp1        performance_schema  public_key.pem   sys
ca-key.pem  client-key.pem   ib_logfile0  innodb_space  portal              server-cert.pem  xportal
ca.pem      ib_buffer_pool   ib_logfile1  mysql         private_key.pem     server-key.pem

概要信息

我们先看下我们这两张表的概要信息

[root@localhost data]# innodb_space -s ibdata1    system-spaces |grep t_user_info
name                            pages       indexes (grep 以后这行会丢失)
innodb_space/t_user_info        502528      2           
innodb_space/t_user_info1       502528      2    

列出了表占用的页数,以及索引个数,相对比较简单

表的索引信息

我们先看下对应表的索引信息

innodb_space -s ibdata1 -T innodb_space/t_user_info space-indexes

输出:

[root@localhost data]# innodb_space -s ibdata1 -T innodb_space/t_user_info space-indexes
id          name       root    fseg        fseg_id     used        allocated   fill_factor 
71          PRIMARY    3       internal    1           465         480         96.88%      
71          PRIMARY    3       leaf        2           406505      406560      99.99%      
72          unq_user   4       internal    3           177         224         79.02%      
72          unq_user   4       leaf        4           87720       87776       99.94%      
[root@localhost data]# innodb_space -s ibdata1 -T innodb_space/t_user_info1 space-indexes
id          name       root    fseg        fseg_id     used        allocated   fill_factor 
73          PRIMARY    3       internal    1           465         480         96.88%      
73          PRIMARY    3       leaf        2           406505      406560      99.99%      
74          unq_user   4       internal    3           177         224         79.02%      
74          unq_user   4       leaf        4           87720       87776       99.94%   

解释:

  • id:索引ID列
  • name 索引名称 PRIMARY 表示聚簇索引
  • root 索引根节点位于的数据页编号
  • fseg 索引类型(internal为内部节点,我们一般称为非叶子节点,leaf为叶子节点)
    • 索引存储在非叶子节点上

    • 数据存储在叶子节点上

  • fseg_id 索引所属段ID
    • 虽然两张表t_user_info和t_user_info1的段都一样,但是表示的都是对应表的.ibd文件里的,分属于不同的表空间
  • used 使用的page页数量
    • 通过表一(t_user_info)和表二(t_user_info1)的的对比,bigint类型创建表的时候是固定大小,并不会取你创建表的()中的数值
  • allocated 申请的数据页数量
    • 一共申请了495040个数据页,使用了494867个数据页
    • 从这里可以看出不是一个个的申请,是一次申请一批,预留一定的buffer,这里保证连贯性,从磁盘里读取的时候,不是只读对应的一页,而是相关的数据连着一片,这种方式对
    • 我们算下 select (480+406560+224+87776)* 16*1024/(1024*1024*1024) = 7.5537 GB 接近物理存储的空间,物理磁盘还会有些填充
  • fill_factor used/allocated 使用百分比

统计每个类型占用连续空间的页的数量

统计的是t_user_info.ibd文件里的信息

[root@localhost data]# innodb_space -s ibdata1 -T innodb_space/t_user_info space-page-type-regions
start       end         count       type                
0           0           1           FSP_HDR             
1           1           1           IBUF_BITMAP         
2           2           1           INODE               
3           16383       16381       INDEX               
16384       16384       1           XDES                
16385       16385       1           IBUF_BITMAP         
16386       16388       3           INDEX               
16389       16447       59          FREE (ALLOCATED)    
16448       32767       16320       INDEX                 
......
475200      491519      16320       INDEX               
491520      491520      1           XDES                
491521      491521      1           IBUF_BITMAP         
491522      491583      62          FREE (ALLOCATED)    
491584      496839      5256        INDEX               
496840      496895      56          FREE (ALLOCATED)    
496896      496904      9           INDEX               
496905      497151      247         FREE (ALLOCATED)  

解释:

  • Start 表示起始页的页码
  • end 表示同类型结束的页码
  • count 为当前类型page的连续数量

统计表空间中各页类型占比

innodb_space -s ibdata1 -T innodb_space/t_user_info space-page-type-summary

结果

[root@localhost data]# innodb_space -s ibdata1 -T innodb_space/t_user_info space-page-type-summary
type                count       percent     description         
INDEX               494867以上是关于InnoDB存储引擎存储结构详解-实战篇的主要内容,如果未能解决你的问题,请参考以下文章

MySQL——索引视图事务,存储引擎MyLSAM和InnoDB(实战篇!)

MySQL基础篇(05):逻辑架构图解和InnoDB存储引擎详解

MySQL进阶实战4,MySQL索引详解,下篇

Innodb存储引擎表空间详解

详解MySQL存储引擎Innodb

详解MySQL存储引擎Innodb