zoukankan      html  css  js  c++  java
  • MySQL事务提交过程

    一、MySQL事务提交过程(一)

    MySQL作为一种关系型数据库,已被广泛应用到互联网中的诸多项目中。今天我们来讨论下事务的提交过程。

    由于mysql插件式存储架构,导致开启binlog后,事务提交实质是二阶段提交,通过两阶段提交,来保证存储引擎和二进制日志的一致。

    此目录节点只讨论binlog未打卡状态下的提交流程,后续会讨论打开binlog选项后的提交逻辑。

    测试环境

    OS:WIN7

    ENGINE:

    bin-log:off

    DB:

    测试条件

    set autocommit=0;
    -- ----------------------------
    
    -- Table structure for `user`
    
    -- ----------------------------
    
    DROP TABLE IF EXISTS `user`;
    
    CREATE TABLE `user` (
    
    `id` int(20) NOT NULL,
    
    `account` varchar(20) NOT NULL,
    
    `name` varchar(20) NOT NULL,
    
    PRIMARY KEY (`id`),
    
    KEY `id` (`id`) USING BTREE,
    
    KEY `name` (`name`) USING BTREE
    
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8;

    测试语句

    insert into user values(1, 'sanzhang', '张三');
    commit;

    一般常用的DML:Data Manipulation Language 数据操纵语言,对表的数据进行操作,(insert、update、delete )语句

    和 DCL:Data Control Language 数据库控制语言(创建用户、删除用户、授权、取消授权)语句

    和 DDL:Data Definition Language 数据库定义语言,对数据库内部的对象进行创建、删除、修改的操语句,

    均是使用MySQL提供的公共接口mysql_execute_command,来执行相应的SQL语句。我们来分析下mysql_execute_command接口执行的流程:

    mysql_execute_command
    
    {
    
       switch (command)
    
       {
    
           case SQLCOM_INSERT:
    
                    mysql_insert();
    
                    break;
    
           case SQLCOM_UPDATE:
    
                    mysql_update();
    
                    break;
    
           case SQLCOM_DELETE:
    
                    mysql_delete();
    
                    break;
    
           ......
    
       }
    
       if thd->is_error()  //语句执行错误
    
         trans_rollback_stmt(thd);
    
      else
    
        trans_commit_stmt(thd);
    
    }

    从上述流程中,可以看到执行任何语句,最后都会执行trans_rollback_stmt或者trans_commit_stmt,这两个分别是语句回滚和语句提交。

    语句提交,对于非自动模式下,主要有两个作用:

    1、释放autoinc锁,这个锁主要用来处理多个事务互斥的获取自增序列。因此,无论最后执行的是语句提交还是语句回滚,该资源都是需要立马释放掉的。

    2、标识语句在事务中的位置,方便语句级回滚。执行commit后,可以进入commit流程。

    现在看下具体的事务提交流程:

    mysql_execute_command
    
    trans_commit_stmt
    
    ha_commit_trans(thd, FALSE);
    
    {
    
        TC_LOG_DUMMY:ha_commit_low
    
            ha_commit_low()   
    
                innobase_commit
    
                {
    
                    //获取innodb层对应的事务结构
    
                    trx = check_trx_exists(thd);
    
                    if(单个语句,且非自动提交)
    
                    {
    
                         //释放自增列占用的autoinc锁资源
    
                         lock_unlock_table_autoinc(trx);
    
                         //标识sql语句在事务中的位置,方便语句级回滚
    
                         trx_mark_sql_stat_end(trx);
    
                    }
    
                    else 事务提交
    
                    {
    
                         innobase_commit_low()
    
                         {  
    
                            trx_commit_for_mysql();
    
                                <span style="color: #ff0000;">trx_commit</span>(trx); 
    
                         }
    
    //确定事务对应的redo日志是否落盘【根据flush_log_at_trx_commit参数,确定redo日志落盘方式】
    
                        trx_commit_complete_for_mysql(trx);
    
    trx_flush_log_if_needed_low(trx->commit_lsn);
    
    log_write_up_to(lsn);
    
                    }
    
                }
    
    }
    trx_commit
    
        trx_commit_low
    
            {
    
                trx_write_serialisation_history
    
                {
    
                    trx_undo_update_cleanup //供purge线程处理,清理回滚页
    
                }
    
                trx_commit_in_memory
    
                {
    
                    lock_trx_release_locks //释放锁资源
    
                    trx_flush_log_if_needed(lsn) //刷日志
    
                    trx_roll_savepoints_free //释放savepoints
    
                }
    
            }

    MySQL是通过WAL方式,来保证数据库事务的一致性和持久性,即ACID特性中的C(consistent)和D(durability)。

    WAL(Write-Ahead Logging)是一种实现事务日志的标准方法,具体而言就是:

    1、修改记录前,一定要先写日志;

    2、事务提交过程中,一定要保证日志先落盘,才能算事务提交完成。

    通过WAL方式,在保证事务特性的情况下,可以提高数据库的性能。

       

    从上述流程可以看出,提交过程中,主要做了4件事情,

    1、清理undo段信息,对于innodb存储引擎的更新操作来说,undo段需要purge,这里的purge主要职能是,真正删除物理记录。在执行delete或update操作时,实际旧记录没有真正删除,只是在记录上打了一个标记,而是在事务提交后,purge线程真正删除,释放物理页空间。因此,提交过程中会将undo信息加入purge列表,供purge线程处理。

    2、释放锁资源,mysql通过锁互斥机制保证不同事务不同时操作一条记录,事务执行后才会真正释放所有锁资源,并唤醒等待其锁资源的其他事务;

    3、刷redo日志,前面我们说到,mysql实现事务一致性和持久性的机制。通过redo日志落盘操作,保证了即使修改的数据页没有即使更新到磁盘,只要日志是完成了,就能保证数据库的完整性和一致性;

    4、清理保存点列表,每个语句实际都会有一个savepoint(保存点),保存点作用是为了可以回滚到事务的任何一个语句执行前的状态,由于事务都已经提交了,所以保存点列表可以被清理了。

    关于mysql的锁机制,purge原理,redo日志,undo段等内容,其实都是数据库的核心内容。

    MySQL 本身不提供事务支持,而是开放了存储引擎接口,由具体的存储引擎来实现,具体来说支持 MySQL 事务的存储引擎就是 InnoDB。

    存储引擎实现事务的通用方式是基于 redo logundo log

    简单来说,redo log 记录事务修改后的数据, undo log 记录事务前的原始数据。

    所以当一个事务执行时实际发生过程简化描述如下:

    1. 先记录 undo/redo log,确保日志刷到磁盘上持久存储。
    2. 更新数据记录,缓存操作并异步刷盘。
    3. 提交事务,在 redo log 中写入 commit 记录。

    在 MySQL 执行事务过程中如果因故障中断,可以通过 redo log 来重做事务或通过 undo log 来回滚,确保了数据的一致性。

    这些都是由事务性存储引擎来完成的,但 binlog 不在事务存储引擎范围内,而是由 MySQL Server 来记录的。

    那么就必须保证 binlog 数据和 redo log 之间的一致性,所以开启了 binlog 后实际的事务执行就多了一步,如下:

    1. 先记录 undo/redo log,确保日志刷到磁盘上持久存储。
    2. 更新数据记录,缓存操作并异步刷盘。
    3. 将事务日志持久化到 binlog。
    4. 提交事务,在 redo log 中写入commit记录。

    这样的话,只要 binlog 没写成功,整个事务是需要回滚的,而 binlog 写成功后即使 MySQL Crash 了都可以恢复事务并完成提交。

    要做到这点,就需要把 binlog 和事务关联起来,而只有保证了 binlog 和事务数据的一致性,才能保证主从数据的一致性。

    所以 binlog 的写入过程不得不嵌入到纯粹的事务存储引擎执行过程中,并以内部分布式事务(xa 事务)的方式完成两阶段提交。

    二、MySQL事务提交过程(二)

    前一章节我们介绍了在关闭binlog的情况下,事务提交的大概流程。之所以关闭binlog,是因为开启binlog后事务提交流程会变成两阶段提交,这里的两阶段提交并不涉及分布式事务,当然mysql把它称之为内部xa事务(Distributed Transactions),与之对应的还有一个外部xa事务。

    这里所谓的两阶段提交分别是prepare阶段和commit阶段。

    内部xa事务主要是mysql内部为了保证binlog与redo log之间数据的一致性而存在的,这也是由其架构决定的(binlog在mysql层,而redo log 在存储引擎层);

    外部xa事务则是指支持多实例分布式事务,这个才算是真正的分布式事务。

    既然是xa事务,必然涉及到两阶段提交,对于内部xa而言,同样存在着提交的两个阶段。

    下文会结合源码详细解读内部xa的两阶段提交过程,以及各种情况下,mysqld crash后,mysql如何恢复来保证事务的一致性。

    测试环境在前章节的基础上加了:

    配置文件参数:

    log-bin=D:mysqllog5-6-21mysql-bin
    
    binlog_format=ROW
    
    set autocommit=0;
    
    innodb_support_xa=1
    
    sync_binlog=1;
    
    innodb_flush_log_at_trx_commit=1

    【innodb_flush_log_at_trx_commit=1,sync_binlog=1

    不同的模式区别在于,写文件调用write和落盘fsync调用的频率不同,所导致的后果是mysqld 或 os crash后,不严格的设置可能会丢失事务的更新。

    双一模式是最严格的模式,这种设置情况下,单机在任何情况下不会丢失事务更新。】

    prepare阶段:

        1.设置undo state=TRX_UNDO_PREPARED; //trx_undo_set_state_at_prepare调用

        2.刷事务更新产生的redo日志;【步骤1产生的redo日志也会刷入】

    MYSQL_BIN_LOG::prepare
    
    ha_prepare_low
    
        {
    
    engine:
    
    binlog_prepare
    
    innobase_xa_prepare
    
    mysql:
    
    trx_prepare_for_mysql
    
    {
    
                    1.trx_undo_set_state_at_prepare    //设置undo段的标记为TRX_UNDO_PREPARED
    
                    2.设置事务状态为TRX_STATE_PREPARED
    
                    3.trx_flush_log_if_needed  //将产生的redolog刷入磁盘
    
                }
    
         }

    commit阶段:

       1.将事务产生的binlog写入文件,刷入磁盘;

       2.设置undo页的状态,置为TRX_UNDO_TO_FREE或TRX_UNDO_TO_PURGE;  // trx_undo_set_state_at_finish调用

       3.记录事务对应的binlog偏移,写入系统表空间; //trx_sys_update_mysql_binlog_offset调用

    MYSQL_BIN_LOG::commit
    
        ordered_commit
    
       {
    
    1.FLUSH_STAGE
    
            flush_cache_to_file  //  刷binlog
    
    2.SYNC_STAGE
    
            sync_binlog_file    //Call fsync() to sync the file to disk.
    
    3.COMMIT_STAGE
    
            ha_commit_low
    
            {
    
                binlog_commit
    
                innobase_commit   
    
                    trx_commit(trx) 
    
                    {
    
                        trx_write_serialisation_history(trx, mtr);  //更新binlog位点,设置undo状态
    
                        trx_commit_in_memory(trx, lsn); //释放锁资源,清理保存点列表,清理回滚段
    
                    }        
    
            } 
    
        }

    在任何情况下(机器掉电)mysqld crash或者os crash,MySQL仍然能保证数据库的一致性。数据的一致性是如何做到的哪?正是二阶段提交。

    我们结合几种场景来分析下二阶段提交是如何做到的:

    1.prepare阶段,redo log落盘前,mysqld crash

    2.prepare阶段,redo log落盘后,binlog落盘前,mysqld crash

    3.commit阶段,binlog落盘后,mysqld crash

    对于第一种情况,由于redo没有落盘,毫无疑问,事务的更新肯定没有写入磁盘,数据库的一致性受影响;

    对于第二种情况,这时候redo log写入完成,但binlog还未写入,事务处于TRX_STATE_PREPARED状态,这是提交还是回滚呢?

    对于第三种情况,此时,redo log和binlog都已经落盘,只是undo状态没有更新,虽然redo log和binlog已经一致了,事务是否应该提交?

       

    我们结合mysqld异常重启后的执行逻辑以及关键的源代码。

    对于第三种情况,我们可以搜集到未提交事务的binlog event,所以需要提交;

    对于第二种情况,由于binlog未写入,需要通过执行回滚操作来保证数据库的一致性。

    异常重启后,如何判断事务该提交还是回滚

    1.读binlog日志,获取崩溃时没有提交的event;  //info->commit_list中含有该元素

    2.若存在,则对应的事务要提交;否则需要回滚。

    判断事务提交或回滚源码如下:

    上面讨论了两阶段提交的基本流程,以及服务器异常crash后,mysql如何重启恢复保证binlog和数据的一致性。

    简而言之,对于异常的xa事务,若binlog已落盘,则事务应该提交;binlog未落盘,则事务就应该回滚。

    //异常重启后,回滚流程

    innobase_rollback_by_xid
    
    rollback_by_xid
    
    trx_rollback_resurrected
    
        trx_rollback_active
    
            row_undo
    
            {
    
                //从回滚页获取undo记录
    
                //分析undo记录类型
    
                if (insert)
    
                    row_undo_ins
    
                else
    
                    row_undo_mod
    
            }

    //异常重启后,提交流程

    commit_by_xid
    
    trx_commit_for_mysql

    //写binlog接口

    handler.cc:binlog_log_row
    
    sql/binlog.cc:commit
    
    mysys/my_sync:my_sync
    
    sql/binlog.cc:sync_binlog_file
    
    handler/ha_innodb.cc:innobase_xa_prepare

    binlog日志文件是为了解决MySQL主从复制功能而引入的一份新日志文件,它包含了引发数据变更的事件日志集合。

    从库请求主库发送 binlog 并通过日志事件还原数据写入从库,所以从库的数据来源为 binlog。

    这样 MySQL 主库只需做到 binlog 与本地数据一致就可以保证主从库数据一致(暂且忽略网络传输引发的主从不一致)。

    本文整理自:

    https://www.cnblogs.com/exceptioneye/p/5451960.html

    https://www.cnblogs.com/exceptioneye/p/5451976.html

  • 相关阅读:
    因式分解
    插入排序算法
    小技巧(杂乱篇章)
    错误的模糊应用(类继承问题)
    同源策略和跨域解决方案
    Django admin源码剖析
    Python中该使用%还是format来格式化字符串?
    Django的认证系统
    Django中间件
    Django form表单
  • 原文地址:https://www.cnblogs.com/better-farther-world2099/p/9290580.html
Copyright © 2011-2022 走看看