MySQL数据一致性-单机

316 阅读10分钟

数据+LOG

数据库的数据由两部分组成,一部分是数据,一部分是LOG。Innodb的数据包括内存(Innodb buffer pool)中和硬盘中的数据。数据的更改首先会作用到内存中的缓存数据,然后Innodb会根据flush策略将最新的数据flush到硬盘中。因此数据并不是实时落盘的,此时如果进程或者系统崩溃的话,没flush到硬盘的数据会丢失。Innodb采用了WAL技术,通过Redo日志与Undo日志保证了数据的持久性与原子性。

WAL技术

计算机科学中,预写式日志(Write-ahead logging,缩写 WAL)是关系数据库系统中用于提供原子性和持久性(ACID属性中的两个)的一系列技术。在使用WAL的系统中,所有的修改在提交之前都要先写入log文件中。

Redo与Undo

为啥使用Redo与Undo。

» MySQL数据库InnoDB存储引擎Log漫游(1)

IO性能 Undo + Redo的设计主要考虑的是提升IO性能。虽说通过缓存数据,减少了写数据的IO.

但是却引入了新的IO,即写Redo Log的IO。如果Redo Log的IO性能不好,就不能起到提高性能的目的。 为了保证Redo Log能够有比较好的IO性能,InnoDB 的 Redo Log的设计有以下几个特点:

 A. 尽量保持Redo Log存储在一段连续的空间上。因此在系统第一次启动时就会将日志文件的空间完全分配。
     以顺序追加的方式记录Redo Log,通过顺序IO来改善性能。
 B. 批量写入日志。日志并不是直接写入文件,而是先写入redo log buffer.当需要将日志刷新到磁盘时
     (如事务提交),将许多日志一起写入磁盘.
 C. 并发的事务共享Redo Log的存储空间,它们的Redo Log按语句的执行顺序,依次交替的记录在一起,
     以减少日志占用的空间。例如,Redo Log中的记录内容可能是这样的:
     记录1: <trx1, insert>
     记录2: <trx2, update>
    记录3: <trx1, delete>
     记录4: <trx3, update>
     记录5: <trx2, insert>
 D. 因为C的原因,当一个事务将Redo Log写入磁盘时,也会将其他未提交的事务的日志写入磁盘。
 E. Redo Log上只进行顺序追加的操作,当一个事务需要回滚时,它的Redo Log记录也不会从Redo Log中删除掉。

Redo为啥采用物理page+逻辑

zhuanlan.zhihu.com/p/109417488

» MySQL数据库InnoDB存储引擎Log漫游(2)

这也是为啥需要double write buffer

保证日志的刷盘

上文提到Innodb通过Redo日志与Undo日志保证了数据的持久性与原子性。所以日志的刷盘策略很关键,只有日志及时刷盘,持久性和原子性才得以实现。 这里面就涉及到几个参数,包括innodb_flush_method、innodb_flush_log_at_commit等。

innodb_flush_method

Defines the method used to flush data to InnoDB data files and log files, which can affect I/O throughput. innodb_flush_method与open文件的模式不是一一对应,因为innodb_flush_method同时指定了data files和log files的刷盘方式,这两者的刷盘方式可能不一致。 举个例子,innodb_flush_method为O_DIRECT,以O_DIRECT模式open data files,数据绕过缓存直接写入硬盘,log files仍然需要过操作系统缓冲。 image.png 注意: innodb_flush_method为O_DIRECT时 用O_DIRECT打开数据文件,那为什么还要fsync,因为需要刷新为了把directory cache和inode cache元数据也刷新到存储设备上。

日志文件还是要过文件系统缓存,所以也需要fsync。

innodb_flush_log_at_commit

控制着redo日志在commit时的刷盘行为,参考上文,因为日志仍然需要过操作系统缓冲,所以数据安全性要求高的需要设置为1,每次事务commit都把redo日志fsync到硬盘上。

innodb_flush_log_at_commit的可选值为0,1,2。

当值为0时,LOG buffer中的数据每1s写入os缓存并fsync,当值为2时,每次事务commit都把redo日志写入到os缓存,每1s再fsync到磁盘。 image.png

数据完整性

上文说Redo日志与Undo日志保证了数据的持久性和原子性,这个是Innodb层面的。因为MySQL是Server-engine架构,server层面需要使用binlog。 www.infoq.cn/article/M6g… 在同时考虑Redo日志与binlog日志时就需要保证三个方面:

  • 数据内容一致性
  • 数据顺序一致性
  • 效率

数据内容一致性

为什么?

如何做的?

  • 自动为每个事务分配一个唯一的ID(XID)。
  • COMMIT会被自动的分成Prepare和Commit两个阶段。
  • Binlog会被当做事务协调者(Transaction Coordinator),Binlog Event会被当做协调者日志。
  • 当实例从崩溃中恢复时,需要将活跃的事务从undo中提取出来,对于ACTIVE状态的事务直接回滚,对于Prepare状态的事务,如果该事务对应的binlog已经记录,则提交,否则回滚事务。

具体实现

image.png 以上的图片中可以看到,事务的提交主要分为两个主要步骤:

  1. 准备阶段(Storage Engine(InnoDB) Transaction Prepare Phase) 此时SQL已经成功执行,并生成xid信息及redo和undo的内存日志。然后调用prepare方法完成第一阶段,papare方法实际上什么也没做,将事务状态设为TRX_PREPARED,并将redo log刷磁盘。

  2. 提交阶段(Storage Engine(InnoDB)Commit Phase)

     2.1 记录协调者日志,即Binlog日志。如果事务涉及的所有存储引擎的prepare都执行成功,则调用TC_LOG_BINLOG::log_xid方法将SQL语句写到binlog(write()将binary log内存日志数据写入文件系统缓存,fsync()将binary log文件系统缓存日志数据永久写入磁盘)。此时,事务已经铁定要提交了。否则,调用ha_rollback_trans方法回滚事务,而SQL语句实际上也不会写到binlog。
    2.2 告诉引擎做commit

最后,调用引擎的commit完成事务的提交。会清除undo信息,刷redo日志,将事务设为TRX_NOT_STARTED状态。

PS:记录Binlog是在InnoDB引擎Prepare(即Redo Log写入磁盘)之后,这点至关重要。

由上面的二阶段提交流程可以看出,一旦步骤2.1中的操作完成,就确保了事务的提交,即使在执行步骤2.2时数据库发送了宕机。此外需要注意的是,每个步骤都需要进行一次fsync操作才能保证上下两层数据的一致性。步骤2的fsync参数由sync_binlog=1控制,步骤3的fsync由参数innodb_flush_log_at_trx_commit=1控制,俗称“双1”,是保证CrashSafe的根本。

数据顺序一致性

为什么?

因为mysql是多线程的,如果没有机制保证的话,在多个事务并发执行的情况下先写binlog不一定代表先在innodb中commit。 以下图Binlog与Innodb commit顺序不一致为例。 image.png 如上图,事务按照T1、T2、T3顺序开始执行,将二进制日志(按照T1、T2、T3顺序)写入日志文件系统缓冲,调用fsync()进行一次group commit将日志文件永久写入磁盘,但是存储引擎提交的顺序为T2、T3、T1。当T2、T3提交事务之后,若通过在线物理备份进行数据库备份(xtrabackup只备份Redo,不备份binlog),所以虽然在线物理备份记录的Binlog点位是T1、T2、T3都已经提交的点位,但是在恢复时因为T1在Innodb层未进行Commit,所以最终会丢失T1的数据。

如何做的

旧版本(性能差)

prepare_commit_mutex 在每次进行xa事务时,在prepare阶段事务先拿到一个全局的prepare_commit_mutex, 然后执行前面说的持久化(fsync)redo log与binlog,然后等fsync完了之后再释放prepare_commit_mutex,这样相当于串行化的效果虽然保证了binlog与redo log之间顺序一致性,但是却导致每个事务都需要一个fsync操作。

组提交

Binary Log Group Commit 组提交的目的是减少fsync的次数。 Binlog组提交的基本思想是,引入队列机制保证Innodb commit顺序与binlog落盘顺序一致。

前提

RedoLog本身就是组提交的。 2PC中的prepare阶段,会对redo进行一次刷盘操作(innodb_flush_log_at_trx_commit=1),这时候redo group commit的过程如下:

  1. 获取 log_mutex
  2. 若flushed_to_disk_lsn>=lsn,表示日志已经被刷盘,跳转5
  3. 若 current_flush_lsn>=lsn,表示日志正在刷盘中,跳转5后进入等待状态
  4. 将小于LSN的日志刷盘(flush and sync)
  5. 退出log_mutex 这个过程是根据LSN的顺序进行合并的,也就是说一次redo group commit的过程可能会讲别的未提交事务中的lsn也一并刷盘 segmentfault.com/a/119000001…
Binary Log组提交的实现
组提交整体过程
  1. binlog prepare
  2. InnoDB prepare
  3. binlog commit(ordered commit) --3.1 Stage #1: flushing transactions to binary log --3.2 Stage #2: Syncing binary log file to disk --3.3 Stage #3: Commit all transactions in order.
  4. InnoDB commit
5.6

为了提高并发性能,肯定要细化锁粒度。MySQL 5.6 引入了 binlog 的组提交(group commit)功能,prepare 阶段不变,只针对 commit 阶段,将 commit 阶段拆分为三个过程:

flush stage:多个线程按进入的顺序将 binlog 从 cache 写入文件(不刷盘); sync stage:对 binlog 文件做 fsync 操作(多个线程的 binlog 合并一次刷盘); commit stage:各个线程按顺序做 InnoDB commit 操作。 其中,每个阶段有 lock 进行保护,因此保证了事务写入的顺序。

实现方法是,在每个 stage 设置一个队列,第一个进入该队列的线程会成为 leader,后续进入的线程会阻塞直至完成提交。leader 线程会领导队列中的所有线程执行该 stage 的任务,并带领所有 follower 进入到下一个 stage 去执行,当遇到下一个 stage 为非空队列时,leader 会变成 follower 注册到此队列中。

这种组提交的优势在于锁的粒度减小,三个阶段可以并发执行,从而提升效率。

5.7优化

延迟写 redo 到 group commit 阶段

MySQL 5.6 的组提交逻辑中,每个事务各自做 prepare 并写 redo log,只有到了 commit 阶段才进入组提交,因此每个事务的 redolog sync 操作成为性能瓶颈。

在 5.7 版本中,修改了组提交的 flush 阶段,在 prepare 阶段不再让线程各自执行 flush redolog 操作,而是推迟到组提交的 flush 阶段,flush stage 修改成如下逻辑:

收集组提交队列,得到 leader 线程,其余 follower 线程进入阻塞; leader 调用 ha_flush_logs 做一次 redo write/sync,即,一次将所有线程的 redolog 刷盘; 将队列中 thd 的所有 binlog cache 写到 binlog 文件中。 这个优化是将 redolog 的刷盘延迟到了 binlog group commit 的 flush stage 之中,sync binlog 之前。通过延迟写 redolog 的方式,为 redolog 做了一次组写入,这样 binlog 和 redolog 都进行了优化。

Innobackupex为啥不需要备份Binlog?

首先看看什么时候需要binlog=>当实例从崩溃中恢复时,需要将活跃的事务从undo中提取出来,对于ACTIVE状态的事务直接回滚,对于Prepare状态的事务,如果该事务对应的binlog已经完整记录,则提交,否则回滚事务。

因此,XA Recover只对Prepare状态事务有影响。

如何做的

FLUSH TABLE WITH READ LOCK

Innobackupex中有一步是FLUSH TABLE WITH READ LOCK。 FTWRL主要包括3个步骤:

  1. 上全局读锁(lock_global_read_lock)
  2. 清理表缓存(close_cached_tables)
  3. 上全局COMMIT锁(make_global_read_lock_block_commit)。

FTWRL在备份中的作用=>阻塞新事务开启以及活跃事务提交,为获取一致性位点做准备。这个时候Redo日志与Binlog日志已经是一致的了。

FLUSH NO_WRITE_TO_BINLOG ENGINE LOGS

把Log buffer中的Redo刷到硬盘上,然后Innobackupex拷走。

segmentfault.com/a/119000001… blog.csdn.net/zbszhangbos… mysql.taobao.org/monthly/202… mysql.taobao.org/monthly/201…