innodb

参考链接: https://draveness.me/mysql-innodb/ https://mp.weixin.qq.com/s?__biz=MzU5NDk0MTc2OA==&mid=2247483937&idx=1&sn=46ecf87cc91a1793bc930da3be367c32&scene=21#wechat_redirect

概念

数据库与实例的区别:

  • 数据库:物理操作系统文件系统或其他形式文件类型的集合
  • 实例:后台线程以及一个共享内存区 mysql一般是数据库和实例一一对应的关系。

mysql进程: 启动一个实例UNIX一般会启动2个进程:

  • mysqld :真正的数据库服务守护进程
  • mysqld_safe :用于检查和设置 mysqld 启动的控制程序,它负责监控 MySQL 进程的执行,当 mysqld 发生错误时,mysqld_safe 会对其状态进行检查并在合适的条件下重启

InnoDB存储

数据存放(磁盘里的):表空间(tablespace)->段(segment)->区(extent)->页(page)->行(row) 页大小默认16KB(可通过innodb_page_size参数调整)

每个 16KB 大小的页中可以存放 2-200 行的记录。

存储表时,表定义(.frm)和索引、数据等信息(.ibd)是分开存储等。

.frm

创建一张表时,就会在datadir目录下新建一个tablename.frm文件,包含表结构信息

.ibd

数据存储有两种方式,由参数innodb_file_per_table参数控制,默认关闭。

当此参数关闭时,所有数据库的表、索引、以及系统信息都存储在系统表空间中(格式:ibdata1, ibdata2),是共享的表空间。

当此参数关闭时,每张表都单独的表空间(格式ibd),存储该表的数据和索引。但是系统信息依旧存放在ibdata

注意:这里说的表索引的存放是指索引的B+树结构,而不是索引定义。索引定义与表定义存放在.frm中。

文件格式与行存储格式

文件格式定义了存储引擎如何管理这个表空间的文件结构;行存储格式定义存储具体数据在存储文件中的存储方式。每种文件格式包含一组行存储格式

主要两种文件格式:

  • Antelope mysql早期
    • compact
    • redundant
  • Barracuda mysql5.5引入
    • compact mysql5.5之前默认的行格式
    • redundant 最早的行格式,冗余较多
    • compressed compact基础上,进一步压缩
    • dynamic mysql8.0默认,改进对长列存储,存储在外部的overflow列中。没有压缩。

DYNAMICCOMPRESSED 行格式在处理长列数据时都使用了溢出页面(overflow),并在主数据页中保存了指向这些页面的指针,但 COMPRESSED 格式提供了额外的压缩功能来进一步优化存储。

页结构

每个页包含7个结构:

  • 内部的header/trailer:page heaher/page directory 记录页的状态信息

  • 页的header/trailer:fil header/fil trailer 记录页的头信息

  • infinum,suprenum 分别记录比该页任何主键都小的值和最大的值。记录插入、查询范围管理。

  • user records 真正记录数据的部分

  • free space 链表结构,因为行之间的顺序是由next_reocrd指针控制

  • 页之间:双向链表;

  • 行之间:单向链表(会按照表的主键按顺序进行排序,不一定是id字段,以你设置的该表的主键为准),因此一个页中存放的都应该是同一张表的数据,不然不同表之间的主键无法比较大小

当B+树进行查找记录时,一般只能找到对应的页。然后将该页放到内存中(也就是页加载)继续找对应的行,主要用到【page directory】: 页目录是一个数组结构, 我们可以在页目录中使用二分查找的方式进行搜索。页目录用来存储每组最后一条记录的地址偏移量, 这些地址偏移量会按照先后顺序存储起来, 每组的地址偏移量也被称之为槽(slot), 每个槽相当于指针指向了不同组的最后一个记录。

由于槽指向的是一组中最大的值, 所以如果我们判断到某个值比我们的上一个槽值大, 比下一个槽值小的时候, 那么我们就应该到上一个槽的位置, 上一个槽指向的就是上一个页目录中最大的, next_record指针指向的就是下一个槽中的最小值了, 因为页内是单向指针, 所以我们必须要从前往后找。

innodb内存架构

主要三方面:

  • 缓冲池 buffer pool
  • 重做缓冲池 redo log pool
  • 额外内存池

缓冲池

减少IO操作,一定条件触发刷盘。除了数据之外,里面还存储了索引页、Undo页、插入缓冲、自适应哈希索引、InnoDB锁信息和数据字典。

可以发现,内存和磁盘中的修改单位都是,这两种是什么关系呢: 会先查找缓冲池里是否有要修改的页,如果没有缓存,则从磁盘上的数据文件读取数据页,并将其加载到 Buffer Pool 中。

  • Buffer Pool 中的页是磁盘上数据页的缓存副本
  • 当更新数据时,修改的是 Buffer Pool 中的页,而不是磁盘上的页。
  • 只有当脏页被刷到磁盘时,磁盘上的数据文件中的页才会被更新。

重做缓冲池

innodb的一个特性是日志先行。也就是事务提交时,会先记录redo log到redo log buffer,然后再更新缓冲池页数据(此时为脏页)。

因此,提交事务实际上和缓冲池+重做缓冲池+checkpoint有关:

  1. 提交事务时,会先将redo log 写入 redo log buffer
  2. 将数据更新到缓冲池,此时还没真正写入磁盘,也就是脏页
  3. 当触发checkpoint,会先将redo log写入到磁盘里的重做日志文件,然后将脏页刷新到磁盘中

触发 Checkpoint 的情况主要有以下几种:

  1. 定时触发: InnoDB 会根据配置的 checkpoint_age_target 参数,定期触发 Checkpoint。这个参数表示希望脏页在内存中停留的时间,默认值为 7200 秒(2 小时)。当脏页在内存中停留的时间超过 checkpoint_age_target 时,就会触发 Checkpoint。

  2. 日志文件大小达到阈值: 当 Redo Log Buffer 中的日志数据量达到一定阈值时,也会触发 Checkpoint。这个阈值由 innodb_log_buffer_size 参数控制,默认值为 16MB。当 Redo Log Buffer 中的日志数据量超过这个阈值时,InnoDB 会将 Redo Log Buffer 中的日志数据写入磁盘,并同时触发 Checkpoint。

  3. 系统资源不足: 当系统资源不足,例如内存不足或磁盘空间不足时,InnoDB 也会触发 Checkpoint。这是为了释放内存和磁盘空间,避免系统崩溃。

  4. 手动触发: 用户可以通过 FLUSH LOGS 命令手动触发 Checkpoint。

  5. 事务提交: 当事务提交时,InnoDB 会检查 Redo Log Buffer 中的日志数据量是否超过了 innodb_log_buffer_size 参数的阈值。如果超过了阈值,InnoDB 会将 Redo Log Buffer 中的日志数据写入磁盘,并同时触发 Checkpoint。

额外内存池

额外内存池主要包含以下内容:

  1. 系统内存: 用于 MySQL 服务器自身运行所需的内存,包括操作系统内核、MySQL 进程、线程等。

  2. 连接池: 用于存储连接信息,包括连接状态、用户权限等。

  3. 查询缓存: 用于存储查询结果,以便下次执行相同查询时直接从缓存中读取结果,提高查询效率。

  4. 线程栈: 用于存储每个线程执行时的局部变量、函数调用信息等。

  5. 锁信息: 用于存储锁相关信息,包括锁类型、锁状态等。

  6. 其他内存: 用于存储一些其他数据,例如日志缓冲区、临时表等。

两次写 doublewrite

另外,mysql 有个特色是两次写机制。目的是为了保证写入或更新数据的原子性和一致性,主要防止断电等故障引起的数据页的部分写入失败,可以实现数据恢复。

大致步骤: 在提交事务时,没有立即写入磁盘,会先写到内存里的一个连续区域(称为doublewrite buffer)。当doublewrite buffer写满后,会写入磁盘里的一个连续区域(称为doublewrite文件)。只有当这两步都完成后,才会真正写入磁盘的对应数据位置里。

当发生故障重启时,会校验数据文件和doublewrite文件是否一致。若不一致,进行恢复,也就是将doublewrite文件copy到数据文件中(在刷脏页时,并不是直接刷入磁盘,而是copy到内存中的Doublewrite Buffer中,然后再拷贝至磁盘共享表空间(你可以就理解为磁盘)中)。

判断是否开了两次写机制:

1
SHOW VARIABLES LIKE 'innodb_doublewrite';

两阶段提交 tow-phase commit

说到两次写,就想到另一个长得像的概念:两阶段提交。两者不是一个概念。

两阶段提交的主要过程:

  1. 准备阶段:会先把变更写入redolog和binlog中
  2. 提交阶段:确保redolog和binlog都记录好后
  3. fsync :是一个系统调用,用于将文件系统中的缓冲区数据强制同步到物理磁盘上。

两阶段提交 解决的是事务的原子性一致性问题,确保在主从复制或系统崩溃时,事务日志和二进制日志保持一致。

日志

  • mysql日志
    • 错误日志
    • 二进制日志,也就是binlog,记录数据库操作
    • 查询日志,记录来自客户端的所有语句
    • 慢日志
  • innnodb层面日志
    • redo log
    • undo log

常见日志配置参数:

参数名称 说明 默认值
log_error 错误日志文件路径 ./hostname.err
slow_query_log 是否开启慢查询日志 OFF
slow_query_log_file 慢查询日志文件路径 ./hostname-slow.log
long_query_time 慢查询阈值,单位为秒 10
general_log 是否开启通用日志 OFF
general_log_file 通用日志文件路径 ./hostname.log
binlog_format 二进制日志格式 STATEMENT
binlog_row_image 二进制日志记录行数据的方式 FULL
binlog_cache_size 二进制日志缓存大小 32K
log_bin 是否开启二进制日志 OFF
binlog_file_size 二进制日志文件大小 1G
expire_logs_days 二进制日志文件过期时间,单位为天 0
log_slave_updates 是否记录从服务器更新操作 OFF
log_queries_not_using_indexes 是否记录未使用索引的查询 OFF
log_slow_admin_statements 是否记录慢速管理语句 OFF
log_bin_trust_function_creators 是否允许创建函数的语句写入二进制日志 OFF

redo log 和 undo log 相关配置参数:

参数名称 说明 默认值
innodb_log_file_size 每个 Redo Log 文件的大小,单位为字节 5M
innodb_log_files_in_group Redo Log 文件组中文件数量 2
innodb_log_buffer_size Redo Log 缓存大小,单位为字节 16M
innodb_flush_log_at_trx_commit 事务提交时刷脏页到磁盘的策略 1
innodb_log_write_ahead_size Redo Log 写入磁盘的预写大小,单位为字节 16K
innodb_undo_tablespaces Undo Log 文件数量 128
innodb_undo_directory Undo Log 文件目录 ./
innodb_undo_log_truncate Undo Log 文件截断策略 ON
  • innodb_flush_log_at_trx_commit 决定事务提交时刷脏页到磁盘的策略,值为 0、1、2,分别代表:
    • 0:每秒刷一次脏页到磁盘,性能最好,但数据丢失风险最高。
    • 1:每次事务提交都刷脏页到磁盘,性能最差,但数据丢失风险最低。
    • 2:每次事务提交都刷脏页到磁盘,并且每次写入 Redo Log 都刷到磁盘,性能最差,但数据丢失风险最低。

索引

innodb使用B+树作为索引结构。

关于B+树的简单特点:

  • 平衡树,比较次数就是树的高度,所以查找叶子节点的耗费是相同的
  • 非叶子节点存储关键字
  • 叶子节点才存储数据(也就是页,需要读入内存中进一步查找所需的行)

表和索引树的关系:

  • 主键会维护一颗B+树,树的叶子节点存储着行记录的全部信息,即聚集索引(clustered index)。聚集索引决定了数据在磁盘上的物理存储顺序。
  • 其他索引(称为二级索引)也会分别维护一颗B+树,树的叶子节点只存储着表中索引列和主键值(作为书签),即辅助索引(secondary index)。因此这种一般都需要二次查找,也就是拿着查到的主键值,然后在聚集索引中使用主键获取对应的行记录

Innodb使用的锁类型:

  1. 悲观锁,非乐观锁
  2. 主要支持行锁
    1. 共享锁 shared lock 读锁
    2. 互斥锁 exclusive lock 写锁
  3. 也支持表锁(某些情况下使用,如ALTER TABLE
    1. 包括意向锁 intention lock 意向锁本身不会阻止其他事务对不同行加锁,而是用于表明这个事务已经在部分行上持有了锁
      1. 意向共享锁
      2. 意向互斥锁

锁算法:

  • record lock 记录锁,加在索引上
  • gap lock 间隙锁,对索引记录的一段周围连续区域的锁
  • next-key lock

事务与隔离级别

事务4特性ACID:原子性atomicity、一致性consistency、隔离性isolation、持久性durability

4种隔离级别:

  • read uncommited RU 可以读到别的事务没提交的数据,可能引起【脏读
  • read commited RC 可以看到别的事务已经提交的数据,可能引起【不可重复读】。因为一般读是不阻塞另一个事务的写的(除非是select ... for update这种可能会用写锁),所以如果在一个事务中前后两次select,可能会因为其他事务修改了,所以两次读到的结果是不同的。
  • repeatable read RR 可以保证在事务中,前后读取同一行的结果一致。实现方式:事务在第一次读取数据时会创建一个一致性视图(Consistent View),在事务期间,都是基于此视图。但是可能会有【幻读 Phantom Read】(因为此隔离级别只能保证同一行一致,无法保证一定范围内的。所以若查询一定范围内的,可能因为其他事务更新或新增删除范围内的行,导致范围查询结果前后不一致)。
    • 这是mysql默认的隔离级别,可以通过next-key锁在某种程度上解决幻读的问题
  • serializable S 所有查询操作都会被隐式加上读锁,所以其他事务就没法在查询范围内进行修改。但是可能会导致较多的事务等待和性能瓶颈。