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列中。没有压缩。
DYNAMIC 和 COMPRESSED 行格式在处理长列数据时都使用了溢出页面(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有关:
- 提交事务时,会先将redo log 写入 redo log buffer
- 将数据更新到缓冲池,此时还没真正写入磁盘,也就是脏页
- 当触发checkpoint,会先将redo log写入到磁盘里的重做日志文件,然后将脏页刷新到磁盘中
触发 Checkpoint 的情况主要有以下几种:
-
定时触发: InnoDB 会根据配置的
checkpoint_age_target参数,定期触发 Checkpoint。这个参数表示希望脏页在内存中停留的时间,默认值为 7200 秒(2 小时)。当脏页在内存中停留的时间超过checkpoint_age_target时,就会触发 Checkpoint。 -
日志文件大小达到阈值: 当 Redo Log Buffer 中的日志数据量达到一定阈值时,也会触发 Checkpoint。这个阈值由
innodb_log_buffer_size参数控制,默认值为 16MB。当 Redo Log Buffer 中的日志数据量超过这个阈值时,InnoDB 会将 Redo Log Buffer 中的日志数据写入磁盘,并同时触发 Checkpoint。 -
系统资源不足: 当系统资源不足,例如内存不足或磁盘空间不足时,InnoDB 也会触发 Checkpoint。这是为了释放内存和磁盘空间,避免系统崩溃。
-
手动触发: 用户可以通过
FLUSH LOGS命令手动触发 Checkpoint。 -
事务提交: 当事务提交时,InnoDB 会检查 Redo Log Buffer 中的日志数据量是否超过了
innodb_log_buffer_size参数的阈值。如果超过了阈值,InnoDB 会将 Redo Log Buffer 中的日志数据写入磁盘,并同时触发 Checkpoint。
额外内存池
额外内存池主要包含以下内容:
-
系统内存: 用于 MySQL 服务器自身运行所需的内存,包括操作系统内核、MySQL 进程、线程等。
-
连接池: 用于存储连接信息,包括连接状态、用户权限等。
-
查询缓存: 用于存储查询结果,以便下次执行相同查询时直接从缓存中读取结果,提高查询效率。
-
线程栈: 用于存储每个线程执行时的局部变量、函数调用信息等。
-
锁信息: 用于存储锁相关信息,包括锁类型、锁状态等。
-
其他内存: 用于存储一些其他数据,例如日志缓冲区、临时表等。
两次写 doublewrite
另外,mysql 有个特色是两次写机制。目的是为了保证写入或更新数据的原子性和一致性,主要防止断电等故障引起的数据页的部分写入失败,可以实现数据恢复。
大致步骤: 在提交事务时,没有立即写入磁盘,会先写到内存里的一个连续区域(称为doublewrite buffer)。当doublewrite buffer写满后,会写入磁盘里的一个连续区域(称为doublewrite文件)。只有当这两步都完成后,才会真正写入磁盘的对应数据位置里。
当发生故障重启时,会校验数据文件和doublewrite文件是否一致。若不一致,进行恢复,也就是将doublewrite文件copy到数据文件中(在刷脏页时,并不是直接刷入磁盘,而是copy到内存中的Doublewrite Buffer中,然后再拷贝至磁盘共享表空间(你可以就理解为磁盘)中)。
判断是否开了两次写机制:
|
|
两阶段提交 tow-phase commit
说到两次写,就想到另一个长得像的概念:两阶段提交。两者不是一个概念。
两阶段提交的主要过程:
- 准备阶段:会先把变更写入redolog和binlog中
- 提交阶段:确保redolog和binlog都记录好后
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使用的锁类型:
- 悲观锁,非乐观锁
- 主要支持行锁
- 共享锁 shared lock 读锁
- 互斥锁 exclusive lock 写锁
- 也支持表锁(某些情况下使用,如
ALTER TABLE)- 包括意向锁 intention lock 意向锁本身不会阻止其他事务对不同行加锁,而是用于表明这个事务已经在部分行上持有了锁
- 意向共享锁
- 意向互斥锁
- 包括意向锁 intention lock 意向锁本身不会阻止其他事务对不同行加锁,而是用于表明这个事务已经在部分行上持有了锁
锁算法:
- 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 所有查询操作都会被隐式加上读锁,所以其他事务就没法在查询范围内进行修改。但是可能会导致较多的事务等待和性能瓶颈。