暂无图片
暂无图片
1
暂无图片
暂无图片
暂无图片

MySQL-两阶段commit (two-phase commit)

原创 cap 2022-01-23
1852

由于innodb对于MySQL而言仅仅是存储引擎,在mysql server层和innodb 存储引擎层都存在这不同的日志,分别是binlog和redo log,那么一个事务的提交和回滚 肯定是要有一定顺序的,这就要统一协调binlog和redo log的记录方式和提交顺序,当然在实际情况下MySQL为了提高性能还有group commit等技术,同样也给两次commit的实现提升了难度。既然是两阶段提交,那么session发出一个commit肯定是要有两个阶段的,分别是prepare阶段和commit阶段,简单的说 prepare阶段只是通过mutex将事务xid分别写入到binlog buffer 和redo buffer中,实现commit前的准备动作,而commit阶段主要是对binlog buffer中的binlog数据写入磁盘和对redo buffer中的redo数据写入磁盘,写入成功返回commit complete。接下来详细讨论binlog 和redo log 是怎么样协调使用和提交的。

首先先讨论一下session连接 binlog 和redolog的记录方式
**1.**当一个session 发出一个连接首先是要记录redo的,从binlog记录中可以按到,会记录一个时间戳和xid,此处的xid会跟redo log、数据页、undo log中的xid相关联,以实现提交和数据恢复需要。

# at 1095
#211129 15:53:00 server id 15133306  end_log_pos 1200 CRC32 0xc5edd39a  Query   thread_id=16    exec_time=0     error_code=0    Xid = 258
SET TIMESTAMP=1638172380/*!*/;
/*!80016 SET @@session.default_table_encryption=0*//*!*/;

**2.**此时若session发出一个insert动作,那么发出指令的顺序是session–>server–>engine,指令必然也会在不同层留下记录。
①先记录binlog,首先记录进入server层的时间,然后insert操作,记录操作对应的内容和xid。

# at 1778
#211129 15:56:19 server id 15133306  end_log_pos 1852 CRC32 0x2aa5d962  Query   thread_id=16    exec_time=0     error_code=0
SET TIMESTAMP=1638172579/*!*/;
BEGIN
/*!*/;
# at 1852
#211129 15:56:19 server id 15133306  end_log_pos 1910 CRC32 0x9996dafb  Table_map: `cap`.`cap_tab` mapped to number 164
# at 1910
#211129 15:56:19 server id 15133306  end_log_pos 1955 CRC32 0x39301e35  Write_rows: table id 164 flags: STMT_END_F
### INSERT INTO `cap`.`cap_tab`
### SET
###   @1=2
###   @2='lisi'
# at 1955
#211129 15:56:19 server id 15133306  end_log_pos 1986 CRC32 0xee8ae974  Xid = 267 

②然后到达engine层,实际修改数据页,记录redo log,同样会在redo 中记录xid。(对于事务会使用响应的undo log,在构造前镜像的同时,也会记录一个时间戳,LSN和XID,此处不做讨论)

当上面一系列操作执行完成之后,就是session commit(MySQL自动提交)时刻,session commit 到mysql中就是两步大的动作,分别是prepare和commit,即两阶段提交。

两阶段提交

1.prepare阶段

为了保证commit的一致性,MySQL使用BLGC(binary log group commit)方式实现了在保证日志的一致性情况下还实现了binlog 和redo的group commit,关于BLGC的实现方式可见下图,MySQL数据库进行commit时首先按照顺序将其放入一个队列中,队列中第一个事务为leader,其他事务为follower,leader控制着follower的行为。

image.png
前面有过简单的讨论,prepare主要是对事物中的变更操作(binlog中是SQL语句,redo log中是该事物变更操作的偏移量)写入相应的buffer,那么prepare阶段要做的就是上图中的flush操作,一旦事务语句执行成功就对以排队的方式进入flush队列,跟随leader事务。

2.commit阶段

正如上边所述,这一些列的操作都是一个核心–“保证binlog 和redo log一致性”

①binlog写入–sync阶段

那么这个阶段首先第一步就是写入binlog日志,这一步的写入方式由sync_binlog参数控制,即图中的sync阶段,在flush阶段完成后,leader事务带领着队列里所有事务进入sync阶段,一次io将队列里所有事务的binlog一次性的又binlog buffer写入binlog文件中,并标记commit标识。此动作完成就认为事务完成

②redo log写入–commit阶段

接下来一步就是写redo了,这里的commit是对存储引擎而言的,同样也是将leader事务带领的一对事务在redo log buffer中redo信息接入redo log中,并标记commit标识,这里的写入方式由innodb_flush_log_at_trx_commit参数控制。写入完成返回session commit ok。两阶段提交完成。

image.png

数据恢复

在数据库crash时都会面临一个问题,正在commit或者已经commit没有来得及写入redo log的数据怎么恢复,那么两阶段提交能很好的保证已提交的数据不丢失。MySQL中每次启动无论上次是否正常关闭,都会进行读取redo进行数据恢复,这个恢复主要是比对redo maxLSN 是否大于数据页LSN,若大,则从需要恢复的数据页LSN位置开始顺序读取redo,因为是物理顺序读,这个recover是十分快的,恢复此LSN后再去循环检查,实际上innodb源码中使用while循环实现的。正如两阶段提交以binlog记录顺序为事务顺序依据一样,MySQL启动recover恢复事务也是以binlog为依据的,binlog有的认为提交了,没有的则认为没有提交成功,该回滚的回滚,这也就是上面讨论到的“事务写入binlog成功就会认为事务完成”。每一次recover都会先从读取binlog开始,同时MySQL两阶段提交是一个原子机制,是通过分布式(XA)机制保证binlog和innodb的一致,即使出现XA事务的crash,也可以进行事务合理的前滚和回滚

1.没有commit数据库crash

数据库没有commit,binlog没有该事物信息,毋庸置疑数据库会回滚,通过binlog、redo log 和undo log记录的xid进行回滚。

2.数据库commit但还没写入binlog

数据库还没有成功写入binlog,那么commit就没有成功,数据回滚。

3.数据库commit写入redo log

数据库已经写入了binlog,此时对于数据库而言事务已经提交了,那么在恢复的时候需要binlog中记录的xid和数据页、redo log、undo log中的xid来恢复已提交的数据

「喜欢这篇文章,您的关注和赞赏是给作者最好的鼓励」
关注作者
【版权声明】本文为墨天轮用户原创内容,转载时必须标注文章的来源(墨天轮),文章链接,文章作者等基本信息,否则作者和墨天轮有权追究责任。如果您发现墨天轮中有涉嫌抄袭或者侵权的内容,欢迎发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论