innodb事务系统 - MVCC

author:sufei

源码版本:8.0.16


 阅读过本系列文章中的《事务的启动与提交》,我们已经知道了线程本地的核心结构体为trx_t,其主要保存着该会话事务相关的所有信息,当然也包括本节需要讲解的read_view(一致性视图结构体);innodb事务核心系统trx_sys_t,其维护innodb的事务系统,其中包含本节主要分析的mvcc。

 MVCC是保证多事务并发处理过程中,实现事物之间隔离的一种技术。本文主要分析innodb引擎实现MVCC(多版本控制)细节。

一、Innodb MVCC原理

 innodb存储引擎会对每行记录添加三个字段:

  • 6字节的事务ID(TRX_ID )

该字段主要标识修改该行的事务id。用于多版本并发控制的可见性判断

  • 7字节的回滚指针(ROLL_PTR)

该字段是标识指向undo回滚段该行以前的版本,这样同一行数据就形成了一个链表形成,越老的数据越在链表的后面。如下图所示。

  • 隐藏的ROW_ID (无主键时)
image

 对于同一行数据(ROW_ID=1),第一次修改是事务id号为2的事务进行了插入;第二次则是修改该行,是由事务id号为3的事务进行的,将原来的行记录放入undo回滚段,并修改本行的ROLL_PTR指针指向它;第三次修改该行的事务号为5,同样将原来的行记录放入undo回滚段,并修改本行的ROLL_PTR指针指向它,从而形成一个事务版本的链表。只有当历史版本没有活跃的事务关联时(也就是没有活跃事务需要查看),由清除线程进行历史版本清除工作。

因为事务id号是单调递增的,依照事物的版本来检查每行的版本号,从而判断行的可见性。

 下面通过源码分析来具体说明innodb mvcc的实现。

二、核心数据结构

2.1 ReadView类

 该类是每个THD线程结构持有的一致性视图类,是事务实现一致性读视图的基本结构,用于识别哪些行对本事务是可见的,哪些行对本事务是不可见的。

主要成员变量

成员名 作用
m_low_limit_id 表示创建此视图结构时,系统此时最大的事务id+1(也即下一个事务的id,即trx_sys->max_trx_id值),从而如果行记录的事务id只要大于等于此值,则对其不可见
m_up_limit_id 这个变量保存着结构创建时正在运行中事务中的最小事物id,说明小于此事务id的行对本结构都是可见的。
m_creator_trx_id 初始化时,就是事物本身id
m_ids 视图创建时,系统所有活跃的事务id集合。通过该集合我们知道哪些事务,在我们开启视图时正在执行。
m_low_limit_no 小于该值的行不需要查看undo log,直接读取即可。该值初始化与m_up_limit_id一致,除非当事务trx->no更小时,初始化为trx->no。

 从核心成员变量可以看出:

 其实整个事物可见性就是通过在事务创建时构建readview结构来实现的,其中形成一个[m_up_limit_id,
m_low_limit_id]
区间.

  • 如果row的事务id大于等于m_low_limit_id,对事物不可见,因为修改该行的事务是本身事务之后的未来事物;
  • 如果小于m_up_limit_id,对事物可见,是因为事物开始时,该事务已经提交了。
  • 如果在这之间,我们可以通过m_ids变量,如果row的事务id在m_ids中,说明事务开始时,该行还没有提交,不可见。如果row事务id不在m_ids中,说明事务开始时,该行已经提交,即可见。

主要成员函数

  • 初始化创建函数

void prepare(trx_id_t id)

该行是进行初始化readview结构的函数,主要就是初始化上述变量,在创建了新的readview结构体时,需要调用。

void copy_prepare(const ReadView &other);

void copy_complete();

这对函数与上对函数功能相似,不同的是从另一个readview结构构建而已。需要成对出现。

 上面的函数功能上是一致的,这里主要分析prepare函数,理解innodb是如何创建初始化readview结构体内相关变量的。

/*
该函数时初始化一个readview结构体成员。
参数:
    id  该事务的id
*/
void ReadView::prepare(trx_id_t id) {
  ut_ad(mutex_own(&trx_sys->mutex));
  m_creator_trx_id = id;  //为m_creator_trx_id赋值自身事务id
  /*
  初始化m_low_limit_no,m_low_limit_id,m_up_limit_id都为现在系统最大的事务id,当然后面还需要对
  这些值进行调整。
  */
  m_low_limit_no = m_low_limit_id = m_up_limit_id = trx_sys->max_trx_id;
  /*
  如果此时系统有活跃的读写事务
  1、调用copy_trx_ids成员函数将其id集合拷贝到成员变量m_ids中;
  2、修改m_up_limit_id为活跃事务中的最小id
  */
  if (!trx_sys->rw_trx_ids.empty()) { 
    copy_trx_ids(trx_sys->rw_trx_ids);
  } else {
    m_ids.clear();
  }

  ut_ad(m_up_limit_id <= m_low_limit_id);
  //如果事务系统的事务提交serialisation_list列表不为空,修改m_low_limit_no为最小的提交no
  if (UT_LIST_GET_LEN(trx_sys->serialisation_list) > 0) {
    const trx_t *trx;
    trx = UT_LIST_GET_FIRST(trx_sys->serialisation_list);
    if (trx->no < m_low_limit_no) {
      m_low_limit_no = trx->no;
    }
  }

  m_closed = false;
}

 通过查看源代码,我们看到了核心成员的初始化过程。下面主要看可见性检测函数。

  • 可见性检测函数

void check_trx_id_sanity(trx_id_t id, const table_name_t &name)

该函数就是检测表name中的行记录id是否合法,主要是与系统最大是事物id比较,如果比之大,说明这个事物id是无效的。主要被成员函数changes_visible调用,用于检测一致性视图事物id后是否合法有效。

bool changes_visible(trx_id_t id, const table_name_t &name) const

最主要的函数,用于判断参数给定的事务id,在此事务中是否可见。精简代码如下:

bool changes_visible(trx_id_t id, const table_name_t &name) const
      MY_ATTRIBUTE((warn_unused_result)) {
    ut_ad(id > 0);
    /*
    如果给定row的事务id小于m_up_limit_id,则说明该行是事务开始前就已经提交了
    如果给定row的事务id等于m_creator_trx_id,说明该行是自身修改的记录
    以上两种情况放回true,可见
    */
    if (id < m_up_limit_id || id == m_creator_trx_id) {
      return (true);
    }
    
    check_trx_id_sanity(id, name);  //检测给定row的事务id是否合法
    
    //如果id>= m_low_limit_id,说明该行是事务开始之后,修改的行,不可见
    if (id >= m_low_limit_id) {
      return (false);
    } 
    /*
    此时id只能在[m_up_limit_id,m_low_limit_id)区间,
    如果开始事务时,没有活跃状态的读写事务,则可见
    */
    else if (m_ids.empty()) { 
      return (true);
    }
    //最后检测id是否在活跃事务列表中,如果在,则不可见,如果不在,则可见
    const ids_t::value_type *p = m_ids.data();
    return (!std::binary_search(p, p + m_ids.size(), id));
  }

2.2 MVCC类

 MVCC是实现一致性读的核心结构,本质上是readview的管理类,主要管理所有的readview结构体。innodb事务管理类trx_sys_t通过MVCC类来实现一致性视图管理。下面对MVCC类的简要分析

主要成员变量

成员名 作用
m_free 用于维护空闲的ReadView对象,初始化时创建1024个ReadView对象(trx_sys_create),当释放一个活跃的视图时,会将其加到该链表上,以便下次重用
m_views 这里存储了两类视图,一类是当前活跃的视图,另一类是上次被关闭的只读事务视图。后者主要是为了减少视图分配开销。因为当系统的读占大多数时,如果在两次查询中间没有进行过任何读写操作,那我们就可以重用这个ReadView,而无需去持有trx_sys->mutex锁重新分配;

主要成员函数

成员函数 作用
get_view() 从m_free列表获得一个新的readview结构或者额外新建一个
view_open(view,trx) 通过调用get_view(),view-> prepare, view-> copy_trx_ids构建新的view,并初始化。
view_release(view) 释放一个readview,从m_views列表删除,移到m_free列表中
size() 返回m_views中,非关闭的视图个数

三、Innodb MVCC实现过程

 下面我们通过从视图的创建,可见性判断,以及隔离级别的影响三个方面来分析Innodb MVCC实现细节。

3.1 会话的视图创建

 在innodb内部,通过trx_assign_read_view函数,为本身事务trx_t创建一个一致性视图结构readview。集体逻辑如下:

/*
该函数是为innodb层事务结构体trx_t创建一个一致性试图readview
*/
ReadView *trx_assign_read_view(trx_t *trx) /*!< in/out: active transaction */
{
  //确保事务状态是active  
  ut_ad(trx->state == TRX_STATE_ACTIVE);
  //如果服务器是只读状态,则不需要创建一致性视图
  if (srv_read_only_mode) {
    ut_ad(trx->read_view == NULL);
    return (NULL);
  } else if (!MVCC::is_view_active(trx->read_view)) {
    //从全局事务视图管理类mvcc获取一个一致性视图结构readview
    trx_sys->mvcc->view_open(trx->read_view, trx);
  }

  return (trx->read_view);
}

 注意:上述函数的调用并不是服务层开启事务(如执行begin语句)时,或者innodb层事务激活(调用trx_start_low)时。而是在真正操作或者查看数据库数据时才为其创建该视图结构。

3.2 可见性判断

 对于记录的可见性判断,是在函数lock_clust_rec_cons_read_sees中,该函数主要是判断给定的记录对本事务是否可见,如果不可见,则需要进一步查找更早的版本。

/*
该函数检测一条记录是否可见,返回true为可见,返回false为不可见,需进一步查找更早的版本
*/
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(index->is_clustered());
  ut_ad(page_rec_is_user_rec(rec));
  ut_ad(rec_offs_validate(rec, index, offsets));

  /*
  由于临时表是各个连接私有的,不同连接是不能相互访问各自的临时表,所以
  1、服务器为只读模式
  2、访问的是临时表
  以上情况都是可见的
  */
  if (srv_read_only_mode || index->table->is_temporary()) {
    ut_ad(view == 0 || index->table->is_temporary());
    return (true);
  }
  //获取记录行的事务id
  trx_id_t trx_id = row_get_rec_trx_id(rec, index, offsets);
  //通过本事务的readview视图的changes_visible函数检测该行的可见性
  return (view->changes_visible(trx_id, index->table->name));
}

3.3 隔离级别影响

 现在我们来思考另一个问题?那事务隔离级别又是如何影响相关可见性的呢?对于不同的事务隔离级别:

tx_isolation='READ-COMMITTED'

语句级别的一致性:只要当前语句执行前已经提交的数据都是可见的。

tx_isolation='REPEATABLE-READ'

事务级别的一致性:只要是当前事务执行前已经提交的数据都是可见的。

 从上面隔离级别的表述,我们也可以发现端倪:隔离级别不同,线程事务结构中的readview申请和释放时机不同,即可实现不同的事务隔离级别。

 具体如下:在READ-COMMITTED隔离级别下,每次语句开始重新获取readview即可;在REPEATABLE-READ隔离级别下,事务初始化构建readview,事务结束才释放readview

READ-COMMITTED隔离级别

 ha_innobase::external_lock中,该函数是新语句开始的都会调用该函数,在该函数中,有如下代码:

//如果隔离级别为READ-COMMITTED,则关闭先前的一致性视图
} else if (trx->isolation_level <= TRX_ISO_READ_COMMITTED &&
               MVCC::is_view_active(trx->read_view)) {
      mutex_enter(&trx_sys->mutex);

      trx_sys->mvcc->view_close(trx->read_view, true);

      mutex_exit(&trx_sys->mutex);
}

 重新获取是在row_search_for_mysql下的row_search_mvcc函数中,该函数执行语句搜索时调用。这样就可以根据当前的全局事务链表创建read_view的事务区间,实现read committed隔离级别。

//判断是否是语句开始的事务,true表示语句事务
if (!prebuilt->sql_stat_start) {
    /* No need to set an intention lock or assign a read view */

    if (!MVCC::is_view_active(trx->read_view) && !srv_read_only_mode &&
        prebuilt->select_lock_type == LOCK_NONE) {
      ib::error(ER_IB_MSG_1031) << "MySQL is trying to perform a"
                                   " consistent read but the read view is not"
                                   " assigned!";
      trx_print(stderr, trx, 600);
      fputc('\n', stderr);
      ut_error;
    }
  } else if (prebuilt->select_lock_type == LOCK_NONE) {
    /* This is a consistent read */
    /* Assign a read view for the query */
    //开启一个一致性视图
    if (!srv_read_only_mode) {
      trx_assign_read_view(trx);
    }

    prebuilt->sql_stat_start = FALSE;
  } else {

REPEATABLE-READ隔离级别

 具体创建是在事务创建的是时候,一直维持到事务结束,具体函数为innobase_start_trx_and_assign_read_view,具体相关代码为

  //如果隔离级别是RR,则开启一个一致性视图
  if (trx->isolation_level == TRX_ISO_REPEATABLE_READ) {
    trx_assign_read_view(trx);
  } else {
    push_warning_printf(thd, Sql_condition::SL_WARNING, HA_ERR_UNSUPPORTED,
                        "InnoDB: WITH CONSISTENT SNAPSHOT"
                        " was ignored because this phrase"
                        " can only be used with"
                        " REPEATABLE READ isolation level.");
  }

 结束是在trx_commit_in_memory事务提交中,具体代码如下:

    if (trx->read_view != NULL) {
      trx_sys->mvcc->view_close(trx->read_view, false);
    }

总结

  • 针对RR隔离级别,在第一次创建readview后,这个readview就会一直持续到事务结束,也就是说在事务执行过程中,数据的可见性不会变,所以在事务内部不会出现不一致的情况。
  • 针对RC隔离级别,事务中的每个查询语句都单独构建一个readview,所以如果两个查询之间有事务提交了,两个查询读出来的结果就不一样。从这里可以看出,在InnoDB中,RR隔离级别的效率是比RC隔离级别的高。
  • 针对RU隔离级别,由于不会去检查可见性,所以在一条SQL中也会读到不一致的数据。
  • 针对串行化隔离级别,InnoDB是通过锁机制来实现的,而不是通过多版本控制的机制,所以性能很差。
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,386评论 6 479
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,939评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,851评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,953评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,971评论 5 369
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,784评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,126评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,765评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,148评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,744评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,858评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,479评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,080评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,053评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,278评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,245评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,590评论 2 343

推荐阅读更多精彩内容