《Mysql技术内幕-InnoDB存储引擎》读书笔记 (一)

@(Mysql)

豆瓣链接

官方数据库
下载
导入 /data/mysql57/bin/mysql --socket /data/mysql3306/mysql.socket -uroot -ppassword1 < empoo.sql

虚拟机启动

  • 关闭iptables
  • 开启sshd
  • 修改root密码
  • mkdir /data
    mount /dev/sda1 /data

一、Mysql存储引擎

Mysql概念

  • 数据库。是文件的集合。这些文件保存数据库的数据。
  • 实例。是一个进程,用于管理数据库。

Mysql组成:

  • 连接池组件
  • 管理服务和工具组件
  • SQL接口组件
  • 查询分析器组件
  • 优化器组件
  • 缓存组件
  • 插件式存储引擎
  • 物理文件

存储引擎

  • InnoDB
    • 支持事务
    • 行锁设计
    • 支持外键
    • 支持MCVV,实现4种隔离级别
    • 表的行按照主键顺序存放
    • 最常用的数据库引擎。5.5后的默认引擎
  • MyISAM
    • 5.5前的默认
    • 不支持事务
    • 表锁设计
    • 只缓存索引文件,不缓存数据文件
    • 可以使用myisampack工具进一步压缩。但是压缩后只读。
  • NDB
    • 集群数据库
    • 数据放在内存
    • 高可用和并发
  • Memory
    • 数据放在内存
    • Mysql临时表会用这个引擎
    • 不支持TEXT和BLOB字段。所以如果临时表要用到这两个字段,会使用MyISAM引擎,而该引擎不缓存数据在内存,所以性能会有影响。
  • Archive
    • 只支持INSERT SELECT操作
    • 压缩后存放
    • 适用于高速插入和压缩功能。例如日志的存储
    • Federated
      • 不存放数据,指向远程Mysql的表
    • Maria
      • 用于替换MyISAM,
      • 支持缓存和索引
      • 行锁设计
      • MVCC功能

连接数据库的方式

  • TCP/IP
  • 命令管道或共享内存
  • UNIX 域套接字,就是--socket的参数。

二、InnoDB体系结构

体系结构由:

  • 后台线程
  • 存储引擎内存池
  • 文件
    组成

后台线程

  • Master Thread 。负责将缓冲池的数据异步刷新到磁盘,包括
    • 脏页的刷新
    • 合并插入缓存(INSERT BUGGER)
    • UNDO页的回收
  • IO Thread InnoDB大量使用异步IO(AIO)来处理请求,来提高数据库性能。
    • 有4类IO线程。
      • write
      • read
      • insert buffer
      • log
    • 除了write和read,其他线程都是只有一个,通过show variables like 'innodb_%io_threads'来查看write和read的线程数
    • 通过show engine innodb status来查看IO线程的情况
  • Purge Thread。事务被提交后,undolog就不需要了,PurgeThread用来回收已经分配的undo页
  • Page Cleaner Thread 1.2以后引入,用于回收脏页。

内存

缓冲池

Mysql会把数据存储在硬盘,为了性能,引入了缓冲池,也就是一块内存区域。
Mysql的数据都是按页来存储的。

  • 从硬盘读取页后,首先放到缓冲池。
  • 修改数据后,会修改缓冲池的数据,然后定期同步到硬盘
  • 通过show variables like 'innodb_buffer_pool_size'来查看缓冲池的大小,单位是字节。

缓冲池的数据类型有:

  • 索引页
  • 数据页
  • undo页
  • 插入缓冲页,insert buffer
  • 自适应哈希索引 adaptive hash index
  • 锁信息(lock info)
  • 数据字典信息(data dictionary)

Mysql支持多个缓冲池,通过show variables like 'innodb_buffer_pool_instances'来查看

LRU List 、Free List、FlushList
缓冲池是一个很大的内存区域,存储各种各样的页,页的默认大小是16KB。

缓冲池的数据页由下面3个列表组成,列表的item都是页。

  • LRU List 主要存储数据页
  • Free List 存储空闲的页
  • Flush List 存储脏页

LRU List
LRU List使用LRU(Lastest Recent Used)算法:

  • 使用最频繁的放在列表前端
  • 使用最少的放在末端
  • 当缓冲池不够的时候,优先释放末端的页
  • 新页进入缓冲池后,放在末端开始37%的位置,这个位置称为midpoint。通过show variables like 'innodb_old_blocks_pct'来查看。
    • midpoint前的称为new,是最活跃的数据
    • 后的称为old,是最不活跃的数据
    • 如果缓冲池已满,删除列表末端的页
    • 页从old升级为new,称为page made yound
    • 页没有从old升级为new(应该指一直在old中,直至被删除),称为page not made yound

Free List
数据库启动时,由于缓冲池是空的,这时页都存储在Free列表中(注意Free列表中的页都是没有数据的,或者数据已没有用)

当需要放新的一页到缓冲池:
* 首先查看Free列表是否有空闲的页
* 如果有,使用
* 如果没有。从LRU列表中删除末端页。

Flush List

当数据被修改后,会直接写重做日志和修改缓冲池数据,然后直接返回事务执行成功,这时候数据还没有落到硬盘的。(如果这时候数据库宕机,可以通过重做日志来恢复数据)。
所以就会存在一种状态,就是缓冲池的数据和硬盘的数据是不一致的。这时候缓冲池的这一页称为脏页(注意是缓冲池的数据比硬盘新)。
脏页就存放在Flush List中,脏页也会存储在LRU List中。
Mysql会定期把Flush List的脏页同步到硬盘,这个操作叫Checkpoint。

----------------------
BUFFER POOL AND MEMORY
----------------------
Total large memory allocated 137428992
Dictionary memory allocated 102398
Buffer pool size   8191
Free buffers       7891
Database pages     300
Old database pages 0
Modified db pages  0
Pending reads      0
Pending writes: LRU 0, flush list 0, single page 0
Pages made young 0, not young 0
0.00 youngs/s, 0.00 non-youngs/s
Pages read 266, created 34, written 36
0.00 reads/s, 0.00 creates/s, 0.00 writes/s
No buffer pool page gets since the last printout
Pages read ahead 0.00/s, evicted without access 0.00/s, Random read ahead 0.00/s
LRU len: 300, unzip_LRU len: 0
I/O sum[0]:cur[0], unzip sum[0]:cur[0]

通过show engine innodb status命令可以查看InnoDB的当前状态。

  • BUFFER POOL AND MEMORY 是缓冲池信息
  • Buffer pool size 8191 是缓冲池总大小,单位是页数量
  • Free buffers 7891 Free List的大小
  • Database pages LRU List的大小
  • Old database pages LRU List中old部分的大小
  • Modified db pages Flush List的大小
  • LRU len: 300, unzip_LRU len: 0 LRU List的大小和压缩数据列表的大小

Checkpoint

当事务提交时:

  • 写重做日志
  • 修改缓冲池的页数据
  • 定期执行checkpoint,把缓冲池的脏页刷新到硬盘

执行checkpoint的时机:

  • Sharp Checkpoint 在数据库关机的时候执行。
  • Fuzzy Checkpoint。数据库在运行时定期执行
    • Master Thread Checkpoint 由主线程执行,每1秒或者10秒执行一次
    • FLUSH_LRU_LIST Checkpoint
    • Async/Sync Flush Checkpoint
    • Dirty Page too much Checkpoint

Master Thread

Master Thread有多个循环:

  • 主循环
  • 后台循环
  • 刷新循环
  • 暂停循环

主循环包含两个循环:1秒和10秒(不一定准确是1秒或者10秒,有可能有延迟)

  • 1秒
    • 日志缓冲刷新到硬盘,即使事务还没有提交
    • 如果前1秒的系统IO小于5,合并插入缓存(Insert Buffer)
    • 如果缓冲池脏页比例大于innodb_max_dirty_pages_pct参数,刷线100个脏页到磁盘
    • 如果当前没有用户活动,切换到后台循环
  • 10秒
    • 如果前10秒的系统IO小于200,刷新100个脏页到磁盘
    • 合并最多5个插入缓冲
    • 将日志缓冲刷新到磁盘
    • 删除无用的Undo页
    • 如果脏页比例大于70%,刷新100个脏页到磁盘,否则10个

问题:

  • 插入缓冲是什么来的
  • 为什么还要刷新日志缓冲到磁盘,不是已经到磁盘了吗?
  • Undo页是什么来的

关键特性

  • 插入缓冲
  • 两次写
  • 自适应哈希索引
  • 异步IO
  • 刷新邻接页

插入缓冲
磁盘分为顺序IO和随机IO,例如要插入数据到磁盘的两个地方A和B

  • 顺序IO是指A和B是磁盘中连续的,或者间隔较少的两个位置。这样磁盘通过寻址找到A位置后,可以快速地找到B位置。这样插入AB和个位置的数据就会比较快,因为只需要1次寻址操作。
  • 随机IO是指A和B两个位置没有关系。当磁盘找到A位置后,需要再次通过寻址操作来寻址B。所以就会比较慢,因为需要2次寻址操作。

数据存储
Mysql的数据存储是根据主键来顺序存储的。所以在插入数据的时候:

  • 如果主键是顺序的,它们会存储在顺序的磁盘地址,这样就是顺序IO存储。例如主键是1和2
  • 如果主键是不存储的,Mysql需要存储在不同地方,这样就是随机IO。例如主键是abc和bcd

索引存储
Mysql中主键本身也是索引,称为主索引(Primary index),其他索引称为辅助索引(secondary index)
如果一个表中有辅助索引,在插入数据的时候,除了存储数据,还需要建立索引。
如果插入两条数据,主键是顺序的,但是有一个索引的数据是不顺序的,这样也会产生随机IO。Mysql的优化方法是加入插入缓冲:也就是先把索引放在缓冲池中,定期刷新到磁盘。这样可以减少随机IO的次数。例如插入两条数据,索引位置是相同的,如果分两次刷新到磁盘,就需要两次随机IO。如果合并为一次,只需要一次随机IO。
插入缓冲需要条件:

  • 索引是辅助索引
  • 索引不是唯一的。因为如果是唯一的,插入的时候Mysql需要一次随机IO找到索引位置,看数据有没有重复。这样这次随机IO就肯定需要的,所以使用插入缓冲来优化就没有意义了。

可以通过engine status命令来查看插入缓冲的状态

Ibuf: size 1, free list len 0, seg size 2, 0 merges
merged operations:
 insert 0, delete mark 0, delete 0
discarded operations:
 insert 0, delete mark 0, delete 0
  • seg size 是插入缓冲的大小
  • size 是已经合并的页数
  • free list 是空闲的页数
  • merged operations 被合并的操作

插入缓冲是一棵B+树。由非叶子节点和叶子节点组成:

  • 非叶子节点
    • space 存储spaceid是表的ID
    • marker 用于兼容旧版本
    • offset 页所在的偏移量
  • 叶子节点
    • sapce
    • marker
    • offset
    • metadata
    • 具体数据

这样存储的好处是把同一个表,相邻的索引(offset)放在一起。当需要刷新缓冲数据到磁盘的时候,可以把一个表的N个索引合并在一起,通过一次随机IO就能刷新好。提升刷新的效率。

合并插入缓冲(也就把缓冲刷到磁盘)时机:

  • 辅助索引页被读取到缓冲池时。当执行select操作,需要用到对应的索引时,Mysql会检查插入缓冲是否有该表的该索引缓冲,如果有,立刻刷新到磁盘。因为如果不刷,会影响查询的准确性。
  • Insert Buffer Bitmap 页追踪到索引页已没有可用空间时。缓冲没有空间的时候,立刻刷新。
  • Master Thread。主循环会定期刷新。

两次写
两次写特性用于保证数据的可靠性。
当Mysql刷新缓冲池的一个页,16KB到磁盘,如果写到4K的时候,宕机了,怎么办?
可以通过重做日志来恢复。但是重做日志只会记录在这一页的哪个位置写入内容,例如在偏移量800写入"aaaa"。如果这一页都已经损坏了,那重做日志也恢复不了。
Mysql的解决方法是通过两次写来解决

把缓冲池的一页写入到磁盘的步骤:

  • 把磁盘的数据例如Page1读入到内存
  • 把Page1保存到共享表空间
  • 写入缓冲池的数据到磁盘

其实就是在正式写入前,先把页的旧数据备份一次,当写失败的时候,恢复的时候,从备份还原旧数据,然后再进行新数据的写入。

mysql> show global status like 'innodb_dblwr%'
    -> ;
+----------------------------+-------+
| Variable_name              | Value |
+----------------------------+-------+
| Innodb_dblwr_pages_written | 42    |
| Innodb_dblwr_writes        | 6     |
+----------------------------+-------+
  • Innodb_dblwr_pages_written 是双写写了多少个页
  • Innodb_dblwr_writes 是实际写了多少次
    一般这两个数据是8:1(不知道为什么?是不是因为备份8个页才会正式写入一次到磁盘?)

自适应哈希索引
又称为AHI。
这个是Mysql通过观察查询情况,对应热点数据进行自动建立索引。
建立的是hash索引,存放在缓冲池,复杂度是O(1),所以速度是非常快的。

例如我们查询select * from table where name='a'

当这个查询进行了超过100次,Mysql就会建立AHI

通过engine status可以查看当前的哈希索引状态

0.00 hash searches/s, 0.00 non-hash searches/s
  • hash searches/s是通过哈希索引查询数,每秒
  • non-hash 是没有通过哈希索引的查询数,每秒

异步IO(AIO)
异步IO对应的是同步IO。
异步IO的优点是:

  • 减少IO的等待时间,例如3次IO,只需要等1个IO时间
  • 合并IO操作,减少随机IO

例如我们要查询3个页,(space,page_no)分别是(8,6),(8,7),(10,9)

  • 如果使用同步IO,我们需要发送3次IO请求,然后等待3次IO时间,明显这是比较耗时的。
  • 如果使用异步IO,我们一次性发送3个IO请求,然后等待IO结果。AIO发现前两个IO是连续的,所以可以合并为从8,7开始取16*2KB数据,把3个IO合并为2个,然后只需要1次IO操作时间。

刷新邻近页
在刷新脏页的时候,把相邻的脏页一起刷新。相邻是指两个页在磁盘中属于同一个区。底层就是减少随机IO的次数。

启动关闭和恢复

  • innodb_fash_shutdown

    • 0 数据库关闭时只需full purge和merge insert buffser操作
    • 1 不进行上面的操作,但是会刷新缓冲池的脏页回磁盘
    • 2 不进行上面的操作,下次启动时只需recovery
  • innodb_force_recovery

    • 0 只需所有的恢复操作
    • 1-6 进行部分的恢复操作

三、文件

Mysql的文件分为:

  • 参数文件,例如my.cnf
  • 日志文件,包括各种日志,例如:
    • 错误日志
    • 二进制日志
    • 慢查询日志
    • 查询日志
  • socket文件,用于通过UNIX域套接字方式连接
  • pid文件,用于记录Mysql的进程ID
  • 表结构文件,存储表的结构
  • 存储引擎文件,存储表的。

1. 参数文件

是Mysql启动时的配置文件,Mysql寻找路径是:
[root@livedvd ~]# /data/mysql57/bin/mysql --help |grep my.cnf
/etc/my.cnf /etc/mysql/my.cnf /usr/local/mysql/etc/my.cnf ~/.my.cnf

也可以在执行mysqld的时候,加参数--defaults-file=/data/mysql3306/my.cnf来指定这个文件。

配置文件使用键值对的形式进行存储,例如innodb_buffer_pool_size=1G。如果在配置文件找不到对应的配置,Mysql会使用默认的配置。

参数类型

  • 动态参数。可以在Mysql运行期间进行修改。
  • 静态参数。不可以在Mysql运行期间进行修改。

查看参数的方式:

  • show [global] variables like 'innodb_%'
  • 查找infomation_schema库下面的GLOBAL_VARIABLES表
    修改参数:
  • SET [global | session] name=value 例如:
    • SET session read_buffer_size=2000000
  • SET [@@global. | @@session. | @@] name=value 例如
    • SET @@session.read_buffer_size=2000000

参数分为全局和当前会话。修改参数后,并不会修改配置文件,所以下次启动,还是会是旧的参数。

2.日志文件

错误日志

记录Mysql的错误信息和警告信息。

通过show variables like 'log_error' 来定位日志文件

慢查询日志

Mysql会把超过一定阈值的Sql记录到慢查询日志。

相关的参数有:

  • long_query_time 表示执行时间超过多少秒(注意是大于,不是大于等于),就记录到慢查询日志。
  • log_slow_queries 表示是否记录慢查询日志。设置为ON,才会记录慢查询日志。5.7以后是slow_query_log
  • log_queries_not_using_indexes 是否记录没有使用索引的语句。设置为ON后,没有使用索引的语句也会被记录
  • log_throttle_queries_not_using_indexes 每分钟记录没有使用索引的语句条数。这个参数避免打开log_queries_not_using_indexes 后导致慢查询日志过多
  • log_output 日志输出方式。默认是FILE,可以设置为TABLE,日志会输出到mysql库的slow_log表。该表是CSV引擎。
  • long_query_io 超过多少逻辑IO的语句会记录
  • slow_query_type 日志记录方式
    • 0 不记录
    • 1根据运行时间记录
    • 根据逻辑IO次数记录
    • 根据运行时间和逻辑IO记录
  • slow_query_log_file 慢查询日志文件的位置

查询分为逻辑IO和物理IO

提取慢查询日志

[root@livedvd ~]# /data/mysql57/bin/mysqldumpslow /data/mysql3306/data/slow.log -s t -r -n 10        

Reading mysql slow query log from /data/mysql3306/data/slow.log
Count: 8  Time=0.07s (0s)  Lock=0.00s (0s)  Rows=1.0 (8), root[root]@localhost
  select count(*) from employees where first_name='S'

[root@livedvd ~]# 
  • mysqldumpslow 通过这个命令
  • -s表示排序 t表示按query time排序
  • -r表示倒序
  • -n 表示返回10条语句
  • 通过--help查看更多用法,
  • 也可以使用pt-query-digest工具

查询日志

会记录所有的查询信息。
不知道怎么打开,具体文件在哪里

二进制日志

也就是binary log 也叫binlog。
记录所有对数据库的修改信息,用于:

  • 恢复数据库状态到某个时间点(point-in-time),需要配合冷备。
  • 复制。主从复制
  • 审计,查看是否有SQL注入
打开binlog日志

修改配置文件,加入配置

[mysqld]
log-bin=binlog
binlog_format=mixed # 日志格式
server-id=1 #节点的ID,可以设置为主库是1,从库是2,不能重复。

重启mysql, show variables like 'log%';查看到log_bin=ON
表示打开了binlog日志

相关参数

binlog相关的参数有:

  • max_binlog_size 单个binlog日志的最大字节,大于这个字节就会写入到新的文件。
  • binlog_cache_szie binlog缓冲区大小。当事务没有提交时,Mysql会把修改的内容写入到缓冲区,等commit的时候,写入到binlog日志。当缓冲区满了的话,会写入到临时文件
    • binlog_cache_use binlog缓冲区使用次数
    • binlog_cache_disk_use 临时文件使用次数
  • sync_binlog 。binlog的写入,也会有缓冲区,如果设置这个参数=1,就不会缓冲。这样可用性会较高,但是性能会较差
  • binlog-do-db 表示那些库写入到binlog
  • binlog-ignore-db 表示哪些库不写入binlog
  • log-slave-update 是否将从主复制来的修改,写入到自己的binlog。默认不写,如果要配置master-slave-slave 就需要写
  • binlog_format binlog的格式,可以选:
    • statement 记录执行的sql到binlog。优点是省空间,缺点是对于一些随机语句,可能会导致主从不一致
    • row 记录修改的行到binlog 。缺点是费空间
    • mixed 上面两个混合。Mysql智能选择格式
binlog日志查看
mysql> show master status;
+---------------+----------+--------------+------------------+-------------------+
| File          | Position | Binlog_Do_DB | Binlog_Ignore_DB | Executed_Gtid_Set |
+---------------+----------+--------------+------------------+-------------------+
| binlog.000002 |      490 |              |                  |                   |
+---------------+----------+--------------+------------------+-------------------+
1 row in set (0.00 sec)

通过show master status;查看当前的binlog日志的文件名和大小(Position)等信息

binlog的文件在datadir参数的目录里面。

mysql> show binlog events in 'binlog.000002';
+---------------+-----+----------------+-----------+-------------+---------------------------------------------------------------------+
| Log_name      | Pos | Event_type     | Server_id | End_log_pos | Info                                                                |
+---------------+-----+----------------+-----------+-------------+---------------------------------------------------------------------+

| binlog.000002 | 490 | Anonymous_Gtid |         1 |         555 | SET @@SESSION.GTID_NEXT= 'ANONYMOUS'                                |
| binlog.000002 | 555 | Query          |         1 |         634 | BEGIN                                                               |
| binlog.000002 | 634 | Query          |         1 |         758 | use `employees`; update employees set first_name='kevinlu2' limit 1 |
| binlog.000002 | 758 | Xid            |         1 |         789 | COMMIT /* xid=24 */                                                 |
+---------------+-----+----------------+-----------+-------------+---------------------------------------------------------------------+
11 rows in set (0.00 sec)

mysql> 

通过命令show binlog events in 'binlog.000002'可以看到binlog的内容,如果是statement格式,可以看到执行的sql语句。

/data/mysql57/bin/mysqlbinlog -vv --start-position=0 binlog.000002

通过mysqlbinlog命令也可以查看binlog的信息。其中

  • -vv表示转换row格式。如果不转换,返回二进制信息,看不懂。

可以看到转换后,row格式的内容是这样的:

BINLOG '
2gr1XRMBAAAARAAAAG4BAAAAAHEAAAAAAAMACWVtcGxveWVlcwAJZW1wbG95ZWVzAAYDCg8P/goG
DgAQAPcBADPIzgc=
2gr1XR8BAAAAXQAAAMsBAAAAAHEAAAAAAAEAAgAG///AEScAACJDDwdrZXZpbmx1B0ZhY2VsbG8B
2oQPwBEnAAAiQw8Ia2V2aW5sdTEHRmFjZWxsbwHahA8LS+ze
'/*!*/;
### UPDATE `employees`.`employees`
### WHERE
###   @1=10001 /* INT meta=0 nullable=0 is_null=0 */
###   @2='1953:09:02' /* DATE meta=0 nullable=0 is_null=0 */
###   @3='kevinlu' /* VARSTRING(14) meta=14 nullable=0 is_null=0 */
###   @4='Facello' /* VARSTRING(16) meta=16 nullable=0 is_null=0 */
###   @5=1 /* ENUM(1 byte) meta=63233 nullable=0 is_null=0 */
###   @6='1986:06:26' /* DATE meta=0 nullable=0 is_null=0 */
### SET
###   @1=10001 /* INT meta=0 nullable=0 is_null=0 */
###   @2='1953:09:02' /* DATE meta=0 nullable=0 is_null=0 */
###   @3='kevinlu1' /* VARSTRING(14) meta=14 nullable=0 is_null=0 */
###   @4='Facello' /* VARSTRING(16) meta=16 nullable=0 is_null=0 */
###   @5=1 /* ENUM(1 byte) meta=63233 nullable=0 is_null=0 */
###   @6='1986:06:26' /* DATE meta=0 nullable=0 is_null=0 */

执行的sql是use employees; update employees set first_name='kevinlu1' limit 1,可以看到

  • 即使只修改一个字段,但是row格式会把整行的所有字段都写进binglog。
  • 从机复制的时候,应该根据表的主键来匹配到对应的行,然后使用binlog的数据覆盖整行数据

3. 套接字文件

mysql> show variables like 'socket';
+---------------+------------------------------+
| Variable_name | Value                        |
+---------------+------------------------------+
| socket        | /data/mysql3306/mysql.socket |
+---------------+------------------------------+

mysqld启动会,会生成一个socket文件,mysql连接本机的mysql的话,可以直接指定socket文件,这样就不用输入端口和host了。

连接方式:

/data/mysql57/bin/mysql --socket /data/mysql3306/mysql.socket -uroot -ppassword1

4. pid文件

参数名是pid_file

5. 表结构定义文件

由于Mysql每个表都有自己的引擎类型,所以Mysql的存储是以表为单位的。
每个表都会有一个xx.frm文件来描述表的定义。
frm文件在datadir里面的库文件夹里面。
书上说是文本文件,但是测试发现5.7的mysql是二进制文件。

6.InnoDB存储引擎文件

表空间文件

表空间文件用于存储表的数据,索引,插入缓冲等信息。
例如test库的table1表的表空间文件就是/data/mysql3306/data/test/table1.ibd

相关参数

  • innodb_data_file_path 定义表空间文件的路径,可以指定多个文件,这样Mysql会分开存储表的数据,如果路径位于不同的磁盘,可以提升性能。
  • innodb_file_per_table 是否分开存储表空间文件。如果等于ON,每个表一个ibd文件,否则整个库的数据都存储在一起,idbdata1.ibd。

重做日志文件

如果每次事务提交,Mysql都修改具体的数据文件,性能会比较差。优化方法是先把修改点记录到重做日志文件。然后定期把重做日志的内容更新到具体的数据文件。这样做的另一个好处是,当事务提交后,由于已写入到重做日志,所以Mysql宕机后的重启,可以通过更新重做日志的内容来实现事务不会丢失。
重做日志叫redo log file。位于data目录下面的ib_logfile0和ib_logfile1。这两个文件会循环写入,例如先写0,写满后写1,写满后再写0,不断循环。

  • innodb_log_file_size单个日志文件的最大大小
  • innodb_log_files_in_group 日志文件的数据,默认是2,也就是0和1
  • innodb_mirrored_log_groups 日志镜像文件组的数量,默认是1. 为了可用性,可以加多个重做日志的镜像。
  • innodb_log_group_home_dir 日志文件的目录,默认是data目录

重做日志的写入也有个缓冲区。缓冲区写入磁盘是按512字节写入的,所以一定会成功(为什么?)

所以事务的流程
在事务的执行过程中,不断有信息写入到重做日志,
在事务提交时,先写二进制文件,写入成功后,返回提交成功

四、表

1. 索引组织表

InnoDB中,数据的存储是根据主键来的,也就是主键相邻的数据存储在一起。

定义表时,

  • 如果有主键
  • 如果没有主键
    • 选择一个唯一的非空的字段作为索引
    • 如果没有,自动创建一个6字节的指针

所以在InnoDB看来,每个表都有主键。

2.InnoDB逻辑存储结构

所有的数据都存储在idb文件中,这个称为表空间(tablespace)
表空间由段(segment),区(extent),页(page)组成。
表空间由3个段组成:

  • 叶子节点段
  • 非叶子节点段
  • 回滚段
    每个段由多个区组成
    每个区由多个页组成
    一个页存放一个或多个行
    页有时也称为block

分为叶子段,非叶子段,回滚段

任何情况下,一个区的大小是1M
为了保证数据的连续性,Mysql一次会申请4-5个区的空间。
一般一个页是16K,也就是一个区由64个页
创建表示,Mysql为了节省空间,只会申请32个页的空间,称为碎片页(fragment page)。后面才会一次申请64个页。

可以通过py_innodb_page_info.py工具来查看ibd文件的页组成

[root@livedvd test]# py_innodb_page_info.py -v  t1.ibd 
page offset 00000000, page type <File Space Header>
page offset 00000001, page type <Insert Buffer Bitmap>
page offset 00000002, page type <File Segment inode>
page offset 00000003, page type <B-tree Node>, page level <0000>
page offset 00000000, page type <Freshly Allocated Page>
page offset 00000000, page type <Freshly Allocated Page>
Total number of page: 6:
Freshly Allocated Page: 2
Insert Buffer Bitmap: 1
File Space Header: 1
B-tree Node: 1
File Segment inode: 1
  • 每一个页由自己的偏移量,也可以称为页的ID
  • 每个页都有类型,
  • 这里一共6个页,其中没有使用的2个,已使用的4个,
  • Btree节点页1个,level0表示这是叶子节点,如果等于1表示非叶子节点
  • Freshly Allocated Page表示还没有使用的页,

上面的是新建表后,插入2行7000长度varchar的行后的数据,一个字符的长度是1字节,所以7000就是7000字节,两行就是差不多16k。所以一个页就能把这2行数据存储,所以上面只有一个btree的页
如果再插入一行,就需要2个页了,因为大于1个节点页,就需要一个非叶子节点,

page offset 00000003, page type <B-tree Node>, page level <0001>
page offset 00000004, page type <B-tree Node>, page level <0000>
page offset 00000005, page type <B-tree Node>, page level <0000>

可以看到,有一个非叶子节点,2个叶子节点。

mysql> DELIMITER //
mysql> create procedure load_t1(count int unsigned) begin declare s int unsigned default 1;
    -> declare c varchar(7000) default repeat('a',7000);
    -> while s<= count DO
    -> insert into t1 select NULL,c;
    -> SET s=s+1
    -> ;
    -> END while;
    -> end;
    -> //

DELIMITER //表示修改结束符,从;修改为//

如果继续插入数据,插入64行后,就需要32个页,这时碎片页就用完了,如果继续插入,Mysql就会一次申请64个页,也就是一个区,1M。

默认一页是16K,可以修改innodb_page_size为4K,8K,16K。

页类型有:

  • 数据页(B-tree Node)
  • undo页(undo Log Page)
  • 系统页(System Page)
  • 事务数据页(Transaction system Page)
  • 插入缓冲位图页(Insert Buffer BItmap)
  • 插入缓冲空闲列表页(Insert BUffer Free List)
  • 未压缩的二进制大对象页(Uncompresssed BLOB Page)
  • 压缩的二进制大对象页(compressed BLOB Page)

InnoDB的存储是面向行的,也就是以行来组织存储的

3. InnoDb行记录格式

有4种格式:

  • Compact
  • Redundant主要用于兼容旧版本
  • Dynamic 和Compact一样,不同点,书上说使用完全行溢出。(5.7的mysql实验发现并不是)
  • Compressed 和Dynamic一样,不同点是对可变长度的数据进行zlib压缩,包括BLOG,TEXT,VARCHAR

通过show table status like 'employees'命令可以查看表的Row_format

Compact格式

一行数据的组成

  • 变长字段长度列表。例如有3个可变长度的字段,长度分别为1,2,3,这里会存储03 02 01(逆序存放,3个字段就存放3字节)
    • 如果列长度小于255,使用1字节表示
    • 如果长度小于65535 ,使用2字节表示。
    • 不允许有长度大于65535 的可变列,因为规定varchar的最大长度是65535
  • NULL标志位。用二进制表示,1表示第N页是NULL
  • 记录头信息,5字节
    • 1bit 未知
    • 1bit 未知
    • 1bit deleted_flag 该行是否删除
    • 1bit min_rec_flag
    • 4bit n_owned 该记录所在的槽拥有的记录数
    • 13bit heap_no 索引堆中该记录的排序记录
    • 3bit record_type 记录类型 000表示普通 001 表示B+书节点指针,010 鄙视Infimum,011表示Supremum 1xx表示保留
    • 16bit next_record 下一条记录的相对位置
  • 列数据
    • 有两个固定列:
      • 事务ID列 6字节
      • 回滚指针列 7字节
    • 如果没有主键ID,会自动加一列rowid 6字节
    • 后面就是用户自己的数据了

变长字段只会一定长度的数据,超出的部分会存储到Uncompresssed BLOB Page,并在未超出的数据后记录超出的部分存储在哪个页的哪个偏移量。这个称为行溢出存储。

为什么超出的部分需要存储在其他地方?
因为存储是B+树形式的,如果一个页只有一行数据,那边B+树就没有意义了,就变成链表了。所以一页必须存储大于1行数据。

varchar类型,varchar(N)的N表示最大字符长度,官方说的varchar最大存储是65535字节,长度和字节是不完全一样的。

可以通过命令hexdump -C -v t3.ibd > t3.hex查看具体的ibd文件。

000101a0  61 61 61 61 61 61 61 61  61 61 61 61 61 61 61 61  |aaaaaaaaaaaaaaaa|

61表示一个字节。一行16字节。

char类型。InnoDB中,char类型存储的不一定是定长的数据。

  • 如果表的字符集是latin1,char存储的是定长的数据,如果不够长度,使用ox20来填充。
  • 如果字符集是其他,例如GBK,UTF8,char相当于varchar

4.InnoDB数据页结构

页是Mysql最小的磁盘管理单位。

页的组成:

  • File Header 文件头 38字节
  • Page Header 页头 56字节
  • Infimun 和Supremum Records
  • User Records 用户记录
  • Free Space 空闲空间
  • Page Directory 页目录
  • File Trailer 文件结尾信息 8字节

File Header

  • FIL_PAGESPACE_OR_CHKSUM checksum值 4字节
  • FIL_PAGE_OFFSET 4字节页在表空间中的偏移量,也就是第几个页
  • FIL_PAGE_PREV 前一个数据页的偏移量
  • FIL_PAGE_NEXT 下一个页的偏移量
  • FIL_PAGE_LSN 8字节 LSN(log sequence number)
  • FIL_PAGE_TYPE 页类型 2字节
    • 45BF B+树叶子节点
  • FIL_PAGE_FILE_FLUSH_LSN 8字节LSN值
  • FIL_PAGE_ARCH_LOG_NO_OR_SPACE_ID 4字节表示该页属于哪个表空间
  • PAGE_N_DIR_SLOTS 2字节 在Page Directory 页目录中的Slot(槽)数
  • PAGE_HEAP_TOP 2字节 空闲空间堆中的最小位置
  • PAGE_N_HEAP 2字节 堆中的记录数,也就是该页中的记录数
  • PAGE_FREE 2字节 可重用空间的首指针
  • PAGE_GARBAGE 2字节 已删除的记录数量
  • PAGE_LAST_INSERT 2字节 最后插入记录的位置
  • PAGE_DIRECTION 2字节最后插入的方向
  • PAGE_N_DIRECTION 2字节 一个方向连续插入记录的数量
  • PAGE_N_RECS 2字节 页中记录数量
  • PAGE_MAX_TRX_ID 8字节 修改当前页的最大事务ID,
  • PAGE_LEVEL 2字节 页在索引树中的位置,00表示叶节点
  • PAGE_INDEX_ID 8字节 索引ID
  • PAGE_BTR_SEG_LEAF 10字节
  • PAGE_BTR_SEG_TOP 10字节

Infinum和Supremum Record

每一页中都有2行伪记录,表示当前页最小值和最大值

User Record 和 Free Space

真正的数据,存储方式看上面的行存储描述。

Page Directory

B+Tree只能定位到具体的页,定位后Mysql会把整个页加载到内存。然后通过页的Page Directory找到具体的行。这个寻找方式的复杂度是二分查找。实现方式是通过Page Directory。由于这个操作是二分查找,而且在内存中,所以速度很快。

一个页里面会有很多个槽,一个槽有多行记录,槽中的记录的n_owned值记录该槽拥有的记录数,当插入和删除操作后,Mysql需要对槽进行分裂或者平衡操作。

  • 伪记录 Infimum 的 n_owned 的值总是1
  • Supremum的n_owned的值是[1,8]
  • 用户记录的n_owned的值是[4,8]

假如有主键为 1,2,3,4,5,6,每个槽有4行记录。则槽的形式可能是:

  • 1234 存放在一个槽1,槽1指向的记录是1
  • 56存放在一个槽2,槽2指向的是记录5

File Trailer

这个用于和File Header的checksum检查,检测是否一页数据完整,(已完整写入到磁盘)。
8字节,前4字节等于Header的checksum后4字节等于LSN。

测试

创建一个新表,并插入3行数据

mysql>  create table t5(id int unsigned not null auto_increment,b char(10),primary key (a) );
mysql> insert into t5 set b='aaaaaaaaaa';                                                           
mysql> insert into t5 set b='bbbbbbbbbb';
mysql> insert into t5 set b='cccccccccc';

使用py_innodb_page_info工具,查看到数据页位于第4个页,使用hexdump -C -v t5.ibd > t5.hex 命令,查看表空间文件。第4个页位于1610243的位置,16进制是 c000。
内容如下:

0000c000  36 a9 8f ef 00 00 00 03  ff ff ff ff ff ff ff ff  |6...............|
0000c010  00 00 00 00 06 cd b7 6a  45 bf 00 00 00 00 00 00  |.......jE.......|
0000c020  00 00 00 00 00 2e 00 02  00 db 80 05 00 00 00 00  |................|
0000c030  00 c0 00 02 00 02 00 03  00 00 00 00 00 00 00 00  |................|
0000c040  00 00 00 00 00 00 00 00  00 42 00 00 00 2e 00 00  |.........B......|
0000c050  00 02 00 f2 00 00 00 2e  00 00 00 02 00 32 01 00  |.............2..|
0000c060  02 00 1b 69 6e 66 69 6d  75 6d 00 04 00 0b 00 00  |...infimum......|
0000c070  73 75 70 72 65 6d 75 6d  00 00 00 10 00 21 00 00  |supremum.....!..|
0000c080  00 01 00 00 00 00 0f 80  f4 00 00 02 03 01 10 61  |...............a|
0000c090  61 61 61 61 61 61 61 61  61 00 00 00 18 00 21 00  |aaaaaaaaa.....!.|
0000c0a0  00 00 02 00 00 00 00 0f  81 f5 00 00 02 04 01 10  |................|
0000c0b0  62 62 62 62 62 62 62 62  62 62 00 00 00 20 ff b0  |bbbbbbbbbb... ..|
0000c0c0  00 00 00 03 00 00 00 00  0f 86 f8 00 00 02 07 01  |................|
0000c0d0  10 63 63 63 63 63 63 63  63 63 63 00 00 00 00 00  |.cccccccccc.....|
0000c0e0  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
0000c0f0  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|

后面的都是00来了,因为空间未被使用,找到页的末尾,内容如下:

0000fff0  00 00 00 00 00 70 00 63  b7 5c cf 11 06 cd 89 e1  |.....p.c.\......|

数据解析(下面的数字都是十六进制):

  • FIL_PAGESPACE_OR_CHKSUM = b7 5c cf 11
  • FIL_PAGE_OFFSET = 00 00 00 03 表示这一页在表空间的偏移位是3
  • FIL_PAGE_PREV = ff ff ff ff
  • FIL_PAGE_NEXT =ff ff ff ff 由于只有一个数据页,前后页的值都是空
  • FIL_PAGE_LSN = 00 00 00 00 06 cd 89 e1
  • FIL_PAGE_TYPE = 45 bf 表示这是个叶子页
    • 45BF B+树叶子节点
  • FIL_PAGE_FILE_FLUSH_LSN 00 00 00 00 00 00 00 00
  • FIL_PAGE_ARCH_LOG_NO_OR_SPACE_ID = 00 00 00 2d
  • PAGE_N_DIR_SLOTS = 00 02 2个槽
  • PAGE_HEAP_TOP = 00 db 空闲空间的位置,这一页的位置是c000 加上 00db就是 c0db,可以看到这个位置的数据都是0
  • PAGE_N_HEAP = 80 05 因为是Compact格式,初始值是0x8002,所以 页中的记录数=8005-8002=3
  • PAGE_FREE =00 00 由于没有删除记录,所以是0
  • PAGE_GARBAGE =00 00 由于没有删除记录,所以是0
  • PAGE_LAST_INSERT = 00 c0 最后插入记录的位置为c000+00c0 = c0c0
  • PAGE_DIRECTION =00 02
  • PAGE_N_DIRECTION =0 02
  • PAGE_N_RECS = 00 03 页中记录数量为3
  • PAGE_MAX_TRX_ID = 00 00 00 00 00 00 00 00
  • PAGE_LEVEL =00 00 叶子节点
  • PAGE_INDEX_ID = 00 00 00 00 00 00 00 41

上面是File Header 和Page Header
下面是两个伪记录

infimum:
01 00 02 00 1b 69 6e 66 69 6d 75 6d 00
supremum
04 00 0b 00 00 73 75 70 72 65 6d 75 6d

签名是record header 5字节,伪记录只有一列 char(8)内容是supremum和infimum

从infimum的record的后两字节00 1b,可以计算第一行的位置为c063+001b=c07e。c063是infimum record header之后第一字节的位置(也就是69的位置)。c07e是第一行的内容开始的位置,而不是可变长度数据的位置

所以第一行的数据为

00 00 00 10 00 21 00 00
00 01 00 00 00 00 0f 80 f4 00 00 02 03 01 10 61
61 61 61 61 61 61 61 61 61

c07e是加粗字节的位置,前面5字节是record header,再前面1字节是NULL,没有可变长度。
00 00 00 01是rowid,由于有组件所以这里是a字段的值,a字段是int类型,占4字节。
00 00 00 00 0f 80 是事务ID
f4 00 00 02 03 01 10 是回滚点
61 61 61 61 61 61 61 61 61 61是除主键外第一列的数据,是b字段,char10,10字节,全都是A,61的acii码对应就是a

同样的方法,计算第一行的开始位置,等于c07e+0021=C09F

61 61 61 61 61 61 61 61 61 00 00 00 18 00 21 00
00 00 02 00 00 00 00 0f 81 f5 00 00 02 04 01 10
62 62 62 62 62 62 62 62 62 62

C09F是上面加粗的位置,同理向前6字节是null和record header

所以使用这个方法,就能找到页中所有行。
通过Page Header的next和pre,找到前后的页。这样就能找到表中所有的数据。

Page Directory

槽是逆序存放的,一个槽占2字节。File Trailer往前推就是Page DIrectory。
也就是 00 70 00 63
这里有两个槽,

  • 第一个槽00 63,对应的是0063+c000的位置,就是指向Infimum行的内容(不是header)
  • 第二个槽00 70,对应的是C070,也就是supermum的行的内容,然后找到supermum的头04 00 0b 00 00,其中n_owned在第一个字节的后面4bit,转换04为二进制后是00000100,所以n_owned=0100,十进制就是4。所以n_owned=4。表示这个槽有4行记录,包括super行和其他3行用户行。(其实直接取04的4也可以)

5 Named File Formats机制

现在InnoDB有4中文件格式,后面也会有更多

6 约束

数据完整性

InnoDB提供的约束有:

  • Primary Key 该字段的值需要唯一
  • Unique Key 该字段的值需要唯一
  • Foreign Key 该字段的值需要in另一个表的一个字段的所有取值
  • Default 字段设置默认值
  • NOT NULL 字段不能为NULL

错误值约束

如果输入的值,和字段的数据类型不符合。例如字段是int类型,输入了'a',这就是错误值。
对于错误值,InnoDB有两种做法:

  • 当sql_mode没有STRICT_TRANS_TABLES,修改值为0,并报Warning
  • 当sql_mode有STRICT_TRANS_TABLES,报Error,拒绝写入

通过show variables like 'sql_mode'来查看

触发器约束

可以为一个表设置6个触发器,分别为INSERT UPDATE DELETE 的BEFORE 和AFTER。表示插入,更新,删除的前后。

mysql> create table cash (user_id int NOT NULL,cash INT NOT NULL);
Query OK, 0 rows affected (0.02 sec)

mysql> create table err_log(user_id int ,old_cash int,new_cash int,time datetime);
Query OK, 0 rows affected (0.02 sec)

mysql> delimiter $$

mysql> create trigger trg_usercash_update before update on cash 
    -> for each row
    -> begin
    -> if new.cash-old.cash > 0 then
    -> insert into error_log
    -> select old.user_id,old.cash,new.cash,NOW();
    -> set new.cash=old.cash;
    -> end if;
    -> end;
    -> $$

创建一个触发器,当cash表更新的时候,如果更新后cash字段大于更新前,拒绝更新,并写一条日志

外键约束

在批量修改数据时,外键约束会导致大量的额外开销,因为需要查询对应的约束是否满足。可以通过set foreign_key_checks=0来取消检查

7.视图

视图是一个虚表,和持久表不同的时候,视图中的数据没有实际的物理存储。

8.分区

Mysql支持的分区类型

  • RANGE 分区。例如小于10的放在分区1,小于20的放在分区2
  • LIST 分区,主键在1,2,5,6的放在分区1,主键在4,7的放在分区2
  • HASH 分区,按主键,计算hash值(怎么计算,由用户提供,例如取模),放在指定的分区,这样可以均匀存放
  • KEY分区,和HASH分区类型,只是计算hash值使用Mysql指定的还是,和password还是一样
  • COLUMNS分区,5.5后支持。上面的分区方法都是为每行计算一个int值,然后决定放在哪个分区。COLUMNS分区支持其他数据类型,例如float,datetime等。

子分区,可以在分区的基础上再分区,又叫复合分区。

分区和性能

分区后的好处

  • 分区后,不同分区存放在不同的文件。所以如果需要把一张表存放在不同的硬盘,就可以使用分区
  • 对于分区的键进行查询,速度会有一定提升。
    • 例如1kw的数据,需要根据key1来查询,
      • 如果使用索引,1kw数据可以能需要3层B+tree索引,就需要3次磁盘IO
      • 如果使用key1分区,假如有10个分区,那么可能只需要2层索引,这样可以减少一次IO
      • 其实对查询速度提升是有的,但是很有限。(可能使用索引也是2层,那就没有优化了)
  • 删除单个分区非常快。因为是按分区存放文件的,所以一个分区,就相当于删除一个ibd文件,速度非常快

分区的坏处

  • 分区后。表中的索引在每个分区单独管理。
    • 假如表t 按key1分区10个,同时有索引idx_1(key2),如果查询时使用idx1索引,就需要到每个分区执行索引查询,这时候磁盘IO可能是2*10次。远大于不分区时2-3次磁盘IO
    • 分区查询的性能,可以通过explain命令来查看,有一个partitions列,展示本次查询检索了哪几个分区

总的来说,使用分区要谨慎。

  • 分区更适用于OLAP(在线分析处理),这时候一般需要查询大量数据
  • 不适用于OLTP(在线事务处理),这时候一般都是使用索引查询几条数据。

3
创建表create table t7 (a int ) partition by hash(a) partitions 4;,然后有4个ibd文件

t7.frm
t7#P#p0.ibd
t7#P#p1.ibd
t7#P#p2.ibd
t7#P#p3.ibd
posted @ 2019-12-23 17:48  Xjng  阅读(831)  评论(0编辑  收藏  举报