文章https://segmentfault.com/a/11... 中介绍了存储应该考虑的方向。本文详细介绍其中的mysq,主要是INNODB。总体架构,启动流程,一条语句的执行过程带你快速深刻mysql源码
。再从性能(缓存,数据结构
),功能(ACID实现,索引
)如何实现介绍了mysql中核心点。第二部分为分布式,介绍原生mysql的同步
过程。第三部分是proxy,由于proxy多数会自研,只介绍proxy
应该包含的功能。mysql
mysql为单进程多线程,由于元数据用Innodb保存,启动后除了mysql的处理链接请求/超时等还会启动Innodb的全部线程。
主流程:linux
主函数在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用到的一系列线程。
总体流程图以下:git
必须有这些步骤的缘由:
[1]为了快,全部数据先写入内存,再刷脏
[2]为了防止数据页写过程当中崩溃数据的持久性=》先写redo保证重启后能够恢复。日志写不成功不操做,日志是顺序写,内容少,能够同步等。(最好是物理重作)。
[3]异常回滚=》物理回滚反解复杂,须要一个逻辑日志。
基于undo log又实现了MVCC
unlog等也要保证操做持久化原子化。
[4]为了删除不每次整理页,只标记,为了真正删除/undo不须要的清除=》purge
[5]flush对一个pageid屡次操做合并在一块儿减小随机操做=》二级索引非惟一change buff
[6]Flush过程当中一个页部分写成功就崩溃,没法正确后恢复=》二次写
[7]为完整的主链路。
[8]为异步的刷盘链路github
详细步骤:算法
binlog
】,调用引擎table->file->ha_write_row(table->record[0])开启事务
的操做,trx_start_low。全部数据页。都走这套。包括undo等sql
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链表上。数据库
若是没有则表示须要从磁盘读取。在读盘前首先咱们须要为即将读入内存的数据页分配一个空闲的block。当free list上存在空闲的block时,能够直接从free list上摘取;若是没有,就须要从unzip_lru 或者 lru上驱逐page。先unzip lru。再lru是否有可替换page,直接释放,不然多是脏页多,再线程在LRU上作脏页刷新。后台线程也会按期作脏页刷新。segmentfault
一个流程对buffer的操做步骤:数组
内存整体流程缓存
见上B+树
对非惟一二级索引页,delete_mark,delete,insert顺序插入缓冲区,合并减小随机IO。
写入
入口函数: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上。
事务提交时undolog
1.入口函数:trx_commit_low-->trx_write_serialisation_history
2.事务提交总体流程(写完redo就能够提交了。)
生成事务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
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来清理磁盘上的空间。
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构建老版本记录。
提交 子事务提交写入缓冲区
提交时,准备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完成提交。
当设置该值为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.刷脏线程
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
入口函数为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 日志;
Redo为了保证原子性,要求一块一写
。不够的话要先读旧的而后改而后写。以512字节(最小扇区)对其方式写入,不须要二次写。设置一个值innodb_log_write_ahead_size,不须要这个过程,超过该值补0到一块直接插入
[ps 数据须要二次写,由于可能夸多扇区,leveldb的log增长头直接跳过坏页,redo log固定大小,正常日志都是写成功才会被回放,写内存与写坏后丢只能丢失,解析错误跳过到下一块吧,问题就是要有个大小找到下一个位置]
提交 入口: 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控制
一个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阶段
2.基于语句的复制 statement
优势:日志量少
缺点:特定功能函数致使主从数据不一致,重复执行时没法保证幂等
3.混合类型的复制 mixed (默认语句,语句没法精准复制,则基于行)
其中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的复制线程的负担,转移到本身的身上。这就是所谓的多级复制的概念。
按期checkout-point将队列中执行结束的删除。记录checkpoint后每一个worker是否执行过的bitmap。崩溃恢复时执行Bitmap未执行的部分。按db分粒度大能够换成table
当主库支撑不了。水平扩展。拆表。
须要proxy保证
同步策略影响。
XA分为外部和内部。对于外部。要应用程序或proxy做为协调者。(二阶段提交协调者判断全部prepare后commit)。对于内部,binlog控制。
合并
缓存
路由
故障
发现与定位XA
一致性的实现过滤
加注释