Mysql如何利用日志实现主从复制
date
Mar 6, 2024
slug
how-does-mysql-use-logs-to-achieve-replication
status
Published
tags
编程开发
数据库
summary
Mysql中存在好几种日志,其中bin log、redo log、undo log被用于支持数据库的可靠性,例如数据备份、崩溃恢复等,今天来认识下这几种日志。
type
Post
Mysql中存在好几种日志,其中bin log、redo log、undo log被用于支持数据库的可靠性,例如数据备份、崩溃恢复等,今天来认识下这几种日志。
MySQL日志
redo log(重做日志)
我们知道数据只有落盘了,才能保证持久性,而写磁盘这个操作本身却是一个效率十分低下的操作。很多数据库为了提高写数据的效率,都会采用WAL技术,就是Write Ahead Logging,即先写日志,再写磁盘。
对于mysql来说也是如此,我们使用的最多的存储引擎InnoDB在写数据的时候,会先写redo log日志,再更新内存中的数据,在这里就完成了数据的更新了。引擎会在后续适当的时候,把日志中的记录刷回磁盘中。需要注意的是,redo log文件中记录的是mysql中数据页的操作,例如在哪个数据页做了什么操作,是一种逻辑日志。值得注意的是,即使一个事务未提交,其发生的更改也会被记录下来。
为什么写redo log效率就高呢?首先我们要知道,数据落盘这个操作是对磁盘的随机写,因为数据会分布在磁盘的不同区域里,这导致了写效率的低下。而对于redo log来说,这个写日志的操作是顺续写,效率相比随机写会高非常多。另外redo log并不是单个文件,而是由一组大小相同的文件组成。我们会提前配置好redo日志的数量,例如会配置4个日志文件,每个日志文件大小1GB。InnoDB在写redo log的时候,就会从头开始写,写到第四个文件结尾之后,就会覆盖第一个文件的内容又从头开始写。不过在这之前,引擎需要确保这部份被复写的内容已经刷回磁盘。

有了redo log,即使mysql发生崩溃,此时被修改过的上存在于内存中的记录也不会丢失,在数据库重启后,可以通过redo log的日志记录重新执行写操作,修复丢失的数据,从而保证了磁盘中数据的完整性,这个过程就叫做崩溃恢复。
bin log(二进制日志)
了解了redo log,现在来说说mysql里的另一种日志文件bin log。上面说,redo log是InnoDB引擎创建的日志,也就是说它只能在InnoDB引擎下才能被使用。而对于bin log来说,它是mysql系统服务层级的日志,它会详细记录数据库所执行的每一条写操作的sql记录(INSERT、UPDATE、DELETE、CREATE、ALTER、DROP等),是一种物理日志。因此bin log通常用来做数据库备份使用。
bin log同样由一系列文件组成,每个文件记录了一段时间内的数据库更改。文件名通常包含时间戳,以便于识别和排序。但与redo log循环顺序写不同,bin log采用的是追加写,写到指定配置的大小后,就会创建下一个bin log文件继续写。
bin log同样可以用于恢复数据,如果数据库发生故障,可以使用binlog来重放事务,恢复到故障前的状态。bin log也会用于主从节点的复制,主节点会发送bin log给从节点,以达到主从节点的数据一致。这些在后面中我们再看。
undo log(回滚日志)
MySQL支持完备的事务特性,在事务执行过程中出现错误或者事务被中断,undo log可以用来撤销已经进行的修改,确保数据库状态的一致性。而实际上MySQL的事务是依靠MVCC(多版本并发控制)实现的,在MVCC中会为每一行数据建立快照,这个快照就是undo log。
每当一个新的事务开始,InnoDB会为该事务创建一个undo log。undo log记录了事务所做的每个更改的逆向操作。例如,如果一个事务插入了一行记录,相应的undo log会记录删除这一行的操作;如果一个事务更新了一行记录,undo log会记录将这一行恢复到更新前的状态的操作。undo log通过回滚指针 ROLL_PTR 把一个数据行的所有快照连接起来。
虽然undo log和redo log都记录了数据的更改,但它们的目的不同。redo log用于保证事务的持久性,确保在崩溃后可以恢复更改;而undo log用于事务的回滚和MVCC。
mysql中如何执行一条更新语句
接下来我们来看看,当我们要执行这样的一条更新语句,mysql会做什么事情。
- mysql执行器会让InnoDB引擎去查找id=2的数据,如果该数据所在的数据页已经在内存中,则直接返回即可。如果数据页不在内存,则需要到磁盘中读取到内存,再返回。
- 执行器拿到引擎返回的数据后,执行更新操作,并将更新后的结果告知引擎。
- 引擎拿到新的数据后,将结果更新到内存的数据页中,并写redo log,此时redo log还会记录此时状态为prepare,然后告知执行器执行完成,可以提交事务。
- 执行器收到引擎给的通知后,会写bin log。
- 执行器写完bin log后就会调用引擎提交事务接口,引擎把刚才的redo log的状态修改为commit,到此更新操作完成。

这里可能会存在几个疑问。
疑问一:为什么需要两种日志?
首先bin log是mysql数据库自身提供的备份和恢复的方式,而InnoDB是后来才被合并入mysql的存储引擎,其自身在被合并前就已经具备了奔溃恢复的方式,也就是redo log这套机制。
其二是因为单纯只借助bin log是无法实现奔溃恢复,这是由于前面提到的数据库采用的WAL机制决定的。如果数据库在写完bin log且在提交之前,数据库奔溃了,在数据库恢复的时候,这个事务因为还没提交所以能被回滚,再利用bin log的这条记录可以实现恢复。但是对于之前的日志记录,由于已经处于提交状态了,所以按理应该就是认为已经刷盘不应该再被重复进行恢复。但如果这些记录实际上并没有刷回磁盘就发生了奔溃,那其实这部份数据就丢失了,而且此时发生的数据丢失将会是数据页级的丢失。
可以看到,其实还是因为bin log并没有记录哪些日志已经刷回磁盘的原因。所以在奔溃恢复的时候并无法判断这个事务应不应该拿出来进行重放恢复。
session 1 | session 2 | session 3 |
prepared | ㅤ | ㅤ |
update t set c+1 where id=1; | ㅤ | ㅤ |
commiteed | ㅤ | ㅤ |
ㅤ | prepared | ㅤ |
ㅤ | update t set c+1 where id=1; | ㅤ |
ㅤ | commiteed | ㅤ |
ㅤ | ㅤ | prepared |
ㅤ | ㅤ | update t set c+1 where id=1; |
ㅤ | ㅤ | committed(fail) |
再以上面的三个事务为例子来解释下为什么光靠bin log无法实现奔溃恢复,session 1和session 2两个事务都提交成功了,bin log也是完整的,可是此时id=1的数据还存在于数据页中,两次更新的操作都还没被刷回磁盘,也就是说此时的磁盘和内存数据是不一致的。session 3在提交时失败且数据库奔溃了,重启后发现事务还没提交,于是应用bin log里的这条记录完成了奔溃恢复。但是前面两条bin log记录,由于已经提交了,就没有再执行恢复,于是丢失前面两次的更新操作。
正因如此,我们还需要redo log来帮助恢复,由于redo log提供了check point来记录当前刷盘的进度,因此我们能知道当前哪些数据页已经刷回磁盘了。
疑问二:如果redo log处于prepare后,在commited之前,数据库发生了崩溃,对数据一致性有没影响?
这里要看bin log是否写成功了,毕竟如果bin log都没写成功,那后续主从同步的时候,也不会被同步到其他数据库节点,数据一致性不会存在问题。
因此奔溃恢复时,也就是在重启的时候数据库会检查redo log的那个处于prepare的事务在bin log中的完整性,它们可以通过一个共同的字段XID来关联检查。
- 如果对应的bin log是缺失的,则回滚redo log的这条事务。
- 如果对应的bin log是完整的,则会提交redo log的这条事务。
mysql如何实现主从复制
通过上面的内容我们已经知道了redo log和bin log的作用,当需要进行主从数据同步时,bin log将会在这其中扮演非常重要的角色。接下来我们来看下,在mysql中是如何实现主从复制的。
主库和备库之间维护了一条长连接,而在主库中是通过一条独立的线程(dump_thread)来维护与从库的这条连接。接下来会按照这个流程来进行主从数据同步:
- 从库需要先指定主库的地址(通过change master指令),包括ip、端口、用户名、密码等,还要指定要从bin log的哪个位置进行日志同步。
- 从库执行start slave指令,此时会创建两个线程,io_thread和sql_thread。
- io_thread:负责与主库建立连接
- sql_thread:负责来执行bin log中的sql
- 主库通过dump_thread线程,根据从库要求的bin log的同步位置点开始传输bin log日志给到从库。
- 从库拿到日志数据后,会写到本地的一个中转日志文件中,这个文件叫relay log。
- 从库的sql_thread线程开始读取relay log,解析出里面的指令后开始执行,进行数据恢复。
mysql如何保证主从节点之间的数据一致性
mysql通过传输bin log给到从节点来实现节点数据的一致性,但在高负载或网络延迟的情况下,从服务器可能会落后于主服务器,这种现象称为复制延迟。
有没有什么参数可以来判断当前是否发生主从数据同步延迟呢?mysql确实提供了一个参数,我们可以在从节点执行 show slave status 在它提供的信息里会有一个seconds_behind_master,这个字段记录的就是当前从库延迟了多少秒。
那有什么情况会导致主从延迟大呢?最大原因还是从库当前的压力较大,很多时候我们会尽量将读请求分发给从库来做,导致从库的压力相比主库大。虽然我们可以采用更多的从库来暂时分担从库的压力,但这毕竟治标不治本,我们还是得分析有什么操作会导致数据库的压力大。
最常见的情况就是大事务了。例如你要批量删除一些数据,如果因为这个删除操作导致事务执行了很长一段时间,那在事务被写入bin log之前,bin log都无法发给从库,那这段时间的数据便不一致了。除了这些情况外,网络延迟、主从硬件性能差异、锁竞争等都有可能带来主从延迟。
当出现延迟较大的时候,我们就得牺牲一下数据库的可用性了。我们可以将主库设置为只读,保证其没有新数据写入,让从库在这段时间里追上主库的进度。另外MySQL在5.7及以上版本支持了半同步复制,它确保在主服务器提交事务后,至少有一个从服务器已经接收到并开始处理该事务的binlog,这提高了数据一致性。