共计 16059 个字符,预计需要花费 41 分钟才能阅读完成。
整体架构 / 流程
主流程:主函数在 sql/amin.cc 中调用 Mysqld.cc 中 mysqld_main
首先载入篇日志,信号注册,plugin_register(mysql 是插件式存储引擎设计,innodb,myisam 等都是插件,在这里注册),核心为 mysqld_socket_acceptor->connection_event_loop();
监听处理循环 poll。process_new_connection 处理 handler 有三种,线程池方式只用于商业,一个线程处理所有请求,一个连接一个线程(大多数选择 Per_thread_connection_handler)。
若 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/month…)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。第二正会讲一下
除了用户线程和主线程,在启动时,还创建了 timer_notify 线程。由于为了解决 DDL 无法做到 atomic 等,从 MySQL8.0 开始取消了 FRM 文件及其他 server 层的元数据文件(frm, par, trn, trg, isl,db.opt),所有的元数据都用 InnoDB 引擎进行存储, 另外一些诸如权限表之类的系统表也改用 InnoDB 引擎。因此在加载这些表时,创建了 innodb 用到的一系列线程。
从插入流程开始
外层 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])
引擎层: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 函数把只读事务提升为读写事务。
整体流程图如下:
必须有这些步骤的原因:[1]为了快,所有数据先写入内存,再刷脏 [2] 为了防止数据页写过程中崩溃数据的持久性 =》先写 redo 保证重启后可以恢复。日志写不成功不操作,日志是顺序写,内容少,可以同步等。(最好是物理重做)。[3]异常回滚 =》物理回滚反解复杂,需要一个逻辑日志。基于 undo log 又实现了 MVCC unlog 等也要保证操作持久化原子化。[4]为了删除不每次整理页,只标记,为了真正删除 /undo 不需要的清除 =》purge[5]flush 对一个 pageid 多次操作合并在一起减少随机操作 =》二级索引非唯一 change buff[6]Flush 过程中一个页部分写成功就崩溃,无法正确后恢复 =》二次写 [7] 为完整的主链路。[8]为异步的刷盘链路
性能
磁盘,B+ 树
先来个准确的 B + 比 B 为何更适合:区别两点,一个是 B 树搜索是可以止于非页节点的,包含数据(包含数据在磁盘中页的位置),且数据只出现在树中一次。另一点是叶子节点有双向链表。第一点使得节点可以包含更多路(因为不存数据在磁盘中页的位置,只包含下一层的指针页位置,B 树这两个都要包含),层高会更少;只能到页节点搜索结束,性能稳定。第二点为了扫描和范围索引。
内存 buffer
所有数据页。都走这套。包括 undo 等所有的数据页都存储在一个 LRU 链表上,修改过的 block 被加到 flush_list 上,解压的数据页被放到 unzip_LRU 链表上。
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 上
当另外一个线程也想请求相同 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 的操作步骤:
索引:聚簇索引
见上
二级索引 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_undoa) 从 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 入口函数:trx_commit_low–>trx_write_serialisation_history 事务提交整体流程,注意看下大概顺序,后面会具体讲
写完 redo 就可以提交了。
生成事务 no。如果有 update 类的 undo 日志。加入到 purge_queue(清理垃圾),history 链表(维护历史版本)
子事务提交。Redo log 写到公共缓存
释放 MVCC 的 readview;insert 的 undo 日志释放(可 cache 重用,否则全部释放包括 page 页)
刷日志
在该函数中,需要将该事务包含的 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
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
刷脏 刷脏会在以下情形被触发
启动和关闭时会唤醒刷脏线程
redo log 可能覆盖写时,调用单独线程把未提交 LSN 对应的 redo log 刷盘
LRU LIST 在未能自己释放时,先自己刷脏一页,不行再 唤醒刷脏线程
刷脏线程
innodb_page_cleaners 设置为 4,那么就是一个协调线程(本身也是工作线程),加 3 个工作线程,工作方式为生产者 - 消费者。工作队列长度为 buffer pool instance 的个数,使用一个全局 slot 数组表示。
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 的时间、以及下次刷脏的页数,为下一轮的刷脏做准备。
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 LISTpage_cleaner->n_slots_flushing–; // 刷脏工作线程完成次轮刷脏后,将 flushing 减 1 page_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 决定
2. log_checkpoint
在主线程,合并 insertbuffer,等需要 redo log 空间的以及事务提交后等都会调用 log_free_check_wait =》调用单独的 log checkpoint 进程 这里如果 page cleaner 在 ACTIVE 唤醒 buf_flush_event(buf_flush_page_coordinator_thread 监听),否则自己 buf_flush_sync_all_buf_pools。
redo checkpoint 的入口函数为 log_checkpoint,其执行流程如下:Step1. 持有 log_sys->mutex 锁,并获取 buffer pool 的 flush list 链表尾的 block 上的 lsn,这个 lsn 是 buffer pool 中未写入数据文件的最老 lsn,在该 lsn 之前的数据都保证已经写入了磁盘。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 到当前的 lsn 调用 fil_flush_file_spaces(关闭时 srv_shutdown_log,崩溃恢复……)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 字节)位置。
在 crash recover 重启时,会读取记录在 checkpoint 中的 lsn 信息,然后从该 lsn 开始扫描 redo 日志。
Checkpoint 操作由异步 IO 线程执行写入操作,当完成写入后,会调用函数 log_io_complete 执行如下操作:
fsync 被修改的 redo log 文件
更新相关变量:
log_sys->next_checkpoint_no++
log_sys->last_checkpoint_lsn = log_sys->next_checkpoint_lsn
释放 log_sys->checkpoint_lock 锁
崩溃恢复 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 日志;
二次写 redo 会记 spaceid,pageno, 偏移量内的逻辑日志只记录:’这是一个插入操作’和’这行数据的内容‘。为了省地方。但是这样就有个问题。在 redo log 恢复执行时,如果执行逻辑时一半断电了,比如已经某地址加了一个记录数。但又断电了,重新灰度执行又会执行一遍。就会在地址后又加一条。写坏了。这就需要在脏页落盘时采取二次写。数据写入 ibd 前先顺序写入 ibdata. 在崩溃恢复时,先检验 checksum. 不合法载入 ibdata 的数据。
Redo 为了保证原子性,要求一块一写。不够的话要先读旧的然后改然后写。以 512 字节 (最小扇区) 对其方式写入,不需要二次写。设置一个值 innodb_log_write_ahead_size,不需要这个过程,超过该值补 0 到一块直接插入
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 等一系列操作。2) 原因 两阶段是为了保证 binlog 和 redo log 一致性。server 和备库用 binlog 来恢复同步。innodb 用 undo 和 redo 恢复。1 落 undo 2flush redo 3 flush binlog 4fsync binlog 5fsync redo [ps:sync 可能只是内核缓冲放入磁盘队列,fsync 只保证放入磁盘,都是同步] 2 要在 3 前 因为要先保证 redo 写了才算是事务执行了,否则无法回滚就记了 binlog 4 要在 5 前 提交成功,redo 才会落盘成功,如果先 redo 再 binlog, 但是 binlog 落盘失败,不会回滚,binlog 少记录 但是如果按照 4 到 5 的顺序,有 binlog 落盘成功,如果此时正常,5 失败会回滚,按照 undo 的内容逆着执行一次,binlog 记录了重新执行的,可以正常恢复数据;如果此时异常宕机,无法回滚,就会出问题。binlog 已经落盘,redo 没有落盘。此时数据同步导致主从不一致。因此加两阶段。在第一阶段在写入 xid,undo 设置为 prepare。如果在宕机恢复时对于 prepare 中的发现 binlog 的 xid 没有被执行,重新执行一遍。prepare 阶段的 redo log 可以先不写,在 commit 阶段一起写。只要保证 flush redo 在 flush binlog 前就可以 两阶段其实也是 undo 标识一下 binlog 和 redolog 还不一致。
3) 组提交:两阶段提交,在并发时无法保证顺序一致,用 ordered_commit 控制
如图在 Online backup taken 时,那么事务 T1 在备机恢复 MySQL 数据库时,发现 T1 未在存储引擎内提交,那么在恢复时,T1 事务就会被回滚。数据不一致。
如果关闭 binlog_order_commits。事务各自提交。这时候没有设置不写 redo log。不能保证 Innodb commit 顺序和 Binlog 写入顺序一直,但是不会影响数据一致性。只是物理备份数据不一致。但是依赖于事务页上记录 binlog 恢复的,比如 xtrabackup 就会发生备份数据不一致的情况。
回滚两阶段:正常应该根据 undo 回滚, 但看到 undo 为 iprepare 且 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 (默认语句,语句无法精准复制,则基于行)
主从同步过程
其中 1. Slave 上面的 IO 线程连接上 Master,并请求从指定日志文件的指定位置 (或者从最开始的日志) 之后的日志内容;
重放过程和 master 一样,也 redolog
GTID MySQL 5.6 引入全局事务 ID 的首要目的,是保证 Slave 在复制的时候不会重复执行相同的事务操作;其次,是用全局事务 IDs 代替由文件名和物理偏移量组成的复制位点,定位 Slave 需要复制的 binlog 内容。因此,MySQL 必须在写 binlog 时记录每个事务的全局 GTID,保证 Master / Slave 可以根据这些 GTID 忽略或者执行相应的事务。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。
同步方案 同步复制 所谓的同步复制,意思是 master 的变化,必须等待 slave-1,slave-2,…,slave- n 完成后才能返回。异步复制 master 只需要完成自己的数据库操作即可。至于 slaves 是否收到二进制日志,是否完成操作,不用关心 半同步复制 master 只保证 slaves 中的一个操作成功,就返回,其他 slave 不管。master 既要负责写操作,还的维护 N 个线程,负担会很重。可以这样,slave- 1 是 master 的从,slave- 1 又是 slave-2,slave-3,… 的主,同时 slave- 1 不再负责 select。slave- 1 将 master 的复制线程的负担,转移到自己的身上。这就是所谓的多级复制的概念。这里会有个一致性的问题。开始提交事务 =>write binlog => sync binlog => engine commit => send events => 返回 send_events 失败会导致 master 有 slave 没有 开始提交事务 =>write binlog => sync binlog => send events => engine commit => 返回 send_events 失败若 sync binlog 未同步,导致 XA 不会重做,slave 领先
并行复制
定期 checkout-point 将队列中执行结束的删除。记录 checkpoint 后每个 worker 是否执行过的 bitmap。崩溃恢复时执行 Bitmap 未执行的部分。按 db 分粒度大可以换成 table
扩展性
当主库支撑不了。水平扩展。拆表。
可靠性
需要 proxy 保证
一致性
同步策略影响。XA 分为外部和内部。对于外部。要应用程序或 proxy 作为协调者。(二阶段提交协调者判断所有 prepare 后 commit)。对于内部,binlog 控制。
proxy 功能
共享式的缓存读写分离路由可靠性的保证,主从切换, 故障发现与定位 XA 一致性的实现过滤加注释例如:https://github.com/mariadb-co…