MySQL逻辑架构大致可以分为两层:
- 服务层:MySQL的核心服务功能,包括查询语句解析,词法语法分析,优化,缓存以及内置函数(日期,时间,数学,加密等),跨存储引擎的功能:存储过程,触发器,视图等。
- 存储引擎:负责MySQL中数据的存储和提取。
我们知道在MySQL中,server层的日志,成为binlog(归档日志),主要用来做主从复置和恢复时使用的。redo log是InnoDB引擎的日志,来实现crash-safe能力。
为什么会有两份日志呢?
因为MySQL早期版本中没有InnoDB引擎,MySQL自带的引擎是MyISAM,MyISAM是没有crash-safe能力的,直到将InnoDB以插件的形式引入,InnoDB使用Redo Log来实现crash-safe能力。
binlog和Redo Log主要有以下三点不同:
- Redo Log是InnoDB引擎特有的;binlog是MySQL的server层实现的,不管哪种引擎,都会有binlog。
- Redo Log是物理日志,记录数据是怎么变化的;binlog是逻辑日志,记录的是语句的逻辑操作。
- Redo Log是循环写的,空间固定会用完;binlog是追加写入的,文件写到一定大小后会切换到下一个,并不会覆盖以前的日志。
InnoDB如何保证crash-safe,MySQL服务器宕机重启后:
- 所有已经提交的事务的数据仍然存在
- 所有没有提交的事务的数据自动回滚
InnoDB通过Redo Log和Undo Log保证以上两点。在每个事务提交的时候,将Redo Log写入硬件存储,这样做会牺牲一些性能,但是可靠性最好。为了平衡两者,InnoDB提供了一个innodb_flush_log_at_trx_commit系统变量,用户可以根据应用的需求自行调整。通过redo日志将所有已经提交的事务恢复,已经prepare但是没有commit的事务将会应用undo log做rollback。
在更新数据时,先写日志,再写磁盘,就是所谓的WAL技术(Write-Ahead Logging)。InnoDB引擎先把记录写到Redo Log中,再更新内存,这个时候认为更新就完成了。InnoDB引擎会在适当的时候,将这个记录更新到磁盘里面,一般会在系统比较空闲的时候。
为保证数据的一致性,就必须保证binlog和Redo Log的一致性,MySQL引入两阶段提交(two phase commit or 2pc),来保证数据一致性。具体做法是将Redo log的写入拆成了两个步骤:prepare和commit,这就是所谓的"两阶段提交”。
在主从复制场景下,MySQL内部会自动将普通事务当作一个XA事务(内部分布式事务)来处理:
- 自动为每个事务分配一个唯一的ID(XID)
- COMMIT会被自动的分成Prepare和Commit两个阶段
- Binlog会被当做事务协调者(Transaction Coordinator),Binlog Event会被当做协调者日志
Binlog在2PC中充当了事务的协调者(Transaction Coordinator)。由Binlog来通知InnoDB引擎来执行prepare,commit或者rollback的步骤。事务提交的整个过程如下:
分析此图,当事务提交时(执行commit语句),分别对应以下几个阶段:
协调者准备阶段(Prepare Phase)
此时SQL已经成功执行了,已经产生了语句的redo和undo内存日志,已经进入了事务commit步骤。然后告诉引擎做Prepare完成第一阶段,Prepare阶段就是写Prepare Log(Prepare Log也是Redo Log),将事务状态设为TRX_PREPARED,写Prepare XID(事务ID号)到Redo Log。写XID到Redo Log的时候会一并把Redo Log刷新到磁盘,这个时候Redo Log的日志量大小取决于执行SQL语句时产生的Redo是否被刷盘,这个刷盘是随机的,后台Master线程每秒钟都会刷新一次。协调者提交阶段(Commit Phase)
2.1 记录协调者日志,即Binlog日志
如果事务涉及的所有存储引擎的Prepare都执行成功,则调用TC_LOG_BINLOG::log_xid方法将SQL语句写到Binlog(write()将binary log内存日志数据写入文件系统缓存,fsync()将binary log文件系统缓存日志数据永久写入磁盘),同时也会把XID写入到Binlog。此时,事务已经铁定要提交了。否则,调用ha_rollback_trans方法回滚事务,而SQL语句实际上也不会写到binlog。
2.2 告诉引擎做Commit
最后,调用引擎的Commit完成事务的提交。并且会对事务的undo log从prepare状态设置为提交状态(可清理状态),刷新Commit Log到Redo Log,释放锁,释放mvcc相关的read view等等;将事务设为TRX_NOT_STARTED状态。
记录Binlog是在InnoDB引擎Prepare(即Redo Log写入磁盘)之后,这点至关重要。另外需要注意的一点就是,SQL语句产生的Redo日志会一直刷新到磁盘(master thread每秒fsync redo log),而Binlog是事务commit时才刷新到磁盘,如果binlog太大则commit时会慢。
由上面的二阶段提交流程可以看出,通过两阶段提交方式保证了无论在任何情况下,事务要么同时存在于存储引擎和binlog中,要么两个里面都不存在,可以保证事务的binlog和redo log顺序一致性。一旦阶段2中持久化Binlog完成,就确保了事务的提交。此外需要注意的是,每个阶段都需要进行一次fsync操作才能保证上下两层数据的一致性。阶段1的fsync由参数innodb_flush_log_at_trx_commit=1控制,阶段2的fsync由参数sync_binlog=1控制,俗称“双1”,是保证crash-safe的根本。