innodb为什么写入数据快_[MySQL] 如何做到不丢数据

ab9f6eaaf2aa0bd91bb3cde9219b52b3.png
本文为极客时间专栏《MySQL实战45讲》笔记,文中部分图文来自该专栏。

MySQL是如何做到数据不丢失呢?我们先来介绍下WAL。

WAL(Write-Ahead Logging)机制

WAL 是预写式日志, 关键点在于先写日志再写磁盘。

具体来说,当有一条记录需要更新的时候,InnoDB引擎就会先把记录写到redo log里面,并更新内存,这个时候更新就算完成了。同时,InnoDB引擎会在适当的时候,将这个操作记录更新到磁盘里面,而这个更新往往是在系统比较空闲的时候做。

MySQL在对数据页进行修改时, 通过将"修改了什么"这个操作记录在日志中, 而不必马上将更改内容刷新到磁盘上, 从而将随机写转换为顺序写, 提高了性能。

这种机制一方面提高了MySQL的吞吐量,另一方面也实现了数据的高可靠性。

我们写看下写入机制,再说为啥只要redo log和binlog保证持久化到磁盘,就能确保MySQL异常重启后,数据可以恢复。

binlog的写入机制

事务执行过程中,先把日志写到binlog cache,事务提交的时候,再把binlog cache写到binlog文件中。

一个事务的binlog是不能被拆开的,因此不论这个事务多大,也要确保一次性写入。这就涉及到了binlog cache的保存问题。

系统给binlog cache分配了一片内存,每个线程一个,参数 binlog_cache_size用于控制单个线程内binlog cache所占内存的大小。如果超过了这个参数规定的大小,就要暂存到磁盘。

事务提交的时候,执行器把binlog cache里的完整事务写入到binlog中,并清空binlog cache。

f9ef03e247e9f2a26f83326855a4b358.png

可以看到,每个线程有自己binlog cache,但是共用同一份binlog文件。

图中的write,指的就是指把日志写入到文件系统的page cache,并没有把数据持久化到磁盘,所以速度比较快。

图中的fsync,才是将数据持久化到磁盘的操作。

write 和fsync的时机,是由参数sync_binlog控制的:

  • sync_binlog=0的时候,表示每次提交事务都只write,不fsync;
  • sync_binlog=1的时候,表示每次提交事务都会执行fsync;
  • sync_binlog=N(N>1)的时候,表示每次提交事务都write,但累积N个事务后才fsync。

因此,在出现IO瓶颈的场景里,将sync_binlog设置成一个比较大的值,可以提升性能。在实际的业务场景中,考虑到丢失日志量的可控性,一般不建议将这个参数设成0,比较常见的是将其设置为100~1000中的某个数值。

但是,将sync_binlog设置为N,对应的风险是:如果主机发生异常重启,会丢失最近N个事务的binlog日志。

redo log

redo log的三种状态:

2c3879bfc92664c98c573d49756f46ce.png
redo log
  • 红色:存在redo log buffer中,物理上是在MySQL进程内存中
  • 黄色:写到磁盘(write),但是没有持久化(fsync),物理上是在文件系统的page cache里面
  • 绿色:持久化到磁盘,对应的是hard disk

日志写到redo log buffer是很快的,wirte到page cache也差不多,但是持久化到磁盘的速度就慢多了。

为了控制 redo log 的写入策略,InnoDB 提供了 innodb_flush_log_at_trx_commit 参 数,它有三种可能取值:

  • 0,表示每次事务提交时都只是把 redo log 留在 redo log buffer 中
  • 1,表示每次事务提交时都将 redo log 直接持久化到磁盘
  • 2,表示每次事务提交时都只是把 redo log 写到 page cache

InnoDB 有一个后台线程,每隔 1 秒,就会把 redo log buffer 中的日志,调用 write 写 到文件系统的 page cache,然后调用 fsync 持久化到磁盘。

实际上,除了后台线程每秒一次的轮询操作外,还有两种场景会让一个没有提交的事务的 redo log 写入到磁盘中。

  1. redo log buffer占用的空间即将达到 innodb_log_buffer_size一半的时候,后台线程会主动写盘。注意,由于这个事务并没有提交,所以这个写盘动作只是write,而没有调用fsync,也就是只留在了文件系统的page cache。
  2. 并行的事务提交的时候,顺带将这个事务的redo log buffer持久化到磁盘。假设一个事务A执行到一半,已经写了一些redo log到buffer中,这时候有另外一个线程的事务B提交,如果innodb_flush_log_at_trx_commit设置的是1,那么按照这个参数的逻辑,事务B要把redo log buffer里的日志全部持久化到磁盘。这时候,就会带上事务A在redo log buffer里的日志一起持久化到磁盘。

既然MySQL能保证redo log和binlog能可靠性写入磁盘,那么在他们两者直接如何保证可靠转换的呢?

回答这个问题就得说说另一个机制了。

两阶段提交

redo log 先 prepare,再写 binlog,最后再把 redo log commit。利用这个两阶段提交机制,MySQL保证了redo log和binlog的可靠传输。

我们就一起分析一下在两阶段提交的不同时刻,MySQL异常重启会出现什么现象。

86f048fcc4b989b0b83ec54d02884bf7.png
两阶段提交示意图

如果在图中时刻A的地方,也就是写入redo log 处于prepare阶段之后、写binlog之前,发生了崩溃(crash),由于此时binlog还没写,redo log也还没提交,所以崩溃恢复的时候,这个事务会回滚。这时候,binlog还没写,所以也不会传到备库。到这里,大家都可以理解。

大家出现问题的地方,主要集中在时刻B,也就是binlog写完,redo log还没commit前发生crash,那崩溃恢复的时候MySQL会怎么处理?

我们先来看一下崩溃恢复时的判断规则。

  1. 如果redo log里面的事务是完整的,也就是已经有了commit标识,则直接提交;
  2. 如果redo log里面的事务只有完整的prepare,则判断对应的事务binlog是否存在并完整:
    a. 如果是,则提交事务;
    b. 否则,回滚事务。

这里,时刻B发生crash对应的就是2(a)的情况,崩溃恢复过程中事务会被提交。

由此得出,我们这个两阶段提交机制配合redo log与binlog,就可以保证MySQL崩溃了也不会丢数据(当然了,如果服务器崩了,那就不聊了)。


配置建议

  • innodb_flush_log_at_trx_commit=1。表示每次事务的redolog都直接持久化到磁盘,保证mysql重启后数据不丢失。
  • sync_binlog=1。表示每次事务的binlog都直接持久化到磁盘,保证mysql重启后binlog记录是完整的。

《MySQL实战45讲》专栏里的几个不错的解答如下:

1:MySQL怎么知道binlog是完整的?

回答:一个事务的binlog是有完整格式的:

  • statement格式的binlog,最后会有COMMIT;
  • row格式的binlog,最后会有一个XID event。

另外,在MySQL 5.6.2版本以后,还引入了binlog-checksum参数,用来验证binlog内容的正确性。对于binlog日志由于磁盘原因,可能会在日志中间出错的情况,MySQL可以通过校验checksum的结果来发现。所以,MySQL还是有办法验证事务binlog的完整性的。

2:redo log 和 binlog是怎么关联起来的?

回答:它们有一个共同的数据字段,叫XID。崩溃恢复的时候,会按顺序扫描redo log:

  • 如果碰到既有prepare、又有commit的redo log,就直接提交;
  • 如果碰到只有parepare、而没有commit的redo log,就拿着XID去binlog找对应的事务。

3. 为什么还要两阶段提交呢?干脆先redo log写完,再写binlog。崩溃恢复的时候,必须得两个日志都完整才可以。是不是一样的逻辑?

回答:其实,两阶段提交是经典的分布式系统问题,并不是MySQL独有的。

如果必须要举一个场景,来说明这么做的必要性的话,那就是事务的持久性问题。

对于InnoDB引擎来说,如果redo log提交完成了,事务就不能回滚(如果这还允许回滚,就可能覆盖掉别的事务的更新)。而如果redo log直接提交,然后binlog写入的时候失败,InnoDB又回滚不了,数据和binlog日志又不一致了。

两阶段提交就是为了给所有人一个机会,当每个人都说“我ok”的时候,再一起提交。

4. redo log一般设置多大?

回答:redo log太小的话,会导致很快就被写满,然后不得不强行刷redo log,这样WAL机制的能力就发挥不出来了。

所以,如果是现在常见的几个TB的磁盘的话,就不要太小气了,直接将redo log设置为4个文件、每个文件1GB吧。

5. 正常运行中的实例,数据写入后的最终落盘,是从redo log更新过来的还是从buffer pool更新过来的呢?

回答:这个问题其实问得非常好。这里涉及到了,“redo log里面到底是什么”的问题。

实际上,redo log并没有记录数据页的完整数据,所以它并没有能力自己去更新磁盘数据页,也就不存在“数据最终落盘,是由redo log更新过去”的情况。

  1. 如果是正常运行的实例的话,数据页被修改以后,跟磁盘的数据页不一致,称为脏页。最终数据落盘,就是把内存中的数据页写盘。这个过程,甚至与redo log毫无关系。
  2. 在崩溃恢复场景中,InnoDB如果判断到一个数据页可能在崩溃恢复的时候丢失了更新,就会将它读到内存,然后让redo log更新内存内容。更新完成后,内存页变成脏页,就回到了第一种情况的状态。

6. redo log buffer是什么?是先修改内存,还是先写redo log文件?

回答:这两个问题可以一起回答。

在一个事务的更新过程中,日志是要写多次的。比如下面这个事务:

begin;
insert into t1 ...
insert into t2 ...
commit;

这个事务要往两个表中插入记录,插入数据的过程中,生成的日志都得先保存起来,但又不能在还没commit的时候就直接写到redo log文件里。

所以,redo log buffer就是一块内存,用来先存redo日志的。也就是说,在执行第一个insert的时候,数据的内存被修改了,redo log buffer也写入了日志。

但是,真正把日志写到redo log文件(文件名是 ib_logfile+数字),是在执行commit语句的时候做的。

(这里说的是事务执行过程中不会“主动去刷盘”,以减少不必要的IO消耗。但是可能会出现“被动写入磁盘”,比如内存不够、其他事务提交等情况)。

单独执行一个更新语句的时候,InnoDB会自己启动一个事务,在语句执行完成的时候提交。过程跟上面是一样的,只不过是“压缩”到了一个语句里面完成。


最后我们来看下两阶段提交的整体图:

bf93050712a38ebbad8512861b423ee2.png
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值