我们之前初步了解了MariaDB, 接着学习了MySQL的一些关于权限, 缓存和索引的东西. 接下来我们继续聊聊关于存储引擎的相关知识.
我们之前聊过MySQL的架构, 其中, 有很多可插拔的第三方或者社区支持的存储引擎, 说到底MySQL的很多特性都是来源于存储引擎的, 其中比较有名的就是MyISAM和InnoDB, 相比于MyISAM, InnoDB支持事务, 这一点就让InnoDB让更多人选择, 或者成为默认的存储引擎(5.5版本之后). 但是当然啦, 存储引擎不止这两个, 还有很多的, 接下来我们就来就其中几个比较重要的来做说明.
InnoDB存储引擎
我们在创建一个新表的时候, 就会指明使用到的存储引擎. (CREATE TABLE … ENGINE=…), 所以说到底, 存储引擎是一个表级别的概念, 就是表的类型.
首当其中就是InnoDB了. InnoDB的设计目标是处理大量的短期事务, 并且, 在机器宕机, 出现断电等导致内存数据还没有被写入到磁盘中的情况下, InnoDB可以自动的进行数据的修复, 这样的一大好处就是避免数据的不一致, 而MyISAM不具备这样的特性, 从而只能进行手动的修复, 手动的修复就会很容易的造成数据不一致(inconsistent).
InnoDB的数据存储在表空间中. 这个表空间就是InnoDB组织数据的”黑盒子”, 在文件系统上表现为一个由InnoDB理解的文件系统. 因此, InnoDB也可以支持裸设备, 即是说, 你可以直接分个区无需格式化从而直接供InnoDB使用.
对于InnoDB来说, 有多种存放数据的策略:
- 所有的InnoDB表的数据, 索引, 都放置在同一个表空间中. (不易管理, 一些操作不被支持)
- 每个表使用单独的表空间存储表的数据和索引
(innodb_file_per_table=ON)
对于第一种存放的方式, 在datadir定义的目录下, 会出现idbdata开头的文件, 自动编号.
我们来看看默认配置下, datadir下的文件样子
1 | [root@node1 mysql]# ls -l /var/lib/mysql/ |
其中的ibdata1就是我们的表空间了.
接下来我们修改配置加上innodb_file_per_table之后, 来创建一个新表试试.
1 | MariaDB [(none)]> create database mydb; |
可以看到, 在这个数据库目录下, 有一个叫做t1.ibd的文件. 这个玩意就是表空间文件, 表现为table_name.ibd这个样子. 另外的一个*.frm的文件是表格式定义(format).
接下来我们快速过一遍InnoDB的一些特性:
- 基于MVCC来支持高并发 – MVCC-Multi Version Concurrency Control
- 支持所有的事务隔离级别, 默认级别: REPEATABLE READ
- 使用间隙锁防止幻读
- 使用聚集索引
- 支持”自适应哈希索引” – Adaptive Hash Index
- 支持热备 (在线) –
xtrabackup - 支持行级锁
我们之前在介绍MariaDB的时候就说过, 它使用的其实不是InnoDB存储引擎, 而是一个社区的改良版, XtraDB. 其实在我们列出存储引擎的时候就能看到:

这个Percona是一个著名的MySQL的技术顾问公司.
MyISAM存储引擎
接下来我们再来说MyISAM, 这个存储引擎早期是针对一些数据仓库设计的, 即较少的写操作, 较多的读操作. 因为不适合在线事务处理. 但MyISAM是MySQL很早期的存储引擎, 因此是十分成熟的. 并且他还有一个很棒的特性就是支持全文索引(FULLTEXT idnex), 可以压缩, 支持空间函数. 相较于InnoDB精细的锁粒度, MyISAM是表级的锁, 因此十分容易出现竞争态. 还有就是我们说过的, 不支持安全恢复.
因此在MariaDB中, 我们使用MyISAM的时候, 就会使用Aria存储引擎, 你可以在上面的图中的最后一行看到. 这就是支持安全恢复的MyISAM存储引擎.
那么MyISAM是如何存储数据的呢? 我们之前使用的hellodb数据库就是使用的MyISAM引擎, 我们来看看:
1 | [root@node1 ~]# ls /var/lib/mysql/hellodb/ -l |
可以看到一共有三种文件类型, frm, MYD, MYI. 其实大概可以猜出来, 其中frm和上面一样是存储表的格式的, D代表Data, I代表Index, 因此, 对于MyISAM来说, 数据和索引是分开存放的. 每一个表都是这三个文件.
其他的一些存储引擎
CSV存储引擎
首先CSV (逗号分隔) 就是一种数据结构, 把它当做表来处理, 就是这么简单.
显然这是不支持索引的, 但是对于导出导入就很灵活.
MRG_MYISAM
可以将多个MyISAM的表合并成为一个虚拟表.
BLACKHOLE
就是/dev/null, 没有实现任何的存储机制.
MEMORY
内存存储引擎, 性能很好(因为全是内存表), 支持hash索引, 并且还有表级锁. 这个临时表存在一个存储的上限, 达到上限就会被写入到磁盘上.
PERFORMANCE_SCHEMA
伪存储引擎, 用来记录MySQL的一些运行数据. 启动前和启动后都是空的.
ARCHIVE
用于数据归档, 仅支持insert和select. 会缓存所有写并使用zlib进行压缩, 但是每一次的select都要进行全表扫描. 有行级锁和专用缓存区.
MySQL的并发控制
接下来我们简单的说说, MySQL的读写锁.
读锁就是共享锁, 一个资源可以被多次增加读锁; 写锁就是独占锁, 或者说叫做排它锁. 那么对于并发访问控制而言, 锁的粒度就十分重要了, 比较常见的是, 表级锁和行级锁. 这里肯定不是越精细就越好, 精细的粒度带来的是更大的锁开销, 因此, 寻找一个锁策略可以在数据安全和锁粒度这两者中获取平衡就至关重要了.
几乎每种存储引擎都可以自行实现自己的锁策略和锁粒度, 同时MySQL在服务器级别也实现了锁, 默认的锁级别就是表级锁. 并且用户是可以进行显式请求施加锁的, 虽然这不是一件推荐做的事情, 因为大多数的情况下, 这些操作都是存储引擎自动完成的, 尽量应该避免用户的干预. 我们把这里的隐式锁叫做存储引擎自动施加的, 而显式锁就是用户手动添加.
接下来我们演示一下, 用户手动加锁的操作. 还是先看看帮助文档:
1 | MariaDB [(none)]> help lock |
通过lock tables来指明加锁. 解锁使用lock tables.
现在我们登陆两个线程, 一个加锁, 另外一个尝试做操作, 来看看变化.
线程1:
1 | MariaDB [mydb]> lock tables t1 write; |
线程2:
1 | MariaDB [mydb]> select SQL_NO_CACHE * from t1; |
其中, 第一次的查询操作是没有加锁的, 而第二次的查询时, 我们已经加锁了, 因此查询被堵塞. 在我们释放锁的一瞬间, 结果就返回了. 从查询的时间可以看出来, 这次查询是被堵塞了.
注意这个地方我在每次查询的时候都加上了SQL_NO_CACHE参数, 目的就是为了防止缓存. 如果你在加了写锁之后发现还可以访问数据, 那大概是来源于缓存, 这个时候你可以update表一次, 从而令缓存失效就可以了.
除了这样加锁, 我们还可以在查询的时候就加上锁, 也可以使用FLUSH TABLES来加锁. 不过这里就不赘述了.
MySQL的事务
什么是事务嘞? 这可真是老生常谈了, 事务是一组原子性的SQL查询, 或者说是一个独立的工作单元. 任何一个事务都要经过ACID测试, 即:
- A: Atomicity: 原子性. 整个事务中的所有操作, 要么全部成功执行 , 要么全部失败回滚. 具体实现上, 我们需要撤销和重做日志.
- C: Consistency: 一致性. 数据库总是从一个一致性状态转换至另外一个一致性状态.
- I: Isolation: 隔离性. 一个事物所做出的的操作, 在提交之前, 是不能被其他的事务所看见的. 由此引入, 多种隔离级别, 隔离级别会影响到并发性能和数据的安全性.
- D: Durability: 持久性. 一个事务提交, 其所作的修改会永久保存在数据库中.
我们来看下事务的生命周期:

大体上, 我们需要显式启动事务, 接着进行一堆修改操作, 一旦提交, 就会持久保存. 或者, 进行回滚操作, 就会回到最初的状态.
当然, 只有支持事务的存储引擎才能支持上面的操作. 我们之前使用的测试sql是使用的MyISAM引擎, 我们可以修改成InnoDB来做测试.
事实上, 对于InnoDB而言, 默认每一条语句都是一个小事务, 有一个控制这个行为变量:
1 | MariaDB [hellodb]> show global variables like 'autocommit'; |
也就是自动提交, 这参数默认是开启的, 也就是所其实每一次的SQL的操作, 都会被打开一个事务, 然后自动提交. 这个行为其实是很消耗磁盘IO的, 所以建议显式的进行事务请求和提交, 就可以有效降低数据库的IO.
其实事务还支持保存点(save point), 这个其实有点类似git的版本控制, 我们可以选择回滚到某一个位置, 来看文档:
1 | MariaDB [hellodb]> help savepoint; |
我们可以直接选择回滚到某一个位置上.
事务隔离级别
接着我们来说说事务的隔离级别. 有以下的级别:
- READ UNCOMMITTED (读未提交)
- READ COMMITTED (读提交)
- REPEATABLE READ (可重读)
- SERILIAZABLE (可串行化)
在事务中, 会出现很多问题, 这些问题有:
- 脏读: 读别的事务没有提交的数据
- 不可重复读: 数据两次读到的值不一样
- 幻读: 多个事务, 其中一个事务多次执行的结果在第二个事务的干预下产生了不一致.
关于这个地方, 我要稍微多说说关于这个幻读, 因为有点难以理解. SQL标准上的意思更多的是在强调: 查询结果出现了新增行. 举维基百科的例子就很清楚了:
对于这些问题, 不同的隔离级别会有不同的读问题.
定义MySQL的隔离级别的参数叫做tx_isolation:
1 | MariaDB [hellodb]> show global variables like '%isolation%'; |
可以看到我们现在用的是可重读.
说回到我们的InnoDB, 我们提到了一个特性叫做MVCC机制, 即多版本并发控制, 这个机制会在MySQL启动的时候存储数据集的快照, 然后通过这个快照来实现很多事务和并发操作. 不过这只是一点点浅显的实现, 关于MVCC的技术细节还是十分复杂的. 这里就略过啦.
但是, 尽管我们有事务, 还是可能会有新的问题, 考虑这样的场景. 一个事务进行提交, 其中包含大量的写语句, 假设现在使用的是机械硬盘, 机械硬盘的一个特性就是需要寻道. 如果我们的要操作的数据块是分散的, 大量的随机IO是需要消耗很多时间的, 假设正在寻道中, 数据库服务器崩了, 那么我的这些操作也就中断了, 数据就不一致了. 这咋办?
这就需要日志了, 还记得我们之前提到的事务日志吗? MySQL在提交事务操作的时候, 不会直接写到数据文件里面, 而是先写事务日志, 而这个事务日志文件是磁盘上连续的地址, 一次寻道, 就可以全部写进去. 这样的话, 如果数据库崩溃, 即使内存中缓冲区的数据被清掉了, 但是在重启的时候, 数据库会进行一次检查, 将事务日志的内容写到数据文件中. 另外, 这个事务日志一般都是多个出现, 一般是2个或者3个. 这是为了防止文件过大.
事务日志的写入类型都是 “追加”, 因此写入操作都是 “顺序IO”. 这些日志都是预写式日志(write ahead logging).
事务日志是由存储引擎控制的, 我们来看看innodb的日志:
1 | [root@node1 ~]# ls /var/lib/mysql/ -lh |
我们可以看到两个一样大的文件ib_logfile, 这就是InnoDB的日志啦, 当然这个日志文件的大小也是可以调整的, 日志的数量也是可以调整的, 包括存放的位置都有变量控制:
1 | MariaDB [hellodb]> show global variables like 'innodb_log%'; |
MariaDB的日志文件
接下来我们继续说说MariaDB的日志文件, 我们在上面说到的InnoDB存储引擎对应的数据文件叫做X.ibd. 为了保证数据, 事件, 服务等等多类功能, 提供了多种日志, 这些日志包括:
- 查询日志: query log (默认是不会开启的, 因为磁盘IO过多)
- 慢查询日志: slow query log (慢查询是指查询时间过长的查询, 为了检查和分析速度慢的原因)
- 错误日志: error log
- 二进制日志: binary log
- 中继日志: relay log
- 事务日志: transaction log
我们一个一个看配置和使用.
查询日志
查询日志是用来记录查询操作的, 这个日志如果开启会有非常大量的数据. 如此大量数据, 我们如果要分析日志就会变得比较困难了, 因此这个查询日志有两种记录方式:
- 文件: file
- 表: table
因此, MySQL的查询日志其实也可以导入到MySQL中. 我们先来看一下变量:

这里的general_log就是控制我们的查询日志是否开启的, 默认是关闭的. 而后存储的路径, 这个路径默认是主机名, 这里我的是node1, 所以就这样了. 另外这里的路径是相对的, 我们也可以修改成绝对路径. 后面还有log_output, 表示日志存储格式是文件类型. 除此之外, 也可以改成TABLE或者NONE.
当然了, 并不是所有的查询操作都有被记录的价值. 更多的我们应该关注慢查询日志.
慢查询日志
慢查询是指执行时长超出指定时长的查询操作. 这个指定时长其实也是我们定义的.

默认的时间是10秒钟.
同样的, 也有对应参数标明开启状态和存储位置:

除此之外, 向上面看, 还有其他的参数表示慢查询的一些过滤器啥的. 关于过滤器, 我们可以稍微多看看. 这个会对查询的记录做限制, 例如全连接, 全表扫描, 缓存未命中, 临时表查询等等等才会记录. 另外, 还有记录速率, 记录的详细程度等等选项.
错误日志
错误日志虽然叫做错误, 但其实并不仅仅是错误信息, 也有可能其他的一些信息, 例如daemon启动关闭的事件. 另外, 错误日志中还会出现:
- 运行中产生的错误信息
- scheduler运行event时产生的日志信息
- 主从复制架构中的从服务器上启动从服务器线程时产生的信息
我们使用的CentOS 7发行版中是默认打开了错误日志的, 我们可以见到:

简单看一下这个文件的内容, 也就是上面的说的那些信息. 有些东西看起来像是警告而不是错误, 那是因为还有一个选项. 叫做log_warning, 这个选项决定是否要记录警告信息到错误日志文件中去.
二进制日志
二进制日志是最重要的日志了, 它用来通过”重放”日志文件中的事件, 来生成数据的副本. 在我们的数据目录下就能看到:
1 | [root@node1 ~]# ls /var/lib/mysql/ |
在一些发行版中这个二进制日志是默认打开的, 但也有的版本是需要手动开启的. 控制这个日志开启与否的变量叫做log-bin.
在mysql的客户端中, 其实也可以查看我们当前的二进制日志, 有这么一条:
1 | MariaDB [(none)]> show binary logs; |
这样是展示当前服务器上的日志有哪些 (注意这里并不是正在使用的, 后面会说), 我们之前说过, 这个日志本身mysql也是可以看的, 我们可以通过客户端来检视这些日志中的事件:

我们来看下这里面的表头, 首先是我们读取的文件名, 接着是当前事件的起始位置和结束位置, 这里我们可以结束位置和下一个事件的起始位置是一样的, 也就是说, 这个结束位置, 实际上是指下一个事件的位置, 虽然名字是这么叫的. 接着还有, 事件的类型, 执行的服务器的ID, 这个ID在我们之后的主从复制的架构中会说到. 以及每一个事件的信息.
接下来我们可以做个实验, 我们停止服务之后再开启:
1 | [root@node1 ~]# systemctl stop mariadb |
接着还是来查看一下:
1 | MariaDB [(none)]> show master logs; |
看! 现在就有两个了, 这就说明 我们的这个日志文件是自动滚动的. 那如何得到当前正在使用的日志文件的信息呢? 我们可以使用show master status:
1 | MariaDB [(none)]> show master status; |
这样就可以看到当前正在使用的以及当前的事件要写在什么位置.
另外, 我们的二进制日志有三种记录格式, 分别是:
- 基于”语句”记录: statement
- 基于”行”记录: row
以及将两种混合的mix模式, 让系统自行判定基于哪种方式来进行记录. 那我们为什么需要有多种记录方式呢?
考虑这样的情况, 如果现在有两个事务, 一个是update所有年龄大于等于60的某个值, 另一个是将一个人的年龄从59改成了60, 在这种情况下, 如果记录到日志中之后, 进行重放就会出现问题, 因为语句的执行顺序不一定会相同.
基于行, 我们就记录数据. 基于语句, 我们就记录执行的语句.
接着我们再来说说二进制日志文件的构成, 有两类文件:
- 日志文件: mysql-bin.文件名后缀, 二进制格式
- 索引文件: mysql-bin.index, 文本格式
我们可以cat一下这个索引文件:
1 | [root@node1 mysql]# cat mariadb-bin.index |
那么哪些变量操作我们的二进制日志的行为嘞? 来看:
binlog_format默认这里是statement, 表示我们基于语句进行记录max_binlog_size单个二进制日志文件的最大体积, 默认是1G, 如果达到最大值就会进行自动日志滚动 (当然啦, 这个肯定不会是精确值).max_binlog_stmt_cache_size,max_binlog_cache_size这两个看名字就懂啦expire_logs_days设置日志的过期时长. 默认是0. 意为不启用sync_binlog是否启动二进制日志的同步功能, 0表示不启用, 1表示启用, N(>1)表示等到N个二进制日志收集之后才会被同步.
在我们装的mysql的bin目录下, 有一个专门的用来dump二进制日志的工具, 叫做mysqlbinlog. 这个玩意可以让我们直接在命令行中看日志中的事件, 来随便截一段看看:
1 | BEGIN |
你看, 这些信息其实就是我们刚刚看到的事件的各种信息, 例如发生的日期和时间, 服务器表示, 结束位置, 事件类型, 线程ID, 语句的时间戳和写入二进制文件中的时间差, 错误代码等等
说道工具的使用, 其实直接看文档就好, 一些常用的选项:
- start-position / stop-position : 开始和终止位置
- start-datetime / stop-datetime : 开始和结束时间, 格式形如这样: YYYY-MM-DD hh:mm:ss
中继日志
这个日志等到复制架构中, 我们再来聊. 这个玩意是从服务器用来保存从主服务器的二进制日志中读取到的事件.
事务日志
事务日志是由事务性存储引擎自行管理和使用的, 而且它们一般是成对的出现的, 一个叫做redo, 一个叫做undo.
关于事务日志, 我们在上一节中已经提到了一些. 这里我们就这样啦.
