mysql 架构
mysql 作为一款开源的关系型数据库,采用插件式的存储引擎架构,使查询处理和其他的系统任务以及数据的存储相分离,从而可以根据业务需求和实际情况选择相应的存储引擎。mysql 支持按表来设置不同的存储引擎。
整理上来说,mysql 架构分为四层,分别是:连接层,服务层,引擎层和存储层。
- 连接层:客户端和连接服务。主要完成一些连接处理、授权认证等。
- 服务层:主要完成大部分的核心服务,包括查询解析、优化、缓存及内置函数等。
- 引擎层:真正负责数据的存储和提取,服务器通过 api 和引擎层交互。
- 存储层:将数据存储在文件系统上。
那么,一条 sql 语句是如何执行的?
存储引擎
mysql 常见的储存引擎有 innodb、myisam、memory、ndb 等,其中 innodb 作为 mysql 默认的存储引擎,支持事务、外键和行级锁。
innodb 和 myisam 引擎的对比:
| 存储引擎 | innodb | myisam |
|---|---|---|
| 事务 | √ | × |
| 外键 | √ | × |
| 行级锁 | √ | × |
| 缓存 | 只缓存索引,不缓存真实数据 | 不仅缓存索引还要缓存真实数据,对内存要求较高,而且内存大小对性能有决定性的影响 |
| 索引 | 聚簇索引,数据文件本身就是主键索引的文件 | 非聚簇索引,索引文件和书文件分离,索引存的是数据记录的地址 |
| 文件结构 | .frm(数据表的元数据), .ibd(独享表空间存放,一张表一个), .ibdata(共享表空间存放,所有表一个或多个) | .frm(数据表的元数据), .myd(my data,数据文件), .myi(my index,索引文件 ) |
事务与日志
事务的四大特性:原子性、一致性、隔离性、持久性
- 原子性:一组动作要么全部成功,要么全部失败,不存在中间选项。
- 一致性:事务前后数据库的约束依旧完整,没有被破坏。
- 隔离性:一个事务不影响其他事务的执行。
- 持久性:事务完成后,对数据库的更改应该被永久保存。
innodb 支持事务,并通过 undolog 来保证原子性,redolog 来保证持久性。
具体来说,undolog 记录了数据的逻辑变化。比如一条 insert 语句,undolog 中对应着一条 delete 语句;一条 update 语句,就对应着一条相反的 update 语句。这样当触发回滚时,就能回到事务开始的状态。
而 redolog 是物理日志,记录了磁盘数据页的修改。redolog 的写入方案叫做 WAL(write-ahead logging,预写日志),也就是先写日志,再写磁盘,等空闲的时候才会将 redolog 中的数据写入磁盘中。如果数据写满了但是还没有来得及将数据真正的刷入磁盘当中,那么就会发生“内存抖动”现象,从肉眼的角度来观察会发现 mysql 会宕机一会,此时就是正在刷盘了。
innodb 支持三种将 buffer 持久化到 file 的时机,可以通过 innodb_flush_log_at_trx_commit 指定,分别是:
- 0,延时写,每秒写入 os buffer 并调用 fsync()
- 1,实时写,实时刷,每次提交都会写入 os buffer 并调用 fsync()
- 2,实时写,延时刷,每次提交都会写入 os buffer,每秒调用 fsync()
redolog file 的大小是固定的,采用循环写的模式。当所有文件写满后,会从头覆盖写。因为已经刷入磁盘的数据都会从 redolog 中删除,所以 redolog 提供了 crash-safe 的能力。
innodb 写入的顺序如下:
注意 innodb 除了会写入 undolog 和 redolog 之外,还会写入 binlog。binlog 又叫归档日志,是 mysql 服务层的日志,又叫归档日志,主要负责主从复制和备份恢复。注意 binlog 是追加写的形式,并不会删除已经刷盘的日志,所以无法提供 crash-safe 的能力。
主从复制:
binlog 有三种记录的形式,分别是 statament(语句)、row(行实际数据的变更)和 mixed(混合)。其中 statement 模式保存的是 sql 语句,日志文件小、性能较好,但准确性差,比如说 sql 中有 now()函数就会导致数据不准确。row 模式记录的是实际的变更,准确性好,但日志文件比较大。
为了保证从 redolog 和 binlog 恢复、备份的数据一致性,必须保证 redolog 和 binlog 的一致性。而 mysql 采用两阶段提交的方式来保证两者的一致性,也就是先将 redolog 标记为 prepare,再写 binlog,最后将 redolog 标记为 commit。
至于事务的隔离性,因为隔离性在一定程度上是和并发是矛盾的,所以 mysql 定义了四大隔离级别供用户选择,分别是:
- 读未提交:允许读取尚未提交的数据变更,可能会导致脏读、幻读或不可重复读
- 读已提交:允许读取并发事务已经提交的数据,可以阻止脏读,但是幻读或不可重复读仍有可能发生
- 可重复读:对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改,可以阻止脏读和不可重复读,但幻读仍有可能发生
- 串行化:最高的隔离级别,完全服从 ACID 的隔离级别。所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,该级别可以防止脏读、不可重复读以及幻读
隔离级别的底层实现有两种,一是锁机制,二是 mvcc(多版本并发控制)。
先说锁机制。锁机制针对的是当前读。当前读就是读取数据的最新版本,加锁的读(select ... lock in share mode,select ... for update)以及 update,delete,insert 都是当前读。innodb 实现了两种类型的行锁,分别是共享锁(S 锁)和排他锁(X 锁),还有两种表级的意向锁:意向共享锁(IS)和意向排他锁(IX)。
mvcc 针对的是快照读,也就是常用的 select(不加锁)语句。它维护了一个数据的多个版本,使读写操作没有冲突。对于读已提交的隔离级别,每次执行 select 都生成一个快照读;对于可重复读的隔离级别,开启事务的第一个 select 时生成快照读;而对于串行化的隔离级别,快照读将会退化成当前读。
具体来说,数据表中的每行数据,都存在 trx_id 和 roll_pointer 两个隐藏字段,分别是每次的事务 id 和指向 undolog 指针。通过这个指向 undolog 的指针,可以将该条记录的修改日志串起来,形成一个版本链:
每次生成的快照读,实际上是生成了一个 readview,分别有如下字段:
- m_ids:指的是在创建 ReadView 时,当前数据库中“活跃事务”的事务 id 列表,注意是一个列表, “活跃事务”指的就是,启动了但还没提交的事务
- min_trx_id:指的是在创建 ReadView 时,当前数据库中“活跃事务”中事务 id 最小的事务,也就是 m_ids 的最小值。
- max_trx_id:创建 ReadView 时当前数据库中应该给下一个事务的 id 值,也就是全局事务中最大的事务 id 值 + 1
- creator_trx_id:创建该 ReadView 的事务的事务 id, 只有在对表中的记录做改动时(执行 INSERT、DELETE、UPDATE 这些语句时)才会为 事务分配事务 id,否则在一个只读事务中的事务 id 值都默认为 0
通过遍历版本链,将 readView 数据 和被访问的 trx_id 进行对比,从而判断被访问记录对当前事务可不可见,如果可见的话,即返回数据。