Innodb read only事务、MySQL5.7和Percona的事务改进
trx_commit_in_memory trx->read_view = NULL;
trx_assign_read_view(
read_view_open_now
read_view_create_low
read_view_open_now_low
Innodb read only事务、MySQL5.7和Percona的事务改进
前言
只读事务在MySQL5.6中引入,改进了创建视图快照的开销,减少了持有trx_sys->mutex的时间,这有利于提升只读性能;这一点已经广为人知; 本文的内容基本按照读代码的顺序来的,先了解了下Oracle MySQL5.6.15的只读事务部分代码,再看了Percona5.6对于事务部分的相关改进;随后大概过了下Oracle MySQL5.7对事务部分的优化; 总的来说,Percona移植了其在5.5上所做的优化,而Oracle MySQL5.7优化的更彻底,很多代码都重构了。 本文不涉及到性能测试,只是代码阅读过程的笔记,记录的目的是方便以后查阅方便,因此同时也附带上了一些新版本修改的Rev号。1.如何使用只读事务
a.设置变量tx_read_only,当全局设置为true时,涉及到的SQL只能是只读的。这个参数可以是session 级别,也可以是全局级别; 开启该参数后,就默认所有查询走只读的逻辑; b.开启事务时指明: START TRANSACTION READ ONLY; c.autocommit状态下的查询操作也会被当做只读事务 如果事务中混合了DML操作,就会报如下错误:root@test 09:57:58>delete from t1;
ERROR 1792 (25006): Cannot execute statement in a READ ONLY transaction.2.只读事务涉及的代码逻辑(MySQL5.6)
Innodb将所有的事务对象维护在链表上,通过trx_sys来管理,在5.6中,最明显的变化就是事务链表被拆分成了两个链表: 一个是只读事务链表:ro_trx_list,其他非标记为只读的事务对象放在链表rw_trx_list上; 这种分离,使得读写事务链表足够小,创建readview 的MVCC快照的速度更快; a.开始一个事务 入口函数trx_start_low 1)判断事务是否是只读的;这里will_lock 的定义感觉有点奇怪,在做DML时,这总是一个较大的值,但DML事务完成后,并没用清0,导致随后的一个select不被认为是一个autocommit no-lock的read only事务;不知道是否是预期中的,写了个bug:http://bugs.mysql.com/bug.php?id=71164 trx->no = TRX_ID_MAX //初始值被设置为一个极大值 trx->id = trx_sys_get_new_trx_id(); //事务id为当前最大的事务id(trx_sys->max_trx_id) 2)对于read_only的事务,无需去为其分配回滚段(trx_assign_rseg_low) 3)对于read only的事务,只有不是non-locking autocommit select时,才将trx对象加入到ro_trx_list上; 也就是说,autocommit的只读查询无需加入活跃事务链表。 对于非只读事务,加入到rw_trx_list上; b.创建read view快照 每次显式start transaction with consistent snapshot(在repeatable read 隔离级别下)或者事务的第一条SELECT,都需要去创建一个rearview 创建read view的目的是了限定该查询的事务可见性 trx_assign_read_view->read_view_open_now->read_view_open_now_low 从函数read_view_open_now_low 可以看出,在创建read view时,只需要考虑读写事务链表,这有别于之前版本需要扫描全部事务,因为这是trx_sys->mutex的保护之下,因此可以提升性能。 具体的,首先根据读写事务链表的长度分配read view及一个事务id数组,两者分配在同一块内存 (view = read_view_create_low(n_trx, heap)); 然后将当前活跃的(状态不是TRX_STATE_COMMITTED_IN_MEMORY)读写事务id(rw_trx_list)拷贝到view->trx_ids数组中,id顺序为降序 ut_list_map(trx_sys->rw_trx_list, &trx_t::trx_list, CreateView(view)); view->low_limit_no被设置为当前活跃事务中最小的trx->no(在trx_commit->trx_commit_low->trx_write_serialisation_history->trx_serialisation_number_get中被赋值,设为当前最大事务trx_sys->max_trx_id+1)。 在扫描完所有的读写事务后,设置up_limit_id为当前活跃读写事务的最小事务id; low_limit_no的意思是该read view在读多版本时,无需去读事务号小于这个值的undo日志; low_limit_id表示所有事务id大于等于该值的事务所做的修改都不应该被该view看到; up_limit_id 表示所有小于该值的事务,都能被当前view可见; 然后根据low_limit_no顺序降序将其插入到rx_sys->view_list链表中(read_view_add(view)) c.判断事务可见性 通过函数read_view_sees_trx_id来进行判断,对于活跃的事务,通过二分查找来判断 d.事务提交 backtrace: innobase_commit->trx_commit_for_mysql->trx_commit->trx_commit_in_memory 这时候对于不同的事务类型有所区分: #对于autocommit no-lock的事务类型,直接设置完事务状态,从trx sys的read view链表中移除即可; #对于正常开启的事务,先释放锁(lock_trx_release_locks()),再分别从只读事务和读写事务链表中移除; 对于read only的事务,也需要调用lock_trx_release_locks,举个例子: START TRANSACTION READ ONLY; select * from sbtest1 where id = 999 lock in share mode;trx->auto_commit = (trx->api_trx && trx->api_auto_commit) || thd_trx_is_auto_commit(trx->mysql_thd); trx->read_only = (trx->api_trx && !trx->read_write) || (!trx->ddl && thd_trx_is_read_only(trx->mysql_thd)) || srv_read_only_mode; if (!trx->auto_commit) { ++trx->will_lock; } else if (trx->will_lock == 0) { trx->read_only = TRUE; }
3.Percona对创建read view的改进
Percona在5.5.30及5.6.11之后的版本中对readview这部分逻辑做了修改,Percona的官方博客对此进行了描述; 大体的修改为: a.在开启一个事务时,如果是读写事务,那么会为其在一个全局数组中保留一个slot(trx_start_low->trx_reserve_descriptor(trx)) trx_sys->descriptors是维护活跃事务id的数组,新的事务 id会从数组尾部开始找到位置插入其id值;数组以事务id升序排列 trx_sys->descr_n_used 表示当前读写事务的个数; b.创建read view的内存分配(read_view_create_low)不再是从trx对象的heap中分配,而是使用malloc分配,分配好后cache下来,存储在trx->prebuilt_view中,下次重用该事务对象(trx_t)时就可以重复使用. read_view成员新增max_trx_ids,用于维持活跃事务id数组的长度,只有当前事务链表大于该值时,才需要重分配,分配的数组大小为当前活跃读写事务数的1.1倍. c.由于已经将事务id有序的存储在数组trx_sys->descriptors中,那么这里只需要将这个数组(除了当前事务id)直接进行memcpy即可;这相比Oracle MySQL5.6的便利链表的方式效率更高。 有人可能注意到,在5.6原生逻辑中,遍历活跃读写事务链表时,还要找到最小的trx->no,将其复制给view->low_limit_no,在Percona的改进里增加了一个链表trx_sys->trx_serial_list,用于维护那些已经分配了序列号的事务(见函数trx_serialisation_number_get),由于分配的过程是有序的,因此只需要取列表的第一个节点即可; 相关函数:read_view_open_now_low d.在判断事务可见性时,直接使用c++的bsearch函数;4.MySQL5.7的事务系统及相关改进
a.MySQL5.7在这部分的代码基本上重构了,大量使用C++的类,对于我这样习惯了innodb C语言格式的人来说,还真有点觉得别扭。(Rev:6203) 从其在Rev:6203 commit的日志来看,包含以下改进:大概扫了下: #只读事务不考虑innodb的commit concurrency,提交时不调用trx_commit_complete_for_mysql, 不考虑auot-inc 锁, 无需去唤醒master线程,等等等; #所有MVCC操作使用一个新类MVCC来进行重构; #系统初始化时,会预先创建1024个read view(trx_sys_create); 所有cache的read view被放到MVCC::m_free链表中; 另外一个链表是m_views, 用于存储所有活跃或者标记为关闭的readview,当前只有auto commit no-lock read only的SQL使用这一优化,重用上一个事务的read view;如果只读期间,没有任何的分配事务id,也就是没有写操作(trx_sys->max_trx_id未发生变化),那么这个read view会被直接接着使用;(函数MVCC::view_open) #创建readview的代码路径和之前不同,但入口皆为trx_assign_read_view,调用trx_sys->mvcc->view_open(trx->read_view, trx) 直接从m_free链表中使用一个空闲的readview,无需分配内存(MVCC::get_view) #拷贝活跃事务id的行为(ReadView::prepare)和Percona版本的类似,都是新加了一个list,trx_sys->serialisation_list来维护进入commit阶段分配了序列号的事务(trx->no),直接使用内存拷贝,因为在创建读写事务时,已经在trx_sys->rw_trx_ids中维护了事务id。 #检查事务可见性(view->changes_visible(trx_id)) #除了事务read view外,还为锁系统也分配了内存(rec_pool,table_pool) b.无需显式的开启一个只读事务,自动识别(Rev:5209) #默认情况下,所有的事务都认为以只读的方式开启(除非事务被显式标示为读写操作) #当遇到写操作,或者需要加IX/X锁时,转换为读写模式(见函数trx_start_if_not_started_xa_low); #只读事务不分配事务id(trx_start_low);但对于只读查询但创建了临时表的场景,将其设置为读写事务 #实际上已经没有读事务队列了(Rev:6788); c.同样的事务对象trx_t也为其预分配了内存,(Rev:5744),默认为4M字节的连续内存; 在5.7里增加了一套标准类来处理类似的需要pool的场景1. Refactor the MVCC code 2. Reuse read views for AC-NL-RO selects 3. Use a pool of read views 4. Add MVCC class 5. Use a trx_id to trx_t* map 6. Keep the active trx_id_ts in a vector. 7. Pre-allocate a small cache of record and table locks 8. Avoid extra work when a transaction is tagged as read-only (during commit). 9. General code cleanup
本文来自互联网用户投稿,文章观点仅代表作者本人,不代表本站立场,不承担相关法律责任。如若转载,请注明出处。 如若内容造成侵权/违法违规/事实不符,请点击【内容举报】进行投诉反馈!
