mysql事務隔離分析

首先說明下,這裏主要內容爲整理總結網絡搜索的零散信息。mysql

寫在最前面,mysql事務是在Innodb引擎中得以實現的,若是這點不瞭解的話,請自行了解。sql

事務直接數據的可見性經過MVCC(多版本併發控制)實現。對同一記錄的修改會保存歷史版本的數據,經過一系列的邏輯看判斷當前事務應該獲取的是那個版本的數據,也就是一般意義上的可見性。數據庫

Innodb會爲每行記錄添加三個隱形字段:6字節的事務IDDB_TRX_ID)、7字節的回滾指針(DB_ROLL_PTR)、隱藏的ID。數組

MVCC 在mysql 中的實現依賴的是 undo log 與 read view。網絡

a.undo log: undo log中記錄的是數據表記錄行的多個版本,也就是事務執行過程當中的回滾段,其實就是MVCC 中的一行原始數據的多個版本鏡像數據。併發

b.read view: 主要用來判斷當前版本數據的可見性。性能

下面看下一條記錄的更新過程:ui

1.初始數據行

F1~F6是某行列的名字,1~6是其對應的數據。後面三個隱含字段分別對應該行的事務號和回滾指針,假如這條數據是剛INSERT的,能夠認爲ID爲1,其餘兩個字段爲空。
2.事務1更改該行的各字段的值

當事務1更改該行的值時,會進行以下操做:
用排他鎖鎖定該行
記錄redo log
把該行修改前的值Copy到undo log,即上圖中下面的行
修改當前行的值,填寫事務編號,使回滾指針指向undo log中的修改前的行
3.事務2修改該行的值

下面講下調用select時mysql到底作了什麼:spa

首先,看下read_view結構:.net

struct read_view_t{
    // 因爲是逆序排列,因此low/up有所顛倒
    // 能看到當前行版本的高水位標識,>= low_limit_id皆不能看見,low_limit_id取值爲max_trx_id(即還沒有被分配的trx_id)
    trx_id_t    low_limit_id;
    // 能看到當前行版本的低水位標識,< up_limit_id皆能看見,up_limit_id取值爲當前活躍最小事務id
    trx_id_t    up_limit_id;
    // 當前活躍事務(即未提交的事務)的數量
    ulint        n_trx_ids;
    // 以逆序排列的當前獲取活躍事務id的數組
    // 其up_limit_id<tx_id<low_limit_id
    trx_id_t*    trx_ids;    
    // 建立當前視圖的事務id
    trx_id_t    creator_trx_id;
    // 事務系統中的一致性視圖鏈表
    UT_LIST_NODE_T(read_view_t) view_list;
};

read_view構建邏輯:

在mysql的trx_sys中,一直維護着一個全局的活躍的讀寫事務id(trx_sys->descriptors),id按照從小到大排序,表示在某個時間點,數據庫中全部的活躍(已經開始但還沒提交)的讀寫(必須是讀寫事務,只讀事務不包含在內)事務。當須要一個一致性讀的時候(即建立新的readview時),會把全局讀寫事務id拷貝一份到readview本地(read_view_t->trx_ids),當作當前事務的快照。read_view_t->up_limit_id是read_view_t->trx_ids這數組中最小的值,read_view_t->low_limit_id是建立readview時的max_trx_id(即還沒有被分配的trx_id,這樣在>=判斷時就能夠將讀事務開啓後提交的事務包含進來)即必定大於read_view_t->trx_ids中的最大值。當查詢出一條記錄後(記錄上有一個trx_id,表示這條記錄最後被修改時的事務id),可見性判斷的邏輯以下(read_view_sees_trx_id):

1.若是記錄上的trx_id小於read_view_t->up_limit_id,則說明這條記錄的最後修改在readview建立以前,所以這條記錄能夠被看見。

2.若是記錄上的trx_id大於等於read_view_t->low_limit_id,則說明這條記錄的最後修改在readview建立以後,所以這條記錄確定不能夠被看見。

3.若是記錄上的trx_id在up_limit_id和low_limit_id之間,且trx_id在read_view_t->trx_ids之中,則表示這條記錄的最後修改是在readview建立之時,被另一個活躍事務所修改,因此這條記錄也不能夠被看見。若是trx_id不在read_view_t->trx_ids之中,則表示這條記錄的最後修改在readview建立以後被提交,因此能夠看到。

注意當隔離級別設置爲READ UNCOMMITTED時,不會去構建老版本。

判斷行記錄可見行源碼以下:

/*********************************************************************//**
Checks if a read view sees the specified transaction.
@return    true if sees */
UNIV_INLINE
bool
read_view_sees_trx_id(
/*==================*/
    const read_view_t*    view,    /*!< in: read view */
    trx_id_t        trx_id)    /*!< in: trx id */
{
    if (trx_id < view->up_limit_id) {

        return(true);
    } else if (trx_id >= view->low_limit_id) {

        return(false);
    } else {
        ulint    lower = 0;
        ulint    upper = view->n_trx_ids - 1;

        ut_a(view->n_trx_ids > 0);

        do {
            ulint        mid    = (lower + upper) >> 1;
            trx_id_t    mid_id    = view->trx_ids[mid];

            if (mid_id == trx_id) {
                return(FALSE);
            } else if (mid_id < trx_id) {
                if (mid > 0) {
                    upper = mid - 1;
                } else {
                    break;
                }
            } else {
                lower = mid + 1;
            }
        } while (lower <= upper);
    }

    return(true);
}

4.基於上述判斷,若是記錄不可見,則嘗試使用undo去構建老的版本(row_vers_build_for_consistent_read),直到找到能夠被看見的記錄或者解析完全部的undo,代碼以下:

dberr_t row_vers_build_for_consistent_read(...)
{
    ......
    for(;;){
        err = trx_undo_prev_version_build(rec, mtr,version,index,*offsets, heap,&prev_version);
        ......
        trx_id = row_get_rec_trx_id(prev_version, index, *offsets);
        // 若是當前row版本符合一致性視圖,則返回
        if (read_view_sees_trx_id(view, trx_id)) {
            ......
            break;
        }
        // 若是當前row版本不符合,則繼續回溯上一個版本(回到for循環的地方)
        version = prev_version;
    }
    ......
}

可見性分析如上已經差很少了,那麼,不一樣隔離級別是怎麼利用readview達到效果的呢?

針對RR隔離級別,在第一次建立readview(第一次調用select(不加鎖))後,這個readview就會一直持續到事務結束,也就是說在事務執行過程當中,數據的可見性不會變,因此在事務內部不會出現不一致的狀況。針對RC隔離級別,事務中的每一個查詢語句都單獨構建一個readview,因此若是兩個查詢之間有事務提交了,兩個查詢讀出來的結果就不同。從這裏能夠看出,在InnoDB中,RR隔離級別的效率是比RC隔離級別的高。此外,針對RU隔離級別,因爲不會去檢查可見性,因此在一條SQL中也會讀到不一致的數據。針對串行化隔離級別,InnoDB是經過鎖機制來實現的,而不是經過多版本控制的機制,因此性能不好。

 由下面代碼可知,只有單純的select才建立readview,select for update會加鎖因此不會建立readview。

// 只有非鎖模式的select才建立一致性視圖
else if (prebuilt->select_lock_type == LOCK_NONE) {        // 建立一致性視圖
        trx_assign_read_view(trx);
        prebuilt->sql_stat_start = FALSE;
}

也可參考下面描述:

 

參考資料:

https://my.oschina.net/alchemystar/blog/1927425

http://mysql.taobao.org/monthly/2017/12/01/

http://mysql.taobao.org/monthly/2015/12/01/

https://yq.aliyun.com/articles/560506

相關文章
相關標籤/搜索