zoukankan      html  css  js  c++  java
  • mysql事务隔离分析

    首先说明下,这里主要内容为整理总结网络搜索的零散信息。

    写在最前面,mysql事务是在Innodb引擎中得以实现的,如果这点不了解的话,请自行了解。

    事务直接数据的可见性通过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: 主要用来判断当前版本数据的可见性。

    下面看下一条记录的更新过程:

    1.初始数据行

    F1~F6是某行列的名字,1~6是其对应的数据。后面三个隐含字段分别对应该行的事务号和回滚指针,假如这条数据是刚INSERT的,可以认为ID为1,其他两个字段为空。
    2.事务1更改该行的各字段的值

    当事务1更改该行的值时,会进行如下操作:
    用排他锁锁定该行
    记录redo log
    把该行修改前的值Copy到undo log,即上图中下面的行
    修改当前行的值,填写事务编号,使回滚指针指向undo log中的修改前的行
    3.事务2修改该行的值

    下面讲下调用select时mysql到底做了什么:

    首先,看下read_view结构:

    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

  • 相关阅读:
    SpringBoot连接数据库
    String、StringBuffer、StringBulider的区别和解析
    异常This application has no explicit mapping for /error
    node使用
    JS总结defer与async(一)
    前端项目搭建与知识框架
    git ssh配置总结
    JS算法
    JS数据结构
    Http与Http2与Https区别和联系
  • 原文地址:https://www.cnblogs.com/restart30/p/11739464.html
Copyright © 2011-2022 走看看