MYSQL(一) 事务(1)基础

本文介绍了数据库事务的ACID特性,详细讲解了MySQL的四种事务隔离级别,并重点阐述了多版本并发控制(MVCC)的概念,以及在可重复读(RR)隔离级别下MVCC的工作原理,包括readview的创建、数据的可见性判断和源码分析。
摘要由CSDN通过智能技术生成

什么是事务:

事务就是由一组SQL组成,或者一个独立的工作单元。

1.事务ACID

原子性(atomicity):
原子性是指事务包含的所有操作要么全部成功,要么全部失败回滚。因此事务操作如果成功就必须要完全应用到数据库,如果操作失败则不能对数据库有任何影响
一致性(consistency):
一致性是指事务必须使数据库从一个一致性状态转换到另一个一致性状态,也就是说一个事务执行之前和执行之后都必须处于一致性状态。
隔离性(isolation):
隔离性是当多个用户并发访问数据库时,比如操作同一张表时,数据库为每一个开启的事务,不能被其他事务的操作所干扰,多个并发事务之间要相互隔离。
持久性(durability)
持久性是指一个事物一旦被提交了,那么对数据库中数据的改变就是永久性的,即便是在数据库系统遇到故障的情况下也不会丢失提交事务的操作。

2.事务的隔离级别

REPEATABLE READ:这是mysql 默认的隔离级别,能不可重复读,其实在这种隔离级别下,脏读也能避免一部分。一个事务执行过程中看到的数据,总是跟这个事务在启动时看到的数据是一致的

READ COMMITTED:一个事务提交之后,它做的变更才会被其他事务看到

READ UNCOMMITTED:一个事务还没提交时,它做的变更就能被别的事务看到。

SERIALIZABLE:串行化 ,可避免脏读、不可重复读、幻读的发生,后访问的事务必须等前一个事务执行完成,才能继续执行

3.多版本控制(MVCC):

一种提高并发的技术。最早的数据库系统,只有读读之间可以并发,读写,写读,写写都要阻塞。引入多版本之后,只有写写之间相互阻塞,其他三种操作都可以并行,这样大幅度提高了InnoDB的并发度。在内部实现中,与Postgres在数据行上实现多版本不同,InnoDB是在undolog中实现的,通过undolog可以找回数据的历史版本。找回的数据历史版本可以提供给用户读(按照隔离级别的定义,有些读请求只能看到比较老的数据版本),也可以在回滚的时候覆盖数据页上的数据。在InnoDB内部中,会记录一个全局的活跃读写事务数组,其主要用来判断事务的可见性。
这边主要的两个组成是:

a.readview 全局的事务视图来判断可见性
b.undo日志,用于记录数据的变化

MVCC详解

1.实现原理

MVCC在默认的rr隔离级别下,就是对数据库做了一次快照,但是这个快照是记录的全局的事务ID。这个事务id是全局的,而且是递增的。每行记录也有自己的row_trx_id .
每个事务在启动的时候会,会构造一个事务数组,主要是记录这个开启瞬间的事务活跃情况。ID最小的是低水位,最大的值+1是高水位。这个数据其实就是我们所说的readview。
而每行记录会有2或者3个隐藏的字段,手册描述如下

Records in the clustered index contain fields for all user-defined columns. In addition, there is a 6-byte transaction ID field and a 7-byte roll pointer field.
If no primary key was defined for a table, each clustered index record also contains a 6-byte row ID field

包含一个行的事务ID 和undo的指针,如果是没有主键的表,还会包含row Id。

结构readview 和row_trx_id就可以判断数据对于当前事务的可见性:

在这里插入图片描述

如图所示:
1.如果是< m_up_limit_id,则不可见
2.如果>m_low_limit_id,则可见。
3.如果 m_up_limit_id<row_trx_id<m_low_limit_id, 在活跃的数组中,则,不可见,如果不在活跃的数据组中则可见。
其实我的理解,一般的高水位,应该就是本次的事务id+1。

更简单的理解就是,在事务开始前的未提交的不可见。已提交的可见。
2.什么时候创建readview

当然这些讨论都是在RR模式下的。
a. 开启一个事务后,第一个select,会创建的一个readview
b. start transaction with consistent snapshot开始事务时

3.一些源码

mysql-5.7.25\storage\innobase\include\read0types.h readview的类,里面一些方法和变量说明
mysql-5.7.26\storage\innobase\include\row0sel.h

private:
    /** The read should not see any transaction with trx id >= this
    value. In other words, this is the "high water mark". */
    trx_id_t    m_low_limit_id;
    /** The read should see all trx ids which are strictly
    smaller (<) than this value. In other words, this is the
    low water mark". */
    trx_id_t    m_up_limit_id;
    /** trx id of creating transaction, set to TRX_ID_MAX for free
    views. */
    trx_id_t    m_creator_trx_id;
    /** Set of RW transactions that was active when this snapshot
    was taken */
    ids_t       m_ids;
    /** The view does not need to see the undo logs for transactions
    whose transaction number is strictly smaller (<) than this value:
    they can be removed in purge if not needed by other views */
    trx_id_t    m_low_limit_no;
    /** AC-NL-RO transaction view that has been "closed". */
    bool        m_closed;
    typedef UT_LIST_NODE_T(ReadView) node_t;
    /** List of read views in trx_sys */
    byte        pad1[64 - sizeof(node_t)];
    node_t      m_view_list;
};

函数 row_search_mvcc 是读取的入口


if (trx->isolation_level == TRX_ISO_READ_UNCOMMITTED) {
//如果是 READ UNCOMMITTED则什么都不错
            /* Do nothing: we let a non-locking SELECT read the
            latest version of the record */
        } else if (index == clust_index) {
//是聚簇索引的情况,查看每条记录的row_trx_id,判断可见性
            /* 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 (srv_force_recovery < 5
             && !lock_clust_rec_cons_read_sees(
                 rec, index, offsets,
                 trx_get_read_view(trx))) {
                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, need_vrow ? &vrow : NULL,
                    &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;
                prev_rec = rec;
            }
        } else {
            /* We are looking into a non-clustered index,
            and to get the right version of the record we
            have to look also into the clustered index: this
            is necessary, because we can only get the undo
            information via the clustered index record. */
//这是非聚簇索引的情况,但是判断可见性也是需要通过主键里面的隐藏字段 行的trx_id 来判断
            ut_ad(!dict_index_is_clust(index));
            if (!srv_read_only_mode
             && !lock_sec_rec_cons_read_sees(
                    rec, index, trx->read_view)) {
                /* We should look at the clustered index.
                However, as this is a non-locking read,
                we can skip the clustered index lookup if
                the condition does not match the secondary
                index entry. */
//拿到记录会先调用lock_sec_rec_cons_read_sees判断page上记录的最近一次修改trx id是否小于m_up_limit_id,
//如果小于即该page上数据可见,否则即调用row_search_idx_cond_check检查可见性,只对匹配进行回表,读取row trx_id,之后的判断一样
                switch (row_search_idx_cond_check(
                        buf, prebuilt, rec, offsets)) {
                case ICP_NO_MATCH:
                    goto next_rec;
                case ICP_OUT_OF_RANGE:
                    err = DB_RECORD_NOT_FOUND;
                    goto ;
                case ICP_MATCH:
                    goto requires_clust_rec;
                }
                ut_error;
            }
        }



srv_force_recovery 这个也很关键:
可以在storage\innobase\include\srv0srv.h

trx_get_read_view:是创建活跃事务的readview



找到对应说明:
/** Alternatives for srv_force_recovery. Non-zero values are intended
to help the user get a damaged database up so that he can dump intact
tables and rows with SELECT INTO OUTFILE. The database must not otherwise
be used with these options! A bigger number below means that all precautions
of lower numbers are included. */
enum {
    SRV_FORCE_IGNORE_CORRUPT = 1,   /*!< let the server run even if it
                    detects a corrupt page */
    SRV_FORCE_NO_BACKGROUND = 2,    /*!< prevent the main thread from
                    running: if a crash would occur
                    in purge, this prevents it */
    SRV_FORCE_NO_TRX_UNDO = 3,  /*!< do not run trx rollback after
                    recovery */
    SRV_FORCE_NO_IBUF_MERGE = 4,    /*!< prevent also ibuf operations:
                    if they would cause a crash, better
                    not do them */
    SRV_FORCE_NO_UNDO_LOG_SCAN = 5, /*!< do not look at undo logs when
                    starting the database: InnoDB will
                    treat even incomplete transactions
                    as committed */
    SRV_FORCE_NO_LOG_REDO = 6   /*!< do not do the log roll-forward
                    in connection with recovery */
};


一些说明:

这边小于5的意思 其实就是 innodb_force_recovery 参数,默认是0:执行所有的恢复
innodb_force_recovery可以设置为1-6,大的数字包含前面所有数字的影响。
当设置参数值大于0后,可以对表进行select,create,drop操作,但insert,update或者delete这类操作是不允许的。
1(SRV_FORCE_IGNORE_CORRUPT):忽略检查到的corrupt页
2(SRV_FORCE_NO_BACKGROUND):阻止主线程的运行,如主线程需要执行full purge操作,会导致crash
3(SRV_FORCE_NO_TRX_UNDO):不执行事务回滚操作。
4(SRV_FORCE_NO_IBUF_MERGE):不执行插入缓冲的合并操作。
5(SRV_FORCE_NO_UNDO_LOG_SCAN):不查看重做日志,InnoDB存储引擎会将未提交的事务视为已提交。
6(SRV_FORCE_NO_LOG_REDO):不执行前滚的操作。

对应的判断可见性的函数:

/*********************************************************************//**
Checks that a record is seen in a consistent read.
@return true if sees, or false if an earlier version of the record
should be retrieved */
bool
lock_clust_rec_cons_read_sees(
/*==========================*/
    const 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) */
    ReadView*   view)   /*!< in: consistent read view */
{
    ut_ad(dict_index_is_clust(index));
    ut_ad(page_rec_is_user_rec(rec));
    ut_ad(rec_offs_validate(rec, index, offsets));
    /* Temp-tables are not shared across connections and multiple
    transactions from different connections cannot simultaneously
    operate on same temp-table and so read of temp-table is
    always consistent read. */
//临时表总是一致的。
    if (srv_read_only_mode || dict_table_is_temporary(index->table)) {
        ut_ad(view == 0 || dict_table_is_temporary(index->table));
        return(true);
    }
    /* NOTE that we call this function while holding the search
    system latch. */
//row_get_rec_trx_id获取行隐含列的row_trx_id,有兴趣可以看看这个怎么获得的。
    trx_id_t    trx_id = row_get_rec_trx_id(rec, index, offsets);
//真正判断可见性的是changes_visible,就是比较trx_id 和当前readview 数组
    return(view->changes_visible(trx_id, index->table->name));
}

对应的changes_visible对应的是在 readview class里面:

bool changes_visible(
        trx_id_t        id,
        const table_name_t& name) const
        MY_ATTRIBUTE((warn_unused_result))
    {
        ut_ad(id > 0);
        if (id < m_up_limit_id || id == m_creator_trx_id) {
//小于低水位或者是当前的事务,可见
            return(true);
        }
//检查看事务是否有效,但是这里的作用是什么不太明白
        check_trx_id_sanity(id, name);
//如果大于高水位的不可见。
        if (id >= m_low_limit_id) {
            return(false);
        } else if (m_ids.empty()) {
//其他情况,活跃数组中空,都可见
            return(true);
        }
        const ids_t::value_type*    p = m_ids.data();
//这一步应该判断在高水位和低水位中间,且m_ids存在活跃事务的情况,是否在行的trx_id是否在活跃事务数组中。如果在
//返回可见,不在则不可见
        return(!std::binary_search(p, p + m_ids.size(), id));
    }

readview的重要的结构:


private:
    /** The read should not see any transaction with trx id >= this
    value. In other words, this is the "high water mark". */
//高水位
    trx_id_t    m_low_limit_id;
    /** The read should see all trx ids which are strictly
    smaller (<) than this value. In other words, this is the
    low water mark". */
//低水位
    trx_id_t    m_up_limit_id;
    /** trx id of creating transaction, set to TRX_ID_MAX for free
    views. */
//创建当前事务的 trx_id
    trx_id_t    m_creator_trx_id;
    /** Set of RW transactions that was active when this snapshot
    was taken */
//活跃事务id 列表
    ids_t       m_ids;
    /** The view does not need to see the undo logs for transactions
    whose transaction number is strictly smaller (<) than this value:
    they can be removed in purge if not needed by other views */
    trx_id_t    m_low_limit_no;
    /** AC-NL-RO transaction view that has been "closed". */
    bool        m_closed;
    typedef UT_LIST_NODE_T(ReadView) node_t;
    /** List of read views in trx_sys */
    byte        pad1[64 - sizeof(node_t)];
    node_t      m_view_list;
};

参考

https://www.cnblogs.com/zengkefu/p/5678361.html
http://mysql.taobao.org/monthly/2018/11/04/

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值