4.Update执行流程之两阶段提交


highlight: arduino-light

Update执行流程之两阶段提交

下面以一条更新语句: update t set c=c+1 where id=2 为例说明update语句执行流程,图中浅色框表示是在InnoDB内部执行,深色框表示是在执行器中执行。建立连接、解析器、优化器等执行过程略。 image.png

1.执行器先找引擎取id=2这一行。id是主键,引擎直接用B+树搜索找到这一行。如果id=2这一行所在的数据页本来就在内存bufferpool中,就直接返回给执行器;否则,需要先从磁盘读入内存bufferpool,然后再返回,此时会对这行记录加独占锁。

2.写入undolog,保证数据可以回滚。

3.执行器拿到引擎给的行数据,把这个值加上1,比如原来是N,现在就是N+1,得到新的一行数据,引擎将这行新数据更新到内存bufferpool中(脏页)。

4.同时将这个更新操作记录到redo log buffer里面,此时redo log处于prepare状态。然后告知执行器执行完成了,随时可以提交事务。

4.执行器生成这个操作的binlog,并把binlog写入磁盘。

5.执行器调用引擎的提交事务接口,引擎把刚刚写入的redo log改成提交(commit)状态,更新完成。此时会把本次更新对应的 binlog 文件名称和这次更新的 binlog 文件里的位置,写入到 redo log 文件中,同时在 redo log 文件里写入一个 commit 标记。

6.如果触发刷新脏页的操作,则将内存更新后的脏数据刷回磁盘。

什么是两阶段提交

在MYSQL的InnoDB存储引擎中,如果开启了binlog情况下,MYSQL会同时维护binlog和InnoDB中的redo log,为了保证这两个日志的一致性问题,它使用了内部XA事务解决。

内部XA事务是由binlog作为协调者,redo log 作为参与者。

MySQL 内部开启一个 XA 事务,分两阶段来完成 XA 事务的提交,如下图:

image.png

从两个阶段提交定义和上图可以知道,我们是把redo log 拆分为两个步骤:prepare 和 commit,而在这中间加入binlog的写入,具体如下:

  1. prepare 阶段:将 XID(内部 XA 事务的 ID) 写入到 redo log,同时将 redo log 对应的事务状态设置为 prepare,然后将 redo log 刷新到硬盘
  2. commit 阶段:把 XID 写入到 binlog,然后将 binlog 刷新到磁盘,接着调用引擎的提交事务接口,将 redo log 状态设置为 commit

如果在两个阶段出现异常怎么处理? image.png

上图解释说明:

1.时刻A之前:redo log 和 binlog 都没有写入

2.时刻A:redo log 写入,binlog没有写入

3.时刻B:redo log 和binlog都写入,但没有提交commit标识。

不管在时刻A,还是时刻B, redo log 都是处于 prepare 阶段。

image.png

上图就是MySQL重启后恢复数据的流程:

第一种情况:时刻A之前,此时redo log 和 binlog 都没有写入,写入状态非prepare,mysql重启不会恢复,主从数据一致。

第二种情况:mysql重启之后会按照顺序读取redo log 文件,当碰到处于prepare阶段的redo log 文件,就会拿着redo log 的XID去binlog中寻找,看是否有该XID:

1.如果binlog中没有该XID,那么就说明redo log 刷盘完成,但binlog还没有,则回滚事务,对应时刻A。

2.如果binlog中有该XID,那么就说明redo log 和binlog都刷盘完成,但commit标识还没有提交,则提交事务,对应时刻B。

所以对于处于 prepare 阶段的 redo log,即可能提交事务,也可能回滚事务。

这取决于是否能在 binlog 中查找到与 redo log 相同的 XID,如果有就提交事务,如果没有就回滚事务。

这样就可以保证 redo log 和 binlog 这两份日志的一致性了。

总结:两阶段提交是以 binlog 写成功为事务提交成功的标识,因为 binlog 写成功了,就意味着能在 binlog 中查找到与 redo log 相同的 XID。

为什么需要两阶段提交

先说结论:为了保证主从数据一致性。

redo log影响主库的数据,binlog影响从库的数据。所以redo log和binlog必须保持一致才能保证主从数据一致。

如果不使用2阶段提交,数据库的状态就有可能和用它的binlog恢复出来的库的状态不一致。

简单说,redo log和binlog都可以用于表示事务的提交状态,而两阶段提交就是让这两个状态保持逻辑上的一致。

如果redo log持久化并进行了提交,而binlog未持久化数据库就crash了,则从库从binlog拉取数据会少于主库,造成不一致,此时需要回滚。

因此需要内部事务来保证两种日志的一致性。

相信很多有过开发经验的同学都知道分布式事务,这里的redo log和binlog其实就是很典型的分布式事务场景,因为两者本身就是两个独立的个体,要想保持一致,就必须使用分布式事务的解决方案来处理。而将redo log分成了两步,其实就是使用了两阶段提交协议(Two-phase Commit,2PC)。

下面对更新语句的执行流程进行简化,看一下MySQL的两阶段提交是如何实现的:

开始事务--->undolog ---> redolog处于prepare阶段 ----> binlog ---> redolog commit--->commoit提交事务

首先区分一个概念,这里的commit是属于begin transaction…commit语句中的一个步骤,且是最后一个步骤,两个commit是包含的关系。

从图中可看出,事务的提交过程有两个阶段,就是将redo log的写入拆成了两个步骤:prepare和commit,中间再穿插写入binlog。

如果这时候你很疑惑,为什么一定要用两阶段提交呢,如果不用两阶段提交会出现什么情况?

比如先写redo log,再写binlog或者先写binlog,再写redo log不行吗?

我们继续用update t set c=c+1 where id=2这个例子。

假设id=2这一条数据的c初始值为0。

第一种情况:redo log写完,binlog还没有写完的时候,MySQL进程异常重启。

由于redo log已经写完了,系统重启后会通过redo log将数据恢复回来,所以恢复后这一行c的值是1。但是由于binlog没写完就crash了,这时候binlog里面就没有记录这个语句。因此,不管是现在的从库还是之后通过这份binlog还原临时库都没有这一次更新,c的值还是0,与原库的值不同。

第二种情况:binlog写完,redo log还没有写完的时候,MySQL进程异常重启。

同理,如果先写binlog,再写redo log,中途系统crash了,也会导致主从不一致。

由于binlog写完才crash,这时候binlog里面有记录这个语句。但是由于redo log还没有写完,导致主库中没有这条数据。所以这个时候会出现主库数据和从库中的数据不一致的情况。

所以将redo log分成两步写,即两阶段提交,才能保证redo log和binlog内容一致,从而保证主从数据一致。

两阶段提交带来的问题

两阶段提交解决两个日志一致性,那它会不会带来新的问题呢? 虽然两阶段提交是解决了两个日志数据一致性问题,但是它也带来了一定性能问题:

1.磁盘 I/O 次数高:对于“双1”配置(syncbinlog 和 innodbflushlogattrxcommit 都配置为 1),每个事务提交都会进行两次 fsync(刷盘),一次是 redo log 刷盘,另一次是 binlog 刷盘

2.锁竞争激烈:两阶段提交虽然能够保证「单事务」两个日志的内容一致,但在「多事务」的情况下,却不能保证两者的提交顺序一致,因此,在两阶段提交的流程基础上,还需要加一个锁来保证提交的原子性,从而保证多事务的情况下,两个日志的提交顺序一致。

锁竞争激烈

两阶段提交虽然能够保证单事务两个日志的内容一致,但在多事务的情况下,却不能保证两者的提交顺序一致,比如下面这个例子,假设现在有3个事务同时提交:

T1 (--prepare--binlog---------------------commit)

T2 (-----prepare-----binlog----commit)

T3 (--------prepare-------binlog------commit)

时序:

redo log prepare的顺序:T1 ---->T2 ---->T3

binlog的写入顺序:T1 ---->T2 ---->T3

redo log commit的顺序:T2 ----> T3 ---->T1

结论:由于binlog写入的顺序和redo log提交结束的顺序不一致,导致binlog和redo log所记录的事务提交结束的顺序不一样,最终导致的结果就是主从数据不一致。

因此,在两阶段提交的流程基础上,还需要加一个锁来保证提交的原子性,从而保证多事务的情况下,两个日志的提交顺序一致。

所以在早期的MySQL版本中为了解决了顺序一致性的问题是通过加锁来处理的,通过使用preparecommitmutex锁来保证事务提交的顺序,在一个事务获取到锁时才能进入prepare,一直到commit结束才能释放锁,下个事务才可以继续进行prepare操作,一直到 commit 阶段结束才能释放锁,下个事务才可以继续进行 prepare 操作。

但是在并发量很大的时候,会导致锁竞争激烈,而性能低下。

磁盘 I/O 次数高

通过加锁虽然完美地解决了顺序一致性的问题,但在并发量较大的时候,就会导致对锁的争用,性能不佳。除了锁的争用会影响到性能之外,还有一个对性能影响更大的点,就是每个事务提交都会进行两次fsync(写磁盘),一次是redo log落盘,另一次是binlog落盘。大家都知道,写磁盘是昂贵的操作,对于普通磁盘,每秒的QPS大概也就是几百。

因为redo log 和binlog 都是存在对应的缓存里,即redo log缓存在redo log buffer,binlog缓存在 binlog cache中,而持久化它们是各自通过参数来控制的。一般为了数据不会丢失,都会设置这两个参数为1:

当 sync_binlog = 1 的时候,表示每次提交事务都会将 binlog cache 里的 binlog 直接持久到磁

当 innodbflushlogattrx_commit = 1 时,表示每次事务提交时,都将缓存在 redo log buffer 里的 redo log 直接持久化到磁盘

这两个参数都设置为1,就是“双1”配置。那么当有事务提交的时候,至少就是刷两个磁盘(分别是 redo log 刷盘和binlog 刷盘),所以就导致了性能问题。

  • 在没有开启binlog时

redo log的刷盘操作将会是最终影响MySQL TPS的瓶颈所在。为了缓解这一问题,MySQL使用了组提交,将多个刷盘操作合并成一个,如果说10个事务依次排队刷盘的时间成本是10,那么将这10个事务一次性一起刷盘的时间成本则近似于1。

  • 当开启binlog时

为了保证Redo log和binlog的数据一致性,MySQL使用了二阶段提交,由binlog作为事务的协调者。而 引入二阶段提交 使得binlog又成为了性能瓶颈,先前的Redo log 组提交 也成了摆设。为了再次缓解这一问题,MySQL增加了binlog的组提交,目的同样是将binlog的多个刷盘操作合并成一个,结合Redo log本身已经实现的 组提交,分为三个阶段(Flush 阶段、Sync 阶段、Commit 阶段)完成binlog 组提交,最大化每次刷盘的收益,弱化磁盘瓶颈,提高性能。

组提交

总结:为了解决两阶段提交性能问题,在MySQL 5.6 就引入了binlog组提交,即BLGC(Binary Log Group Commit)。binlog组提交的基本思想是,就是当有多个事务提交时,引入队列机制保证InnoDB commit顺序与binlog落盘顺序一致,并将事务分组把多个binlog 刷盘操作合并成一个,从而减少磁盘 I/O 的次数。组内的binlog刷盘动作交给一个事务进行,实现组提交目的。

除了undolog的组提交还有redolog的组提交。

参考:https://blog.csdn.net/cymm_liu/article/details/106030636

参考:https://blog.csdn.net/sanylove/article/details/127576237

参考:https://time.geekbang.org/column/intro/139

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值