1

概述

文章https://segmentfault.com/a/11... 中介绍了存储应该考虑的方向。本文详细介绍其中的mysq,主要是INNODB。整体架构,启动流程,一条语句的执行过程带你快速深入mysql源码。再从性能(缓存,数据结构),功能(ACID实现,索引)如何实现介绍了mysql中核心点。第二部分为分布式,介绍原生mysql的同步过程。第三部分是proxy,因为proxy多数会自研,只介绍proxy应该包含的功能。

关键词:innodb,MVCC,ACID实现,索引,主从同步,proxy

第一章 整体架构/流程

clipboard.png
mysql为单进程多线程,因为元数据用Innodb保存,启动后除了mysql的处理连接请求/超时等还会启动Innodb的所有线程。
主流程:

主函数在sql/amin.cc中
调用Mysqld.cc中mysqld_main

 1. 首先载入日志,信号注册,plugin_register
    (mysql是插件式存储引擎设计,innodb,myisam等都是插件,在这里注册),核心为mysqld_socket_acceptor->connection_event_loop();

 2. 监听处理循环poll。
    process_new_connection处理handler有三种:线程池方式只用于商业,一个线程处理所有请求,一个连接一个线程(大多数选择Per_thread_connection_handler)。
 3. 若thread_cache中有空闲直接获取,否则创建新的用户线程。进入用户线程的handle_connection
    3.1 mysql网络通信一共有这几层:`THD` | Protocol | NET | VIO | SOCKET,protocol对数据的协议格式化,NET封装了net buf读写刷到网络的操作,VIO是对所有连接类型网络操作的一层封装(TCP/IP, Socket, Name Pipe, SSL, SHARED MEMORY),handle_connection初始化THD(线程),开始do_command   (关于THD,有个很好的图:http://mysql.taobao.org/monthly/2016/07/04/)
    3.2.do_command=>dispatch_comand=>mysql_parse=》【检查query_cache有缓存直接返回否则=》】parse_sql=》mysql_execute_cmd判断insert等调用mysql_insert,每条记录调用write_record,这个是各个引擎的基类,根据操作表类型调用引擎层的函数=》写binlog日志=》提交/回滚。注意大家可能都以为是有query_cache的。但是从8.0开启废弃了query_cache。第二正会讲一下
 4. 除了用户线程和主线程,在启动时,还创建了timer_notify线程。由于为了解决DDL无法做到atomic等,从MySQL8.0开始取消了FRM文件及其他server层的元数据文件(frm, par, trn, trg, isl,db.opt),所有的元数据都用InnoDB引擎进行存储, 另外一些诸如权限表之类的系统表也改用InnoDB引擎。因此在加载这些表时,创建了innodb用到的一系列线程。

从插入流程开始

整体流程图如下:
clipboard.png

必须有这些步骤的原因:
clipboard.png
[1]为了快,所有数据先写入内存,再刷脏
[2]为了防止数据页写过程中崩溃数据的持久性=》先写redo保证重启后可以恢复。日志写不成功不操作,日志是顺序写,内容少,可以同步等。(最好是物理重做)。
[3]异常回滚=》物理回滚反解复杂,需要一个逻辑日志。
基于undo log又实现了MVCC
unlog等也要保证操作持久化原子化。
[4]为了删除不每次整理页,只标记,为了真正删除/undo不需要的清除=》purge
[5]flush对一个pageid多次操作合并在一起减少随机操作=》二级索引非唯一change buff
[6]Flush过程中一个页部分写成功就崩溃,无法正确后恢复=》二次写
[7]为完整的主链路。
[8]为异步的刷盘链路

详细步骤:

  1. 外层 handle_connection=>do_commannd=>dispatch_command=>mysql_parse=>mysql_execute_commannd=>sql_cmd_dml::execute=> execute_inner while{对每条记录执行write_record} =>ha_write_row【返回到这里不出错记录binlog】,调用引擎table->file->ha_write_row(table->record[0])
  2. 引擎层:
    row_insert_for_mysql_using_ins_graph开始,有开启事务的操作,trx_start_low。
    首先,需要分配回滚段,因为会修改数据,就需要找地方把老版本的数据给记录下来,其次,需要通过全局事务id产生器产生一个事务id,最后,把读写事务加入到全局读写事务链表(trx_sys->rw_trx_list),把事务id加入到活跃读写事务数组中(trx_sys->descriptors)
    在InnoDB看来所有的事务在启动时候都是只读状态,只有接受到修改数据的SQL后(InnoDB接收到才行。因为在start transaction read only模式下,DML/DDL都被Serve层挡掉了)才调用trx_set_rw_mode函数把只读事务提升为读写事务。
    之后开始事务内部处理。图中所示,细节很多,先不写了。

第二章 性能

磁盘,B+树

  • 准确的B+比B为何更适合?
    区别两点,一个是B树搜索是可以止于非页节点的,包含数据(包含数据在磁盘中页的位置),且数据只出现在树中一次。另一点是叶子节点有双向链表。第一点使得节点可以包含更多路(因为不存数据在磁盘中页的位置,只包含下一层的指针页位置,B树这两个都要包含),层高会更少;只能到页节点搜索结束,性能稳定。第二点为了扫描和范围索引。

    clipboard.png

    clipboard.png

内存buffer

所有数据页。都走这套。包括undo等

name desc
buf_pool_t::page_hash page_hash用于存储已经或正在读入内存的page。根据<space_id, page_no>快速查找。当不在page hash时,才会去尝试从文件读取
buf_pool_t::LRU LRU上维持了所有从磁盘读入的数据页,该LRU上又在链表尾部开始大约3/8处将链表划分为两部分,新读入的page被加入到这个位置;当我们设置了innodb_old_blocks_time,若两次访问page的时间超过该阀值,则将其挪动到LRU头部;这就避免了类似一次性的全表扫描操作导致buffer pool污染
buf_pool_t::free 存储了当前空闲可分配的block
buf_pool_t::flush_list 存储了被修改过的page,根据oldest_modification(即载入内存后第一次修改该page时的Redo LSN)排序
buf_pool_t::flush_rbt 在崩溃恢复阶段在flush list上建立的红黑数,用于将apply redo后的page快速的插入到flush list上,以保证其有序
buf_pool_t::unzip_LRU 压缩表上解压后的page被存储到unzip_LRU。 buf_block_t::frame存储解压后的数据,buf_block_t::page->zip.data指向原始压缩数据。
buf_pool_t::zip_free[BUF_BUDDY_SIZES_MAX] 用于管理压缩页产生的空闲碎片page。压缩页占用的内存采用buddy allocator算法进行分配。

page_hash查找。
LRU只是用于淘汰。一份block。指针保存在hash和lru上(所有的数据页)
flush_list 修改过的block被加到flush_list上,
unzip_LRU 解压的数据页被放到unzip_LRU链表上。

  • cache过程:
    当一个线程请求page时,首先根据space id 和page no找到对应的buffer pool instance。然后查询page hash。如果看到page hash中已经有对应的block了,说明page已经或正在被读入buffer pool,如果io_fix为BUF_IO_READ,说明正在进行IO,就通过加X锁的方式做一次sync(buf_wait_for_read),确保IO完成。

    如果没有则表示需要从磁盘读取。在读盘前首先我们需要为即将读入内存的数据页分配一个空闲的block。当free list上存在空闲的block时,可以直接从free list上摘取;如果没有,就需要从unzip_lru 或者 lru上驱逐page。先unzip lru。再lru是否有可替换page,直接释放,否则可能是脏页多,再线程在LRU上做脏页刷新。后台线程也会定期做脏页刷新。

    一个流程对buffer的操作步骤:
    clipboard.png

内存总体流程

clipboard.png

索引:聚簇索引

见上B+树

二级索引 change buffer

对非唯一二级索引页,delete_mark,delete,insert顺序插入缓冲区,合并减少随机IO。

  • 物理:ibdata第4个page B+ Tree(key:spaceid,offset,counter)
  • 内存:ibuf,B+树
  • 内容: space,offset,发生change的数据
  • 写入:
    1 不会导致空page:delete时只有一条记录 拒绝
    2 不会导致分裂,insert时检查IBUF BITMAP标识剩余空间大小,超出触发merge 拒绝
  • merge:(在很多情况都需要把ibuf里的页进行合并)
    1.辅助索引页被读取到缓冲池时
    2.插入时预估page no空间不足
    3.ibuf空间不足
    4.插入ibuf可能产生ibuf Tree的索引分裂
    5.Master (IDLE ,ACTIVE,SHUTDOWN)
    ……
  • Purge操作和insert在ibuf并发问题
    在purge模式下,用ibuf同时将watch插入到hash table中,如果都在内存里,会给同一份page加锁,没问题,但是要两个线程都写入ibuf_insert时,是没办法控制顺序的(本来就允许这种无序,因为非唯一)。所以需要一个进入后,另一个就放弃,不能都写入ibuf。
    在purge模式下,用ibuf同时将watch插入到hash table中,insert就不会再放入ibuf中了
    其他读取清除这个buf.

第三章 功能——事务

  • A undolog
  • C(一个事务中间状态可见性) MVCC
  • I (多个事物之间可见性/操作不干扰) MVCC
  • D redolog

undolog

  • 物理:回滚段,rseg0在ibdata第6个page,1~32临时表空间,33~128独立表空间或ibdata,存储在ibdata,临时表空间或单独表空间。每个表空间可以包含若干个段。每个段有1024个控制页slot和历史表。每个slot对应一个undo log对象,有一个undo log header.
  • 内存:全局trx_sys->rseg_array。每个事务trx->rsegs
  • 内容: 逻辑日志
    Insert undo日志记录插入的唯一键值的len和value。
    Update undo日志在insert undo基础上,同时记录了旧记录事务id,以及被更新字段的旧数据
  • 写入

    入口函数:btr_cur_ins_lock_and_undo
     a) 从chached_list或分配一个空闲slot创建undo页
     b) 顺序写undo log header和记录
     c) 在事务提交阶段,加入到history list或释放【见事务提交】
     Undo log的写入在一个单独的mtr中,受redo log的保护,先讲一个子事务mtr。Mtr是InnoDB对物理数据文件  操作的最小原子单元,保证持久性,用于管理对Page加锁、修改、释放、以及日志提交到公共buffer等工作。
    开启时初始化m_impl,比如mlog用于存储redo log记录
    提交时需要将本地产生的日志拷贝到公共缓冲区,将修改的脏页放到flush list上。
  • 回滚:
    入口函数:row_indo_step
    解析老版本记录,做逆向操作
  • 事务提交时undolog
    1.入口函数:trx_commit_low-->trx_write_serialisation_history
    2.事务提交整体流程(写完redo就可以提交了。)
    clipboard.png
    生成事务no。如果有update类的undo日志 。加入到purge_queue(清理垃圾),history链表(维护历史版本)
    子事务提交。Redo log写到公共缓存
    释放MVCC的readview;insert的undo日志释放(可cache重用,否则全部释放包括page页)
    刷日志

    3.在该函数中,需要将该事务包含的Undo都设置为完成状态,先设置insert undo,再设置update undo(trx_undo_set_state_at_finish),完成状态包含三种:

       如果当前的undo log只占一个page,且占用的header page大小使用不足其3/4时(TRX_UNDO_PAGE_REUSE_LIMIT),则状态设置为TRX_UNDO_CACHED,该undo对象会随后加入到undo cache list上;
       如果是Insert_undo(undo类型为TRX_UNDO_INSERT),则状态设置为TRX_UNDO_TO_FREE;
       如果不满足a和b,则表明该undo可能需要Purge线程去执行清理操作,状态设置为TRX_UNDO_TO_PURGE。
    

    对于undate undo需要调用trx_undo_update_cleanup进行清理操作。
    注意上面只清理了update_undo,insert_undo直到事务释放记录锁、从读写事务链表清除、以及关闭read view后才进行,
    这里的slot,undo page ,history关系:
    每个rseg控制页有1024个slot和history。undo page释放后或者移到history list后,就可以把slot清空、undo page转为cache不释放则不动slot

  • purge:删除(更新数据的真正删除),清除过期undo。入口函数srv_do_purge
    作用: 对于用户删除的数据,InnoDB并不是立刻删除,而是标记一下,后台线程批量的真正删除。类似的还有InnoDB的二级索引的更新操作,不是直接对索引进行更新,而是标记一下,然后产生一条新的。这个线程就是后台的Purge线程。此外,清除过期的undo,histroy list,指的是undo不需要被用来构建之前的版本,也不需要用来回滚事务。
    我们先来分析一下Purge Coordinator的流程。启动线程后,会进入一个大的循环,循环的终止条件是数据库关闭。在循环内部,首先是自适应的sleep,然后才会进入核心Purge逻辑。sleep时间与全局历史链表有关系,如果历史链表没有增长,且总数小于5000,则进入sleep,等待事务提交的时候被唤醒(srv_purge_coordinator_suspend)。退出循环后,也就是数据库进入关闭的流程,这个时候就需要依据参数innodb_fast_shutdown来确定在关闭前是否需要把所有记录给清除。接下来,介绍一下核心Purge逻辑。 srv_do_purge

    clipboard.png

    1)首先依据当前的系统负载来确定需要使用的Purge线程数(srv_do_purge),即如果压力小,只用一个Purge Cooridinator线程就可以了。如果压力大,就多唤醒几个线程一起做清理记录的操作。如果全局历史链表在增加,或者全局历史链表已经超过innodb_max_purge_lag,则认为压力大,需要增加处理的线程数。如果数据库处于不活跃状态(srv_check_activity),则减少处理的线程数。
    2)如果历史链表很长,超过innodb_max_purge_lag,则需要重新计算delay时间(不超过innodb_max_purge_lag_delay)。如果计算结果大于0,则在后续的DML中需要先sleep,保证不会太快产生undo(row_mysql_delay_if_needed)。
    3)从全局视图链表中,克隆最老的readview(快照、拿视图为了拿事务id.undo日志中upadte记了事务id),所有在这个readview开启之前提交的事务所产生的undo都被认为是可以清理的。克隆之后,还需要把最老视图的创建者的id加入到view->descriptors中,因为这个事务修改产生的undo,暂时还不能删除(read_view_purge_open)。
    4)从undo segment的最小堆中(堆存放每个段未被purge的最老的undo页),找出最早提交事务的undolog(trx_purge_get_rseg_with_min_trx_id),如果undolog标记过delete_mark(表示有记录删除操作),则把先关undopage信息暂存在purge_sys_t中(trx_purge_read_undo_rec)。
    5)依据purge_sys_t中的信息,读取出相应的undo,同时把相关信息加入到任务队列中。同时更新扫描过的指针,方便后续truncate undolog。
    6)循环第4步和第5步,直到为空,或者接下到view->low_limit_no,即最老视图创建时已经提交的事务,或者已经解析的page数量超过innodb_purge_batch_size。(把delete和Undopage分别存放,detele给工作线程删除)
    7)把所有的任务都放入队列后,就可以通知所有Purge Worker线程(如果有的话)去执行记录删除操作了。删除记录的核心逻辑在函数row_purge_record_func中。有两种情况,一种是数据记录被删除了,那么需要删除所有的聚集索引和二级索引(row_purge_del_mark),另外一种是二级索引被更新了(总是先删除+插入新记录),所以需要去执行清理操作。
    8)在所有提交的任务都已经被执行完后,就可以调用函数trx_purge_truncate去删除update undo(insert undo在事务提交后就被清理了)。每个undo segment分别清理,从自己的histrory list中取出最早的一个undo,进行truncate(trx_purge_truncate_rseg_history)。truncate中,最终会调用fseg_free_page来清理磁盘上的空间。

MVCC

undo+read view 写时并发读
ReadView::id 创建该视图的事务ID;

m_ids 创建ReadView时,活跃的读写事务ID数组,有序存储;记录trx_id不在m_ids中可见
m_low_limit_id  当前最大事务ID;记录rx_id>=ReadView::m_low_limit_id,则说明该事务是创建readview之后开启的,不可见
Rem_up_limit_id ;m_ids 集合中的最小值;记录trx_id< m_up_limit_id该事务在创建ReadView时已经提交了,可见

二级索引回聚簇索引中。
若不可见,则通过undo构建老版本记录。

redolog

  • 物理:log文件,ib_logfile 覆盖写
  • 内存:log buffer log_sys(记了日志在磁盘和内存中用到的信息,比如总大小,一些需要刷盘的阈值等)
  • 内容:记录物理位置spaceid,page,offset上要操作的逻辑日志
  • 写入
    每个子事务的操作都会写入log(mtr.m_impl.m_log中)
    mlog_open_and_write_index=》memcpy=》mlog_close
  • 提交 子事务提交写入缓冲区

    提交时,准备log内容,提交到公共buffer中,并将对应的脏页加到flush list上
    Step 1: mtr_t::Command::prepare_write()
        1.若当前mtr的模式为MTR_LOG_NO_REDO 或者MTR_LOG_NONE,则获取log_sys->mutex,从函数返回
        2.若当前要写入的redo log记录的大小超过log buffer的二分之一,则去扩大log buffer,大小约为原来的两倍。
        3.持有log_sys->mutex
        4.调用函数log_margin_checkpoint_age检查本次写入:如果本次产生的redo log size的两倍超过redo log文件capacity,则打印一条错误信息;若本次写入可能覆盖检查点,还需要去强制做一次同步*chekpoint*
        5.检查本次修改的表空间是否是上次checkpoint后第一次修改(fil_names_write_if_was_clean)
         如果space->max_lsn = 0,表示自上次checkpoint后第一次修改该表空间:
            a. 修改space->max_lsn为当前log_sys->lsn;
            b. 调用fil_names_dirty_and_write将该tablespace加入到fil_system->named_spaces链表上;
            c. 调用fil_names_write写入一条类型为MLOG_FILE_NAME的日志,写入类型、spaceid, page no(0)、文件路径长度、以及文件路径名(将本次的表空间和文件信息加入到一个内存链表上 (去除恢复中对数据字典的依赖))。
            在mtr日志末尾追加一个字节的MLOG_MULTI_REC_END类型的标记,表示这是多个日志类型的mtr。
            如果不是从上一次checkpoint后第一次修改该表,则根据mtr中log的个数,或标识日志头最高位为MLOG_SINGLE_REC_FLAG,或附加一个1字节的MLOG_MULTI_REC_END日志。
            
    Step 2: 拷贝
       若日志不够,log_wait_for_space_after_reserving

    Step 3:如果本次修改产生了脏页,获取log_sys->log_flush_order_mutex,随后释放log_sys->mutex。
    
    Step 4. 将当前Mtr修改的脏页加入到flush list上,脏页上记录的lsn为当前mtr写入的结束点lsn。基于上述加锁逻辑,能够保证flush list上的脏页总是以LSN排序。
    
    Step 5. 释放log_sys->log_flush_order_mutex锁
    
    Step 6. 释放当前mtr持有的锁(主要是page latch)及分配的内存,mtr完成提交。
    • 刷盘 整个事务的提交 trx_commit. 参数innodb_flush_log_at_trx_commit

      当设置该值为1时,每次事务提交都要做一次fsync,这是最安全的配置,即使宕机也不会丢失事务
      当设置为2时,则在事务提交时只做write操作,只保证写到系统的page cache,因此实例crash不会丢失事务,但宕机则可能丢失事务
      当设置为0时,事务提交不会触发redo写操作,而是留给后台线程每秒一次的刷盘操作,因此实例crash将最多丢失1秒钟内的事务,写入一条MLOG_FILE_NAME

    • 刷脏。刷脏后调用log checkpoint把点写入(刷脏就是内存到磁盘和redo没关系,redo写多了需要清除checkpoint写入刷脏点,之前的可以不要了),以后崩溃恢复从此点开始
      1.刷脏会在以下情形被触发

        启动和关闭时会唤醒刷脏线程
        每10s后、按以下比对落后点决定是否要刷脏。
        redo log可能覆盖写时,调用单独线程把未提交LSN对应的点放入log的checkpoint点,只是redolog写checkpoint点。以下参数控制checkpoint和flush刷脏点
            log_sys->log_group_capacity = 15461874893 (90%)
            log_sys->max_modified_age_async = 12175607164 (71%)
            log_sys->max_modified_age_sync = 13045293390 (76%)
            log_sys->max_checkpoint_age_async = 13480136503 (78%)
            log_sys->max_checkpoint_age = 13914979615 (81%)
        
        LRU LIST在未能自己释放时,先自己刷脏一页,不行再 唤醒刷脏线程
        

      2.刷脏线程
      clipboard.png
      innodb_page_cleaners设置为4,那么就是一个协调线程(本身也是工作线程),加3个工作线程,工作方式为生产者-消费者。工作队列长度为buffer pool instance的个数,使用一个全局slot数组表示。
      1)buf_flush_page_cleaner_coordinator协调线程

      主循环主线程以最多1s的间隔或者收到buf_flush_event事件就会触发进行一轮的刷脏。
      协调线程首先会调用pc_request()函数,这个函数的作用就是为每个slot代表的缓冲池实例计算要刷脏多少页,
      然后把每个slot的state设置PAGE_CLEANER_STATE_REQUESTED, 唤醒等待的工作线程。
      由于协调线程也会和工作线程一样做具体的刷脏操作,所以它在唤醒工作线程之后,会调用pc_flush_slot(),和其它的工作线程并行去做刷脏页操作。
      一但它做完自己的刷脏操作,就会调用pc_wait_finished()等待所有的工作线程完成刷脏操作。
      完成这一轮的刷脏之后,协调线程会收集一些统计信息,比如这轮刷脏所用的时间,以及对LRU和flush_list队列刷脏的页数等。
      然后会根据当前的负载计算应该sleep的时间、以及下次刷脏的页数,为下一轮的刷脏做准备。

      2)buf_flush_page_cleaner_worker工作线程

      主循环启动后就等在page_cleaner_t的is_requested事件上,
      一旦协调线程通过is_requested唤醒所有等待的工作线程,
      工作线程就调用pc_flush_slot()函数去完成刷脏动作。
      
      pc_flush_slot:
          先找到一个空间的slot,
          page_cleaner->n_slots_requested--; // 表明这个slot开始被处理,将未被处理的slot数减1 
          page_cleaner->n_slots_flushing++; //这个slot开始刷脏,将flushing加1 
          slot->state = PAGE_CLEANER_STATE_FLUSHING;
          
          刷LRU,FLUSH LIST
          
          page_cleaner->n_slots_flushing--; // 刷脏工作线程完成次轮刷脏后,将flushing减1 p
          age_cleaner->n_slots_finished++; //刷脏工作线程完成次轮刷脏后,将完成的slot加一 
          slot->state = PAGE_CLEANER_STATE_FINISHED; // 设置此slot的状态为FINISHED
          若是最后一个,os_event_set(page_cleaner->is_finished)
      pc_wait_finished:
          os_event_wait(page_cleaner->is_finished);
          统计等
      
      每次刷多少srv_max_buf_pool_modified_pct决定

      3.log_checkpoint

      clipboard.png

      入口函数为log_checkpoint,其执行流程如下:
      Step1. 持有log_sys->mutex锁,并获取buffer pool的flush list链表尾的block上的lsn,这个lsn是buffer pool中未写入数据文件的最老lsn,在该lsn之前的数据都保证已经写入了磁盘。checkpoint 点,        在crash recover重启时,会读取记录在checkpoint中的lsn信息,然后从该lsn开始扫描redo日志。
      Step 2. 调用函数fil_names_clear
          扫描fil_system->named_spaces上的fil_space_t对象,如果表空间fil_space_t->max_lsn小于当前准备做checkpoint的Lsn,则从链表上移除并将max_lsn重置为0。同时为每个被修改的表空间构建MLOG_FILE_NAME类型的redo记录。(这一步未来可能会移除,只要跟踪第一次修改该表空间的min_lsn,并且min_lsn大于当前checkpoint的lsn,就可以忽略调用fil_names_write)
          写入一个MLOG_CHECKPOINT类型的CHECKPOINT REDO记录,并记入当前的checkpoint LSN
      Step3 . fsync 被修改的redo log文件
          更新相关变量:
          log_sys->next_checkpoint_no++
          log_sys->last_checkpoint_lsn = log_sys->next_checkpoint_lsn
      Step4. 写入checkpoint信息
          函数:log_write_checkpoint_info --> log_group_checkpoint
          checkpoint信息被写入到了第一个iblogfile的头部,但写入的文件偏移位置比较有意思,当log_sys->next_checkpoint_no为奇数时,写入到LOG_CHECKPOINT_2(3 *512字节)位置,为偶数时,写入到LOG_CHECKPOINT_1(512字节)位置。
    • 崩溃恢复

      1.从第一个iblogfile的头部定位要扫描的LSN(数据落盘点)
      2.扫描redo log
      1) 第一次redo log的扫描,主要是查找MLOG_CHECKPOINT,不进行redo log的解析,
      2) 第二次扫描是在第一次找到MLOG_CHECKPOINT(获取表和路径)基础之上进行的,该次扫描会把redo log解析到哈希表中,如果扫描完整个文件,哈希表还没有被填满,则不需要第三次扫描,直接进行recovery就结束
      3)第二次扫描把哈希表填满后,还有redo log剩余,则需要循环进行扫描,哈希表满后立即进行recovery,直到所有的redo log被apply完为止。
      3.具体redo log的恢复

         MLOG_UNDO_HDR_CREATE:解析事务ID,为其重建undo log头;
         MLOG_REC_INSERT 解析出索引信息(mlog_parse_index)和记录信息(    page_cur_parse_insert_rec)等
         在完成修复page后,需要将脏页加入到buffer pool的flush list上;查找红黑树找到合适的插入位置
         MLOG_FILE_NAME用于记录在checkpoint之后,所有被修改过的信息(space, filepath);        MLOG_CHECKPOINT用于标志MLOG_FILE_NAME的结束。
      
         在恢复过程中,只需要打开这些ibd文件即可,当然由于space和filepath的对应关系通过redo存了下来,恢复的时候也不再依赖数据字典。
      
         在恢复数据页的过程中不产生新的redo 日志;
    • 二次写
      MySQL 一直使用double write buffer来解决一个page写入的partial write问题,但在linux系统上的Fusion-io Non-Volatile Memory (NVM) file system支持原子的写入。这样就可以省略掉double write buffer的使用, 5.7.4以后,如果Fusion-io devices支持atomic write,那么MySQL自动把dirty block直接写入到数据文件了。这样减少了一次内存copy和IO操作。
      redo会记spaceid,pageno,偏移量内的逻辑日志只记录:’这是一个插入操作’和’这行数据的内容‘,这是一个更新操作,更新内容(为了省地方)。但是这样就有个问题。在redo log恢复执行时,如果页逻辑时一半断电了,redo log在恢复时无法正确恢复更新操作。这就需要在脏页落盘时采取二次写。数据写入ibd前先顺序写入ibdata.在崩溃恢复时,先检验checksum.不合法载入ibdata的数据。

      clipboard.png

      Redo为了保证原子性,要求一块一写。不够的话要先读旧的然后改然后写。以512字节(最小扇区)对其方式写入,不需要二次写。设置一个值innodb_log_write_ahead_size,不需要这个过程,超过该值补0到一块直接插入
      [ps 数据需要二次写,因为可能夸多扇区,leveldb的log增加头直接跳过坏页,redo log固定大小,正常日志都是写成功才会被回放,写内存与写坏后丢只能丢失,解析错误跳过到下一块吧,问题就是要有个大小找到下一个位置]

    server与innodb的事务保证

    • server和引擎层事务的界限
      1.开启事务。server只会调用引擎层。
      server层如果不以命令,是不会显示开启事务的。在SQLCOM_BEGIN等命令会调用trans_begin 分布式事务会调trans_begin(跟踪下)
      证明是正确的,在外层trans_begin并没有调用。并不研究了
      提交会在server层调用各个引擎的事务提交。
      下面说下innodb层的trx
      2.提交事务。根据是否开启binlog和是否有多个引擎执行不同。比如开了Binlog且使用了事务引擎,用Mysql_bin_log的两阶段和组提交。如果没有用事务引擎,直接记log等就可以
      3.事务回滚:分为真正xa回滚还是普通回滚。普通回滚调用引擎层回滚
      4.崩溃恢复:没有server层的崩溃恢复
    • 开启 分配回滚段,获取事务id,加入事务链表
    • 提交 入口: MYSQL_BIN_LOG::commit,如果是分布式事务,用xa,两阶段。prepare和commit。我们先研究普通的提交。XA不作为重点。但是由于server层和Innodb层两个日志,需要保证顺序,也按照XA的两阶段设计。也叫内部xa
      1) xa两阶段
      Prepare

      undo log写入xid,设置状态为PREPARED

      Commit  

      Flush Stage:由leader依次为别的线程对flush redo log到LSN,再写binlog文件
      Sync Stage:如果sync_binlog计数超过配置值,以组为维度文件fsync
      Commit Stage:队列中的事务依次进行innodb commit,修改undo头的状态为完成;并释放事务锁,清理读写事务链表、readview等一系列操作,落盘redo。
      

      2) 原因
      两阶段是为了保证binlog和redo log一致性。server和备库用binlog来恢复同步。innodb用undo和redo恢复。
      1落undo 2flush redo 3 flush binlog 4fsync binlog 5fsync redo [ps:sync可能只是内核缓冲放入磁盘队列,fsync只保证放入磁盘,都是同步] 6 undo D
      保证binlog若成功了,根据Undo的p结果不会回滚出现主从不一致的情况
      3) 组提交:两阶段提交,在并发时无法保证顺序一致,用ordered_commit控制
      clipboard.png
      一个On-line的backup程序新建一个slave来做replication,那么事务T1在slave机器restore MySQL数据库的时候发现未在存储引擎内提交,T1事务被roll back,此时主备数据不一致(搭建Slave时,change master to的日志偏移量记录T3在事务位置之后)。
      如果关闭binlog_order_commits。事务各自提交。这时候没有设置不写redo log。不能保证Innodb commit顺序和Binlog写入顺序一直,但是不会影响数据一致性。只是物理备份数据不一致。但是依赖于事务页上记录binlog恢复的,比如xtrabackup就会发生备份数据不一致的情况。
      每个stage阶段都有各自的队列,使每个session的事务进行排队。,leader控制,当一组事务在进行Commit阶段时,其他新的事务可以进行Flush阶段

    • 回滚
      两阶段:正常应该根据undo非DONE回滚,但看到undo为prepare且binlog有,就不回滚
      当由于各种原因(例如死锁,或者显式ROLLBACK)需要将事务回滚时,ha_rollback_trans=》ha_rollback_low,进而调用InnoDB函数trx_rollback_for_mysql来回滚事务。对于innodb回滚的方式是提取undo日志,做逆向操作。
      提交失败会回滚。走的非xa,调用trx_rollback_for_mysql。原来一直纠结binlog会不会删除。。。跟踪了好久也没看出来,其实是undo中的在提交时重新写一下binlog。这里在子事务里会介绍。

    第四章 分布式

    主从复制

    • 三种日志模式
      1.基于行的复制  row
      优点:符合幂等性,高度保障数据一致。
      缺点:数据量大

      2.基于语句的复制  statement
      优点:日志量少
      缺点:特定功能函数导致主从数据不一致,重复执行时无法保证幂等

      3.混合类型的复制  mixed  (默认语句,语句无法精准复制,则基于行)

    • 主从同步过程
      clipboard.png

      其中1. Slave 上面的IO线程连接上 Master,并请求从指定日志文件的指定位置(或者从最开始的日志)之后的日志内容;
      重放过程和master一样,也redolog

    • GTID

      MySQL 5.6引入全局事务ID的首要目的,是保证Slave在并行复制(并行顺序会乱)的时候不会重复执行相同的事务操作;用全局事务IDs代替由文件名和物理偏移量组成的复制位点(每个日志包含GID_Sets,xx:1-100形式)。

      GTID的组成部分:

       前面是server_uuid:后面是一个串行号
       例如:server_uuid:sequence number
       7800a22c-95ae-11e4-983d-080027de205a:10
       UUID:每个mysql实例的唯一ID,由于会传递到slave,所以也可以理解为源ID。
       Sequence number:在每台MySQL服务器上都是从1开始自增长的串行,一个数值对应一个事务。

      当事务提交时,不管是STATEMENT还是ROW格式的binlog,都会添加一个XID_EVENT事件作为事务的结束。该事件记录了该事务的id(这个是存储引擎里的事务id,崩溃恢复时决是否提交存储引擎中状态为prepared的事务)。

    • 同步方案
      1.同步复制 所谓的同步复制,意思是master的变化,必须等待slave-1,slave-2,...,slave-n完成后才能返回。
      2.异步复制 master只需要完成自己的数据库操作即可。至于slaves是否收到二进制日志,是否完成操作,不用关心
      3.半同步复制 master只保证slaves中的一个操作成功,就返回,其他slave不管。

       这里有个不一致的问题。
       开始提交事务 =>write binlog => sync binlog => engine commit => send events =>返回 commit后崩溃,send_events失败,会导致master有slave没有,需要靠binlog同步补一下。
       开始提交事务 =>write binlog => sync binlog => send events => engine commit =>返回 send_events失败,若sync binlog未落盘,导致XA不会重做,slave领先,若binlog落盘则没有问题,可接受和单机redo一样。
       

      master既要负责写操作,还的维护N个线程,负担会很重。可以这样,slave-1是master的从,slave-1又是slave-2,slave-3,...的主,同时slave-1不再负责select。slave-1将master的复制线程的负担,转移到自己的身上。这就是所谓的多级复制的概念。

    • 并行复制

      clipboard.png
      定期checkout-point将队列中执行结束的删除。记录checkpoint后每个worker是否执行过的bitmap。崩溃恢复时执行Bitmap未执行的部分。按db分粒度大可以换成table

    扩展性

    当主库支撑不了。水平扩展。拆表。

    可靠性

    需要proxy保证

    一致性

    同步策略影响。
    XA分为外部和内部。对于外部。要应用程序或proxy作为协调者。(二阶段提交协调者判断所有prepare后commit)。对于内部,binlog控制。

    第五章 proxy功能

    • 数据库分片的合并
    • 共享式的缓存
    • 读写分离路由
    • 可靠性的保证,主从切换,故障发现与定位
    • XA一致性的实现
    • 过滤加注释

    例如:https://github.com/mariadb-co...


    梦想家
    107 声望76 粉丝