Innodbreadonly事務、MySQL5.7和Percona的事務改進

zhaiwx_yinfeng發表於2016-05-10

前言

只讀事務在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的場景
 


相關文章