前言
只读事务在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 selects3. Use a pool of read views4. Add MVCC class5. Use a trx_id to trx_t* map6. Keep the active trx_id_ts in a vector.7. Pre-allocate a small cache of record and table locks8. 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的场景