MySQL-InnoDB拾遗
之前写过一篇介绍MySQL中存储引擎的文章MySQL之存储引擎,在实际工作中,还是以InnoDB存储引擎为主,此文大部分为InnoDB引擎中的概念拾遗。
InnoDB存储引擎体系结构体系模型
以上为InnoDB存储引擎大致上的体系结构。
其中后台线程负责:刷新内存池中数据;将已修改的数据文件刷新到磁盘。
后台线程又分为Master Thread与IO Thread。Master Thread负责将缓冲池中的数据异步刷新到磁盘,保证数据一致性,包括脏页的刷新,合并插入缓冲等。IO Thread则负责IO请求。
接下来是最核心的部分,内存池负责:缓存磁盘上的数据;在对磁盘文件的数据修改之前在这里缓存;重做日志(redo log)缓冲。内存池的结构又可划分如下图
内存池结构内存池中最重要的结构显而易见是缓冲池。
缓冲池:MySQL数据基于磁盘存储,并将其中的记录按照页的方式进行管理。缓冲池是一段内存区域,来弥补磁盘速度较慢对性能的影响。对于读操作,会将磁盘读取到的页放在缓冲池中,对于写,首先修改在缓冲池中的页,再定时刷新到磁盘。缓冲池的数据淘汰通过LRU算法进行管理。插入缓冲:对于非唯一辅助索引的插入或更新,不是每一次直接插入到索引页中,而是先判断插入的非聚集索引页是否在缓冲池中,若在直接插入,若不在,先放入insert buffer中,之后多次操作合并,提高性能。关于缓冲池更详细的介绍可以看58沈剑的这篇文章缓冲池,这次彻底懂了!。
日志文件
几个重要的日志文件:二进制日志(MySQL层面),慢查询日志(MySQL层面),查询日志(MySQL层面),RedoLog(InnoDB层面),UndoLog(InnoDB层面)。查询日志包括慢查询日志就是SQL的执行记录,一般通过它们来进行SQL优化,具体的不做介绍,着重关注不暴露给外面的几种日志。
BinLog:binlog是MySQL层面的日志,记录了数据的更改,用于故障恢复,主从复制等。binlog共有三种模式:STATEMENT,记录SQL语句;ROW,记录行更改;MIXED,默认采用STATEMENT,某些特殊情况下采用ROW,例如使用了临时表。MySQL中有两个跟binlog相关的重要参数
(1)binlog_cache_size,二进制日志缓冲,当事务的记录大于设定的binlog_cache_size时,mysql会把缓冲区中的日志信息写入一个临时文件中。设置过大,会造成内存浪费。设置过小,会频繁将缓冲日志写入临时文件。
(2)sync_binlog,=0(默认)表示刷新binlog时间点由操作系统自身来决定,操作系统自身会每隔一段时间就会刷新缓存数据到磁盘,性能最好,但有可能丢失数据。=N,代表每N个事务提交会进行一次binlog刷新。N=1最安全,但性能较差。
RedoLog:InnoDB引擎层面,记录了InnoDB的事务日志,由两部分组成,一是内存中的RedoLog缓冲,二是RedoLog文件。RedoLog记录某数据块被修改后的值,可以用来恢复未写入data file的已成功事务更新的数据,保证事务的原子性和持久性。redo日志与binlog日志作用类似,但是前者为InnoDB引擎特有,后者为MySQL层面,所有的存储引擎都会产生binlog。前者是物理格式,记录的是每个页的修改;后者是逻辑日志,记录的是对应的SQL语句。binlog只在事务提交完成后进行一次写入,redolog则在事务进行中不断写入。同binlog一样,redolog也有相关刷新内存缓冲的参数,innodb_flush_log_at_trx_commit控制重做日志刷新到磁盘的策略,默认为1,表示事务提交时进行一次fsync,为0时表示每秒进行一次fsync(效率比前者高,但可能丢失一秒钟的数据)
UndoLog:InnoDB引擎层面,记录数据被修改前的值,可以用来在事务失败时进行rollback,保证事务的一致性,回滚行记录到某个特定版本,通常是逻辑日志,根据每行记录进行记录。
binlog 和 redolog的一致性问题:MySQL使用内部XA(两阶段提交)解决了 binlog 和 redo log的一致性问题。MySQL中的XA实现分为:外部XA和内部XA;前者是指我们通常意义上的分布式事务实现(因为性能原因,生产分布式事务不会采用此方案);后者是指单台MySQL服务器中,Server层作为TM(事务协调者),而服务器中的多个数据库实例作为RM,而进行的跨库事务,也就是一个事务涉及到同一条MySQL服务器中的两个innodb数据库。同时内部XA也用来解决这两种日志的一致性问题。
B+树索引
InnoDB采用B+树作为底层的索引结构,B+树由二叉查找树(左子树键值小于根,右大于根),再由平衡二叉树(子树高度差最大位1),B树演化而来,专为磁盘等存储设备设计。B+树中所有记录节点都是按键值得大小顺序存放在同一层的叶子节点上,由各叶子节点指针进行连接。
一颗高度为2的B+树B+树索引分为聚集索引和辅助索引,前者按照主键构造B+树(当InnoDB表没有主键时,会选择使用唯一非空索引或者自动创建的6字节指针作为主键。),同时叶子节点中存放的即为整张表的行记录数据,也将聚集索引的叶子节点称为数据叶,这个特性决定了索引组织表中数据也是索引的一部分。后者叶子节点中不包含行记录的全部数据,叶子节点除了包含键值以外,索引行中还包含了一个书签,该书签(其实就是相应行数据的聚集索引键)用来告诉InnoDB哪里可以找到与索引相对应的行数据。通过辅助索引获取数据的流程是先找到对应书签,再通过主键索引找到相应的数据
多个键值的B+树数据按照(a,b)的顺序进行了存放,对于where a=xxx and b=xxx与where a=xxx都是可以使用(a,b)联合索引的。而对于where b=xxx则不能使用该索引,叶子节点b值也非有序,不能使用索引。这也是索引的左前缀原则的原理。
InnoDB中的锁
InnoDB中锁可以分为2个类型:
1.共享锁 :允许事务读一行数据
2.排它锁:允许事务删除或更新一行数据
锁的兼容性:若事务T1获得行R的共享锁,T2可以立即获取R的共享锁,这种情况称为锁兼容。排它锁与任何类型的锁都不兼容。
意向锁:InnoDB支持多粒度锁定,允许事务在行锁和表锁同时存在,为了支持不同粒度上的加锁操作,InnoDB使用了意向锁的概念。意向锁将锁定的对象分为多个层次,意味着事务希望在更细粒度上进行加锁。例如,若需要对记录R上排它锁,那么分别需要对数据库、表、页上意向锁,最后再对R上排它锁。其中任何一个部分导致等待,那么该操作需要等待组粒度锁的完成。例如若已经有事务对记录所在的表进行了共享锁,那么再要对R上排它锁,由于锁的不兼容性,需要等待表锁操作的完成。引进意向锁后,系统对某一行加锁时不必逐个检查与下一级结点的封锁冲突了,提升了性能。
一致性非锁定读:指InnoDB通过行多版本并发控制(MVCC)的方式来读取当前数据库中的数据。如果读取的行正在执行update或delete操作,此时的读取操作并不会等待行锁释放,而是读取行的一个快照数据。MVCC只在可重复读与读已提交下工作。对于前者,总是读取事务开始时的行数据版本,对于后者,总是读取被锁定行的最新一份快照数据。MVCC可以使得大部分读操作都不用加锁。
一致性锁定读:显式的对数据库读取操作进行加锁保证数据逻辑的一致性。对于select语句,支持两种模式:select...for update; select...lock in share mode。前者对读取的行记录加一个排它锁,其他事务不能对该行加任何锁。后者对读取的行记录加一个共享锁,其他事务可以加共享锁但不能加排它锁。这种方式也是我们熟知的数据库悲观锁。
行锁的3种算法:Record Lock:单个行记录的锁;Gap Lock:间隙锁,锁定一个范围,但不包含记录本身;Next-Key Lock:锁定一个范围,并包含记录本身。在可重复读隔离级别下,InnoDB使用Next-Key Lock解决幻读问题。
数据的复制
上面说过,MySQL通过binlog完成数据的复制。当数据库做主从架构时,主库把数据更改记录到二进制日志中,备库将主库的日志复制到自己的中继日志(Relay log),备库读取中继日志的信息重放。MySQL会按照事务的提交顺序而非执行顺序来记录binlog。备库启动一个IO线程跟主库建立连接,然后在主库上启动一个二进制转储线程,该线程用于读取主库上二进制日志中的事件,如果该线程追赶上了主库,则会进入休眠状态。备库的SQL线程从中继日志中读取事件并执行。若binlog为statement模式,有些SQL可能略有问题,比如使用NOW函数,会有短暂延迟,若为row模式,无法处理主库修改表schema的情况。
主从同步延时的解决:半同步复制(当提交事务时,客户端接收到查询结束反馈前必须保证二进制日志已经传输到至少一台备库上),并行复制(从库开启多个sql线程,并行重放),直连主库(要求读写均走主库),修改代码逻辑(避免更新或插入后立即读取),在事务中进行操作(在事务中读写都走主库)