MYSQL—两阶段提交
binlog 和 redo log:
有binlog了为什么还要有redo log:
- 历史原因,MyISAM不支持崩溃恢复,而InnoDB在加入MySQL前就已经支持崩溃恢复了
- InnoDB使用的是WAL技术,事务提交后,写完内存和日志,就算事务完成了,脏页有没有落盘不在乎!没有redo的情况下,奔溃恢复后如果binlog是残缺的,说明事务没成功,可以通过undo回滚;如果binlog是完整的,却并不能确定崩溃前脏页有没有落盘,这就无法以binlog是否完整判断事务在数据库崩溃前是否已经持久化、脏页是否落盘,而引入redo后,配合两阶段提交,加上redo是物理日志,就可以进行崩溃恢复了。而且binlog本身因为内容格式的原因,并不适合做崩溃恢复,更适合主从同步或备份恢复。
能不能只用redo,不用binlog:
不考虑主从集群、备份恢复,只从崩溃恢复和高效写WAL角度考虑的话,可以把binlog关闭。binlog本身默认就是关闭的
为什么 binlog cache 是每个线程自己维护的,而 redo log buffer 是全局共用的:
binlog 是不能“被打断的”。一个事务的 binlog 必须连续写(主从同步要求,不然可能会出错),因此要整个事务完成后,再一起写到文件里。而 redo log 并没有这个要求(redo用于崩溃恢复,物理日志,先修改哪个后修改哪个无所谓),中间有生成的日志可以写到 redo log buffer 中。redo log buffer 中的内容还能“搭便车”,其他事务提交的时候可以被一起写到磁盘中。
两阶段提交:
两阶段提交只有在开启了binlog的情况下才会有,而binlog默认是关闭的,那么默认情况下就是:
将事务的数据变更记录到redo(prepare状态的redo),当事务提交时,进入到commit阶段,先把redo的状态更改为commit,然后根据innodb_flush_log_at_trx_commit参数有不同行为:
参数为0,啥也不干
参数为1,write + fsync
参数为2,write
崩溃恢复时,如果redo是commit,那么直接用来做数据恢复,如果是prepare,结合redo的完整性决定是恢复还是回滚事务
在开启了binlog的情况下,需要考虑redo log 和 binlog的一致性:
- 如果在将 redo log 刷入到磁盘之后, MySQL 突然宕机了,而 binlog 还没有来得及写入。MySQL 重启后,通过 redo log 能崩溃恢复,但是 binlog 里面没有该事务的记录,在主从架构中,binlog 会被复制到从库,由于 binlog 丢失了记录,主从发生了不一致;
- 如果在将 binlog 刷入到磁盘之后, MySQL 突然宕机了,而 redo log 还没有来得及写入。由于 redo log 还没写,崩溃恢复以后这个事务无效,而 binlog 里面记录了这条记录,在主从架构中,binlog 会被复制到从库,从库更新了这条记录,主从发生了不一致;
MySQL 为了避免出现两份日志之间的逻辑不一致的问题,使用了「两阶段提交」来解决:
prepare 阶段:将 XID(内部 XA 事务的 ID) 写入到 redo log,同时将 redo log 对应的事务状态设置为 prepare,然后将 redo log 持久化到磁盘(innodb_flush_log_at_trx_commit = 1 的作用,如果是0,不会持久化);
commit 阶段:把 XID 写入到 binlog,然后将 binlog 持久化到磁盘(sync_binlog = 1 的作用),接着调用引擎的提交事务接口,将 redo log 状态设置为 commit,此时该状态并不需要持久化到磁盘,只需要 write 到文件系统的 page cache 中就够了,因为只要 binlog 写磁盘成功,就算磁盘里的 redo log 的状态还是 prepare 也没有关系,一样会被认为事务已经执行成功;
不管是时刻 A(redo log 已经写入磁盘, binlog 还没写入磁盘),还是时刻 B (redo log 和 binlog 都已经写入磁盘,还没写入 commit 标识)崩溃,此时的 redo log 都处于 prepare 状态。
在 MySQL 重启后会按顺序扫描 redo log 文件,碰到处于 prepare 状态的 redo log,就拿着 redo log 中的 XID 去 binlog 查看是否存在此 XID:
- 如果 binlog 中没有当前内部 XA 事务的 XID,说明 redolog 完成刷盘,但是 binlog 还没有刷盘,则回滚事务。对应时刻 A 崩溃恢复的情况。
- 如果 binlog 中有当前内部 XA 事务的 XID,说明 redolog 和 binlog 都已经完成了刷盘,则提交事务(使用prepare的redo进行崩溃恢复,虽然是prepare,但redo是完整的)。对应时刻 B 崩溃恢复的情况。
可以看到,对于处于 prepare 阶段的 redo log,既可以提交事务,也可以回滚事务,这取决于是否能在 binlog 中查找到与 redo log 相同的 XID,如果有就提交事务,如果没有就回滚事务。这样就可以保证 redo log 和 binlog 这两份日志的一致性了。
所以说,两阶段提交是以 binlog 写成功为事务提交成功的标识,因为 binlog 写成功了,就意味着能在 binlog 中查找到与 redo log 相同的 XID。
两阶段缺点:
- 磁盘 I/O 次数高:对于“双1”配置,每个事务提交都会进行两次 fsync(刷盘),一次是 redo log 刷盘,另一次是 binlog 刷盘。
- 锁竞争激烈:两阶段提交虽然能够保证「单事务」两个日志的内容一致,但在「多事务」的情况下,却不能保证两者的提交顺序一致,因此,在两阶段提交的流程基础上,还需要加一个锁来保证提交的原子性,从而保证多事务的情况下,两个日志的提交顺序一致。在早期的 MySQL 版本中,通过使用 prepare_commit_mutex 锁来保证事务提交的顺序,在一个事务获取到锁时才能进入 prepare 阶段,一直到 commit 阶段结束才能释放锁,下个事务才可以继续进行 prepare 操作。