• 事务隔离等级及InnoDB实现简单总结


    一、数据库中事务的隔离等级
    这里首先要明确的是,这里的“隔离”都是在“事务”的基础上讨论的,通常的事务通过 start transaction开启,之后通过rollback或者commit来结束。由于大部分情况下对于mysql的操作都是单条语句的操作,我想大部分人在操作mysql的时候不是在操作测试数据就是在查询日志,除了一些专门做业务的开发来说,很少有人来关注事务方面的内容。对于那些对事务相对不是很熟悉的同学来说,通常应该对于系统常见的执行单位进程、线程、协程等应该是有些基础了解的。其实这个东西可以大致的类比为和这个像类似的概念,它们都涉及到了两个重要的基础概念:并发、隔离。对于数据库的隔离等级来说,是指当多个事务并发送执行的情况下不同事务对于数据库的修改对于其它数据库的影响。
    下面是wikipedia对于事务隔离等级的说明

    Isolation Levels, Read Phenomena and Locks[edit]

    Isolation Levels vs Read Phenomena[edit]

    Isolation levelDirty readsNon-repeatable readsPhantoms
    Read Uncommitted may occur may occur may occur
    Read Committed - may occur may occur
    Repeatable Read - - may occur
    Serializable - - -

    由于wiki的说明已经非常详细了,所以这里只是做个简单的注释。
    "Dirty reads":
        由于基于MVCC机制的数据库都是实时修改数据库中的记录,同时加上了版本控制(也就是MVCC中version中的意义),从而可以让不同的事务看到各自特有的一个版本信息。作为对比其实可以看到,myisam引擎就没有这种问题,因为myisam并没有使用MVCC功能,而是在操作的时候直接使用了锁表功能,这个锁定粒度大,所以并发性有较大限制。由于每个记录都是实时写入的,所以如果不加任何限制,那么一个事务修改之后、事务提交之前的内容其实是可以被其它事务看到的,所以这里看到的就可以认为是一个脏数据,因为这个事务还没有提交,所以这个数据并不是最终结果。
    "Non-repeatable reads":
        和前一个对比,当一个事务提交之后,事务提交的修改都完整一致的落入数据库。此时如果允许读取已提交的内容,那么此时两次相同的select语句(可能包涵sum、avg等其它聚合操作)在另一个事务中可能有不同的结构。当然,对于MVCC来说,它实现的代价并不比避免"Dirty reads"的代价更高,所有这个也是INNODB的默认事务隔离等级。
    "Phantoms":
        前两个限制的其实都是每个记录本身数据的内容,也就是对于某个已存在记录本身数值修改的影响,而这里的“幻读”则是对于查询结果集的一个限制。或者说前面还都是“各扫门前雪”的模式,而这个幻读的要求就是一个联防甚至连坐的概念。这个phantom单词比较生僻,它的本意是指幽灵之类的“怪力乱神”之类的东西。就好像你照了几张照片,回去看的时候,发现里面突然多了一个鬼脸,所以你吓了一身冷汗,当时喊了一声“有妖气”。其实这里的意思就是说“幻读”这个翻译本身和很多专业术语翻译一样,有些莫名其妙,比较接地气的翻译可能是“神出鬼没读”、“幽灵读”之类的翻译,当然这个翻译感觉更low了。为了给大家一个直观的印象,就好像这个图片里圆圈里的现象一样(图片来源):
    事务隔离等级及InnoDB实现简单总结 - Tsecer - Tsecer的回音岛
     
    二、InnoDB事务在读取记录时的判断
    mysql-5.1.61storageinnobase ow ow0sel.c
    row_search_for_mysql
    /* We are ready to look at a possible new index entry in the result
    set: the cursor is now placed on a user record */
     
    if (prebuilt->select_lock_type != LOCK_NONE) {
    ……
    } else {
    /* This is a non-locking consistent read: if necessary, fetch
    a previous version of the record */
     
    if (trx->isolation_level == TRX_ISO_READ_UNCOMMITTED) {
     
    /* Do nothing: we let a non-locking SELECT read the
    latest version of the record */
     
    } else if (index == clust_index) {
     
    /* Fetch a previous version of the row if the current
    one is not visible in the snapshot; if we have a very
    high force recovery level set, we try to avoid crashes
    by skipping this lookup */
     
    if (UNIV_LIKELY(srv_force_recovery < 5)
        && !lock_clust_rec_cons_read_sees(
        rec, index, offsets, trx->read_view)) {
     
    rec_t* old_vers;
    /* The following call returns 'offsets'
    associated with 'old_vers' */
    err = row_sel_build_prev_vers_for_mysql(
    trx->read_view, clust_index,
    prebuilt, rec, &offsets, &heap,
    &old_vers, &mtr);
     
    if (err != DB_SUCCESS) {
     
    goto lock_wait_or_error;
    }
     
    if (old_vers == NULL) {
    /* The row did not exist yet in
    the read view */
     
    goto next_rec;
    }
     
    rec = old_vers;
    }
    }
    当对DB遍历读取到一条记录的时候,通过lock_clust_rec_cons_read_sees(rec, index, offsets, trx->read_view)判断当前事务是否可以看到当前(访问)记录,这个函数比较关键的就是trx->read_view(当然记录本身rec也很重要)。
    三、记录对事务可见性的判断
    lock_clust_rec_cons_read_sees===>>>
     
    /*************************************************************************
    Checks that a record is seen in a consistent read. */
     
    ibool
    lock_clust_rec_cons_read_sees(
    /*==========================*/
    /* out: TRUE if sees, or FALSE if an earlier
    version of the record should be retrieved */
    rec_t* rec, /* in: user record which should be read or
    passed over by a read cursor */
    dict_index_t* index, /* in: clustered index */
    const ulint* offsets,/* in: rec_get_offsets(rec, index) */
    read_view_t* view) /* in: consistent read view */
    {
    ……
    /* NOTE that we call this function while holding the search
    system latch. To obey the latching order we must NOT reserve the
    kernel mutex here! */
     
    trx_id = row_get_rec_trx_id(rec, index, offsets);
     
    return(read_view_sees_trx_id(view, trx_id));
    }
    其中row_get_rec_trx_id函数的功能非常简单,就是读取一个记录中的trxid字段,这个字段是InnoDB中所有记录都存在的一个系统(控制)字段,这个字段虽然对于用户不可见的,但是它本身对于事务可见性的实现非常重要,另外也是隐式锁(implicit lock)的实现基础。这个字段表示的就是最后一个修改该记录的事务ID,而事务ID是可以保证在任意时间空间唯一的,即使重启之后新分配的事务ID也不会和之前的事务ID重复,所以这个事务ID本身也可以认为是一个记录的版本号信息,这个版本号直观的理解就是SVN的版本号,而这个版本号也就是InnoDB MVCC的实现基础。
    lock_clust_rec_cons_read_sees函数其实就是判断当前事务的readview是否可见这个记录,
    ===>>>lock_clust_rec_cons_read_sees===>>>read_view_sees_trx_id===>>>
    mysql-5.1.61storageinnobaseinclude ead0read.ic
    /*************************************************************************
    Checks if a read view sees the specified transaction. */
    UNIV_INLINE
    ibool
    read_view_sees_trx_id(
    /*==================*/
    /* out: TRUE if sees */
    read_view_t* view, /* in: read view */
    dulint trx_id) /* in: trx id */
    {
    ulint n_ids;
    int cmp;
    ulint i;
     
    if (ut_dulint_cmp(trx_id, view->up_limit_id) < 0) {
     
    return(TRUE);
    }
     
    if (ut_dulint_cmp(trx_id, view->low_limit_id) >= 0) {
     
    return(FALSE);
    }
     
    /* We go through the trx ids in the array smallest first: this order
    may save CPU time, because if there was a very long running
    transaction in the trx id array, its trx id is looked at first, and
    the first two comparisons may well decide the visibility of trx_id. */
     
    n_ids = view->n_trx_ids;
     
    for (i = 0; i < n_ids; i++) {
     
    cmp = ut_dulint_cmp(
    trx_id,
    read_view_get_nth_trx_id(view, n_ids - i - 1));
    if (cmp <= 0) {
    return(cmp < 0);
    }
    }
     
    return(TRUE);
    }
    四、事务readview的创建
    mysql-5.1.61storageinnobase ead ead0read.c
     
    /*************************************************************************
    Opens a read view where exactly the transactions serialized before this
    point in time are seen in the view. */
     
    read_view_t*
    read_view_open_now(
    /*===============*/
    /* out, own: read view struct */
    dulint cr_trx_id, /* in: trx_id of creating
    transaction, or (0, 0) used in
    purge */
    mem_heap_t* heap) /* in: memory heap from which
    allocated */
    {
    read_view_t* view;
    trx_t* trx;
    ulint n;
     
    ut_ad(mutex_own(&kernel_mutex));
     
    view = read_view_create_low(UT_LIST_GET_LEN(trx_sys->trx_list), heap);
     
    view->creator_trx_id = cr_trx_id;
    view->type = VIEW_NORMAL;
    view->undo_no = ut_dulint_create(0, 0);
     
    /* No future transactions should be visible in the view */
     
    view->low_limit_no = trx_sys->max_trx_id;
    view->low_limit_id = view->low_limit_no;
     
    n = 0;
    trx = UT_LIST_GET_FIRST(trx_sys->trx_list);
     
    /* No active transaction should be visible, except cr_trx */
     
    while (trx) {
    if (ut_dulint_cmp(trx->id, cr_trx_id) != 0
        && (trx->conc_state == TRX_ACTIVE
    || trx->conc_state == TRX_PREPARED)) {
     
    read_view_set_nth_trx_id(view, n, trx->id);
     
    n++;
     
    /* NOTE that a transaction whose trx number is <
    trx_sys->max_trx_id can still be active, if it is
    in the middle of its commit! Note that when a
    transaction starts, we initialize trx->no to
    ut_dulint_max. */
     
    if (ut_dulint_cmp(view->low_limit_no, trx->no) > 0) {
     
    view->low_limit_no = trx->no;
    }
    }
     
    trx = UT_LIST_GET_NEXT(trx_list, trx);
    }
     
    view->n_trx_ids = n;
     
    if (n > 0) {
    /* The last active transaction has the smallest id: */
    view->up_limit_id = read_view_get_nth_trx_id(view, n - 1);
    } else {
    view->up_limit_id = view->low_limit_id;
    }
     
     
    UT_LIST_ADD_FIRST(view_list, trx_sys->view_list, view);
     
    return(view);
    }
    这个地方是遍历系统所有(除了自己)活跃/准备事务,对于这些事务来说,这个readview是不可见的,因为它们还没有提交完成,所以等待它们提交之后,它们修改的数据对于readview不可见。明显地,对于大于trx_sys->max_trx_id(存储在view->low_limit_id中)的事务,readview均不可见(由于这些事务在该readview之后创建);另外,比所有活跃事务中最小事务ID(存储在view->up_limit_id中)还要小的事务肯定是已经提交完成了,所以肯定是可见的。对于在两者之间的事务,除了这里遍历的事务之外都已经提交,所以也是可见的;或者说view->low_limit_id和view->up_limit_id之间,除了这里遍历的事务之外都是可见的。这里其实也就是read_view_sees_trx_id中的逻辑流程。
    举个栗子:假设系统中最大事务ID为10,当前活跃事务ID为 2、4、6,那么此时说明事务ID小于10的事务除了2、4、6之外都已经提交完成(所以都是对readview可见),反之,对于事务ID大于等于10的事务readview不可见。对于这个判定方法如何表示呢?这里使用了一个区间加上一个枚举集合的表示方法,其中的区间就是2和10,而枚举的内容就是{2、4、6},判断方法就是trxid小于2的都可见,trxid大于10的都不可见,trxid在这个区间内的只要不和枚举集合相等就可见。
    五、可见记录内容的回滚
    row_sel_build_prev_vers_for_mysql===>>>row_vers_build_for_consistent_read===>>>trx_undo_prev_version_build
    roll_ptr = row_get_rec_roll_ptr(rec, index, offsets);
    old_roll_ptr = roll_ptr;
     
    *old_vers = NULL;
     
    if (trx_undo_roll_ptr_is_insert(roll_ptr)) {
     
    /* The record rec is the first inserted version */
     
    return(DB_SUCCESS);
    }
     
    rec_trx_id = row_get_rec_trx_id(rec, index, offsets);
     
    err = trx_undo_get_undo_rec(roll_ptr, rec_trx_id, &undo_rec, heap);
     
    if (err != DB_SUCCESS) {
     
    return(err);
    }
     
    ptr = trx_undo_rec_get_pars(undo_rec, &type, &cmpl_info,
        &dummy_extern, &undo_no, &table_id);
     
    ptr = trx_undo_update_rec_get_sys_cols(ptr, &trx_id, &roll_ptr,
           &info_bits);
    ptr = trx_undo_rec_skip_row_ref(ptr, index);
     
    ptr = trx_undo_update_rec_get_update(ptr, index, type, trx_id,
         roll_ptr, info_bits,
         NULL, heap, &update);
    这里的实现同样是利用了记录中对于用户不可见的ROLLPTR字段,这个字段记录了对于这个记录的修改历史,同样和svn类比,就是一个记录的修改历史,或者更学术化的说法就是"操作子",有了这些信息就可以逐步回滚该记录,回滚出的记录中同样包涵了前面说到的最后一个修改事务的trxid,这样问题就转变成了一个循环问题。大致如此。
    六、简单例子
    1、测试数据
    创建一个非常简单的table,里面只有一行数据
    mysql> insert tsecer values(1,1);
    Query OK, 1 row affected (0.00 sec)
     
    mysql> select * from tsecer;
    +------+------+
    | k    | v    |
    +------+------+
    |    1 |    1 |
    +------+------+
    1 row in set (0.00 sec)
     
    mysql> 
    2、三个终端
    前两个分别设置为READ UNCOMMITTED和READ COMMITTED,最后一个在事务中更新字段,可以看到事务隔离等级为READ UNCOMMITTED马上看到了修改之后的数值,而READ COMMITTED看到的依然是事务开始前的数值。
     
    终端1:
    mysql> start transaction;
    Query OK, 0 rows affected (0.00 sec)
     
    mysql> update tsecer set v=2 where k=1;
    Query OK, 1 row affected (0.00 sec)
    Rows matched: 1  Changed: 1  Warnings: 0
     
    mysql> 
    终端2:
    mysql> set session transaction isolation level READ UNCOMMITTED;
    Query OK, 0 rows affected (0.00 sec)
     
    mysql> start transaction;
    Query OK, 0 rows affected (0.00 sec)
     
    mysql> select * from tsecer;                                    
    +------+------+
    | k    | v    |
    +------+------+
    |    1 |    2 |
    +------+------+
    1 row in set (0.00 sec)
     
    mysql> 
    终端3:
    mysql> set session transaction isolation level READ COMMITTED;
    Query OK, 0 rows affected (0.00 sec)
     
    mysql> start transaction;
    Query OK, 0 rows affected (0.00 sec)
     
    mysql> select * from tsecer;
    +------+------+
    | k    | v    |
    +------+------+
    |    1 |    1 |
    +------+------+
    1 row in set (0.00 sec)
    3、InnoDB的默认事务隔离级别
    mysql> show session variables like 'tx_isolation';
    +---------------+----------------+
    | Variable_name | Value          |
    +---------------+----------------+
    | tx_isolation  | READ-COMMITTED |
    +---------------+----------------+
    1 row in set (0.00 sec)
     
    mysql> show global variables like 'tx_isolation'; 
    +---------------+-----------------+
    | Variable_name | Value           |
    +---------------+-----------------+
    | tx_isolation  | REPEATABLE-READ |
    +---------------+-----------------+
    1 row in set (0.00 sec)
     
    mysql> 
    从这个地方看,InnoDB的默认隔离级别是REPEATABLE-READ。
    mysql-5.1.61sqlmysqld.cc
    static int mysql_init_variables(void)
    {
    ……
      global_system_variables.table_plugin= NULL;
      global_system_variables.tx_isolation= ISO_REPEATABLE_READ;
      global_system_variables.select_limit= (ulonglong) HA_POS_ERROR;
    ……
    也就是说,这个ISO_REPEATABLE_READ是系统默认的事务隔离级别。
  • 相关阅读:
    git(重点)
    C#技巧记录——持续更新
    结点和节点的区别
    WebSocketSharp send record_stop without send record_start
    cefsharp 拦截所有请求 解决chunked导致数据接收不完整的问题
    计算mp3长度 毫秒
    pydub分割音频文件
    c# 获取文件信息
    实现一边写代码一边展示网页的效果
    c# webapi swagger Area 多级层次分组 添加header参数
  • 原文地址:https://www.cnblogs.com/tsecer/p/10487938.html
Copyright © 2020-2023  润新知