由于mysql开源、体积小、速度快,总体拥有成本低,目前已广泛被大小公司使用,特别是在互联网,全球前20大互联网网站有18家使用了MYSQL,有些公司除使用外还在mysql的功能基础上做一定的优化和改造,使之更加适合公司特殊业务场景,比如说阿里。
另外,由于mysql的生态越来完善,像阿里的canal、唯品会的RDP、VDB都是基于mysql的binlog,及时监控表的数据变化,让其它应用服务及时感知表的数据变化,异步更新缓存或将数据异步准实时同步ES集群。MYSQL数据库在中国的应用情况,已经是国内互联网公司默认标配,人才储配,架构、解决方案储备非常完善了,而传统企业看,迅速意识到互联网重要性,MYSQL一定是他们的首选方案,它的优势非常多,那么我今天主要聊聊mysql如何保证不丢数据的。
mysql保证不丢数据,binlog和redolog功不可没,也正是因为有这两个日志相互配合,innodb引擎已被广泛使用重要原因之一。而了解mysql内部确保数据不丢失的原理,学习里面优秀的设计要点,然后我们再借鉴这些优秀的设计要点进行实践应用,加深理解。
在mysql中有两种LOG,分别是redo log和binlog,只要保证持久化到磁盘,即使异常或服务器cash,它也能确保重启后可以恢复数据,那么今天来探讨MYSQL如何数据不丢失的,在探讨之前先简单了解一下binlog和redolog。
MySQL中的binlog是一个二进制文件,它记录了所有的增删改操作,节点之间的复制就是依靠binlog来完成的,包括依赖binlog的应用很多也是基于binlog。
我们的先来了解一下binlog写入流程,如下图:
从上图中可以看到,binlog的写入逻辑比较简单:事务执行过程中,先把日志写到 binlog cache,事务提交的时候,再把 binlog cache的内容写到 binlog 文件中。
binlog有三种模式,分别是statement、row、mixed,其中mixed其实它就是前两种格式的混合, 决定用哪种模式是由binlog_format这个参数来定的。
mysql为什么要引入mixed模式呢?因为有些 statement 格式的 binlog 可能会导致主备不一致,所以要使用 row 格式。但 row 格式的缺点是,很占空间。比如你用一个 delete 语句删掉 10 万行数据,用statement 的话就是一个 SQL 语句被记录到 binlog 中,占用几十个字节的空间。但如果用 row 格式的 binlog,就要把这 10 万条记录都写到 binlog 中。这样做,不仅会占用更大的空间,同时写 binlog 也要耗费 IO 资源,影响执行速度。所以,MySQL 就取了个折中方案,也就是有了 mixed 格式的 binlog。mixed 格式的意思是,MySQL 自己会判断这条 SQL 语句是否可能引起主备不一致,如果有可能,就用 row 格式,否则就用 statement 格式。也就是说,mixed 格式可以利用 statment 格式的优点,同时又避免了数据不一致的风险。
为了提高性能,通常会将有关联性的多个数据修改操作放在一个事务中,这样可以避免对每个修改操作都执行完整的持久化操作。这种方式,可以看作是人为的组提交(group commit)。除了将多个操作组合在一个事务中,记录binlog的操作也可以按组的思想进行优化:将多个事务涉及到的binlog一次性flush,而不是每次flush一个binlog。事务在提交的时候不仅会记录事务日志,还会记录二进制日志,但是它们谁先记录呢?二进制日志是MySQL的上层日志,先于存储引擎的事务日志被写入,与组提交的相关的参数是分别binlog_group_commit_sync_delay 和 binlog_group_commit_sync_no_delay_count 来实现。
1、binlog_group_commit_sync_delay 参数,表示延迟多少微秒后才调用 fsync;
2、binlog_group_commit_sync_no_delay_count 参数,表示累积多少次以后才调用 fsync。
这两个条件是或的关系,也就是说只要有一个满足条件就会调用 fsync,所以,当 binlog_group_commit_sync_delay 设置为 0 的时候,binlog_group_commit_sync_no_delay_count 也无效了。
binlog叫逻辑归档日志,记录的是这个语句的原始逻辑,redo log也叫重做日志,是物理日志,记录的是“在某个数据页上做了什么修改”;它是固定大小的,另外redo log是循环写的,空间固定会用完;
redo log包括两部分:一是内存中的日志缓冲(redo log buffer),该部分日志是易失性的;二是磁盘上的重做日志文件(redo log file),该部分日志是持久的。在概念上,innodb通过force log at commit机制实现事务的持久性,即在事务提交的时候,必须先将该事务的所有事务日志写入到磁盘上的redo log file和undo log file中进行持久化。
为了确保每次日志都能写入到事务日志文件中,在每次将log buffer中的日志写入日志文件的过程中都会调用一次操作系统的fsync操作(即fsync()系统调用)。要写入到磁盘上的log file中(redo:ib_logfileN文件,undo:share tablespace或.ibd文件),中间还要经过操作系统内核空间的os buffer,调用fsync()的作用就是将OS buffer中的日志刷到磁盘上的log file中,也就是说,从redo log buffer写日志到磁盘的redo log file中,过程如下:
为了控制 redo log 的写入策略,InnoDB 提供了innodb_flush_log_at_trx_commit 参数,它有三种可能取值:
内存中(buffer pool)未刷到磁盘的数据称为脏数据(dirty data),由于数据和日志都以页的形式存在,所以脏页表示脏数据和脏日志。在innodb中,数据刷盘的规则只有一个:checkpoint,但是触发checkpoint的情况却有几种。不管怎样,checkpoint触发后,会将buffer中脏数据页和脏日志页都刷到磁盘,innodb存储引擎中checkpoint分为两种:
LSN称为日志的逻辑序列号(log sequence number),在innodb存储引擎中,lsn占用8个字节。LSN的值会随着日志的写入而逐渐增大,根据LSN,可以获取到几个有用的信息:
1.数据页的版本信息。
2.写入的日志总量,通过LSN开始号码和结束号码可以计算出写入的日志量。
3.可知道检查点的位置。
LSN不仅存在于redo log中,还存在于数据页中,在每个数据页的头部,有一个fil_page_lsn记录了当前页最终的LSN值是多少,通过数据页中的LSN值和redo log中的LSN值比较,如果页中的LSN值小于redo log中LSN值,则表示数据丢失了一部分,这时候可以通过redo log的记录来恢复到redo log中记录的LSN值时的状态。
在启动innodb的时候,不管上次是正常关闭还是异常关闭,总是会进行恢复操作,因为redo log记录的是数据页的物理变化,因此恢复的时候速度比逻辑日志(如二进制日志)要快很多。而且,innodb自身也做了一定程度的优化,让恢复速度变得更快。
重启innodb时,checkpoint表示已经完整刷到磁盘上data page上的LSN,因此恢复时仅需要恢复从checkpoint开始的日志部分。例如,当数据库在上一次checkpoint的LSN为10000时宕机,且事务是已经提交过的状态。启动数据库时会检查磁盘中数据页的LSN,如果数据页的LSN小于日志中的LSN,则会从检查点开始恢复。
还有一种情况,在宕机前正处于checkpoint的刷盘过程,且数据页的刷盘进度超过了日志页的刷盘进度。这时候一宕机,数据页中记录的LSN就会大于日志页中的LSN,在重启的恢复过程中会检查到这一情况,这时超出日志进度的部分将不会重做,因为这本身就表示已经做过的事情,无需再重做。
另外,事务日志具有幂等性,所以多次操作得到同一结果的行为在日志中只记录一次。而二进制日志不具有幂等性,多次操作会全部记录下来,在恢复的时候会多次执行二进制日志中的记录,速度就慢得多。例如,某记录中id初始值为2,通过update将值设置为了3,后来又设置成了2,在事务日志中记录的将是无变化的页,根本无需恢复;而二进制会记录下两次update操作,恢复时也将执行这两次update操作,速度比事务日志恢复更慢。
redo log不是二进制日志,虽然二进制日志中也记录了innodb表的很多操作,也能实现重做的功能,但是它们之间有很大区别。
WAL 机制主要得益于两个方面:redo log 和 binlog 都是顺序写,磁盘的顺序写比随机写速度要快;组提交机制,可以大幅度降低磁盘的 IOPS 消耗。如果你的 MySQL 现在出现了性能瓶颈,而且瓶颈在 IO 上,可以通过以下几种方法来提升性能:
1、设置 binlog_group_commit_sync_delay 和 binlog_group_commit_sync_no_delay_count 参数,减少 binlog 的写盘次数。这个方法是基于“额外的故意等待”来实现的,因此可能会增加语句的响应时间,但没有丢失数据的风险。
2、将 sync_binlog 设置为大于 1 的值(比较常见是 100~1000)。这样做的风险是,主机掉电时会丢 binlog 日志。
3、将 innodb_flush_log_at_trx_commit 设置为 2,这样做的风险是,主机掉电的时候会丢数据。
一般不建议你把 innodb_flush_log_at_trx_commit 设置成 0,因为把这个参数设置成 0,表示 redo log 只保存在内存中,这样的话 MySQL 本身异常重启也会丢数据,风险太大。而 redo log 写到文件系统的 page cache 的速度也是很快的,所以将这个参数设置成 2 跟设置成 0 其实性能差不多,但这样做 MySQL 异常重启时就不会丢数据了,相比之下风险会更小。
参考资料:
1、详细分析MySQL事务日志(redo log和undo log)
2、MYSQL实战45讲