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)判断事务是否是只读的;

        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;
        }
 
这里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; 

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的日志来看,包含以下改进:

 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
 
大概扫了下:
#只读事务不考虑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的场景
 

 

原创文章,转载请注明: 转载自Simple Life

本文链接地址: Innodb read only事务、MySQL5.7和Percona的事务改进

Post Footer automatically generated by wp-posturl plugin for wordpress.


Comments

Leave a Reply

Your email address will not be published. Name and email are required


Current month ye@r day *