innodb事务系统 - 事务启动和提交

author:sufei

源码版本:8.0.16


一、核心结构体

​ 在理解分析innodb事务系统过程前,我们先要对相关的核心结构体进行分析说明,以便更好的对后续内容理解。

1.1 trx_t

​ 该结构体是每个线程THD结构都持有一个,在用户线程连接连接进入mysql服务器时,会初始化一个THD结构与每个连接会话对应。此时该THD结构还没有初始化一个innodb事务结构trx_t与之关联,直到第一个事务(进入innodb层代码,begin语句并没有进入innodb层),innodb层会为该THD结构(也就是该链接)初始化一个事务结构体trx_t与之关联

 相应的核心函数为check_trx_exists函数,其会为连接会话的THD结构从innodb事务池中分配一个trx_t结构体。后续这个连接的所有事务一直复用trx_t里面的数据结构,直到这个连接断开。相关精简代码如下:

trx_t *check_trx_exists(THD *thd)
{
  trx_t *&trx = thd_to_trx(thd);  //获取thd结构关联的innodb trx_t结构体
  ut_ad(EQ_CURRENT_THD(thd));

  if (trx == NULL) {  //如果还没分配,则从innodb事务池,拿出一个分配
    /*
    在innobase_trx_allocate函数调用栈中,主要有如下操作:
    1、调用trx_create_low函数,从事务池中获取一个事务结构体trx_t
    2、将该trx_t结构体添加到innodb事务系统的mysql_trx_list全局事务链表中
    */
    trx = innobase_trx_allocate(thd);

    /* User trx can be forced to rollback,
    so we unset the disable flag. */
    ut_ad(trx->in_innodb & TRX_FORCE_ROLLBACK_DISABLE);
    trx->in_innodb &= TRX_FORCE_ROLLBACK_MASK;
  } else { //已经存在则初始化trx_t结构体某些字段
    ut_a(trx->magic_n == TRX_MAGIC_N);

    innobase_trx_init(thd, trx);
  }
  return (trx);
}

 我们知道了会话线程在innodb层的事务逻辑基本使用trx_t结构体来维护,下面对该结构体的核心成员进行说明:

成员名 说明
id 事务id,在事务刚创建的时候分配(只读事务永远为0,读写事务通过一个全局trx_sys->max_trx_id产生器产生,非0),目的就是为了区分不同的事务(只读事务通过指针地址来区分)
事务提交前,通过同一个全局id生产器产生的,主要目的是为了确定事务提交的顺序,保证加入到history list中的update undo有序,方便purge线程清理。
state 事务当前状态,可选值为:
TRX_STATE_NOT_STARTED :
TRX_STATE_FORCED_ROLLBACK
TRX_STATE_ACTIVE
TRX_STATE_PREPARED
TRX_STATE_COMMITTED_IN_MEMORY
read_view 事务当前开启的一致性视图,用来表示当前事务的可见范围(后续分析)
lock 事务当前持有的innodb锁信息(后续分析)
isolation_level 事务当前的隔离级别
commit_lsn 记录事务提交时的lsn
read_only 标识事务是只读事务

1.2 trx_sys_t

 该结构体是innodb事务系统核心内存结构,用来维护系统的事务信息,全局只有一个,在启动的时候初始化。主要成员说明:

成员名 说明
mvcc innodb多版本并发控制管理类
max_trx_id 表示系统当前还未分配的最小事务id,如果有一个新的事务,直接把这个值作为新事务的id,然后这个字段递增即可
min_active_id innodb事务系统活跃事务中的最小事务id
serialisation_list 存放当前系统的所有活跃的读写事务,按trx_t->no排序
rw_trx_list 存放当前系统的所有读写事务,包括活跃的和已经提交的事务。按照事务id排序,此外,奔溃恢复后产生的事务和系统的事务也放在上面。
mysql_trx_list 存放所有用户创建的事务,系统的事务和奔溃恢复后的事务不会在这个链表上,但是这个链表上可能会有还没开始的用户事务。

二、事务开启

 在innodb中,有两种事务:读写事务,就是会对数据进行增删改的事务,另外一种是只读事务,仅仅对数据进行读取。Innodb提供了多钟开启事务的方式,用户可以选择开启只读事务还是读写事物。没有指定开启读写事物,默认开启只读事务,当涉及到DML数据操作语句,才自动将只读事务转换为读写事物。以下是几种开启事务的方式:

2.1 BEGIN or START TRANSACTION

​ 这是最常见的开启事务方式,当以BEGIN开启一个事务时,语句默认是以只读事务的方式启动。其等效的语句有BEGIN WORK”及“START TRANSACTION”。其实不管是那种语句显示开启事务,在MySQL源码中都是只调用trans_begin函数,精简代码如下:

/*
该函数为服务层开启一个事务,开启事务时会隐形提交现在未提交的事务,释放相应的锁结构。
参数说明:
    THD : 线程结构体
    flags : 事务标志,表明现在开启事务是否为只读事务亦或者释放需要开启一致性视图等,具体可取参数如下:
            MYSQL_START_TRANS_OPT_WITH_CONS_SNAPSHOT = 1
                表明开启事务的同时,获取一致性视图,语句添加WITH CONSISTENT SNAPSHOT选项
            MYSQL_START_TRANS_OPT_READ_ONLY = 2
                开启只读事务,该事务不能进行任何修改数据操作,语句添加READ ONLY选项
            MYSQL_START_TRANS_OPT_READ_WRITE = 4
                开启读写事务,语句添加READ WRITE选项
            MYSQL_START_TRANS_OPT_HIGH_PRIORITY = 8
                高级别事务,语句添加HIGH PRIORITY选项
*/
bool trans_begin(THD *thd, uint flags) {
  bool res = false;
  Transaction_state_tracker *tst = NULL;  //创建事务跟踪结构体指针

  DBUG_ENTER("trans_begin");

  if (trans_check_state(thd)) DBUG_RETURN(true);
  //初始化事务跟踪结构体指针
  if (thd->variables.session_track_transaction_info > TX_TRACK_NONE)
    tst = (Transaction_state_tracker *)thd->session_tracker.get_tracker(
        TRANSACTION_INFO_TRACKER);

  thd->locked_tables_list.unlock_locked_tables(thd);  //释放该线程所有表结构锁

  if (thd->in_multi_stmt_transaction_mode() ||
      (thd->variables.option_bits & OPTION_TABLE_LOCK)) {
    thd->variables.option_bits &= ~OPTION_TABLE_LOCK;
    thd->server_status &=
        ~(SERVER_STATUS_IN_TRANS | SERVER_STATUS_IN_TRANS_READONLY);
    DBUG_PRINT("info", ("clearing SERVER_STATUS_IN_TRANS"));
    res = ha_commit_trans(thd, true);   //提交未提交的活跃事务
  }

  /*
    Release transactional metadata locks only after the
    transaction has been committed.
  */
  thd->mdl_context.release_transactional_locks();  //事务提交之后释放MDL锁

  // 只读事务与读写事物是互斥的,所以传入的flags参数不能两个标准位是都设置
  DBUG_ASSERT(!((flags & MYSQL_START_TRANS_OPT_READ_ONLY) &&
                (flags & MYSQL_START_TRANS_OPT_READ_WRITE)));
  if (flags & MYSQL_START_TRANS_OPT_READ_ONLY) {//如果是只读事务,将事务只读标记设置为true
    thd->tx_read_only = true;
    if (tst) tst->set_read_flags(thd, TX_READ_ONLY);
  } else if (flags & MYSQL_START_TRANS_OPT_READ_WRITE) {
    /*
    显示开启一个读写事物,需要先检测服务器释放工作在readonly模式,如果是则直接拒绝,除非
    是super用户。
    */
    if (check_readonly(thd, true)) DBUG_RETURN(true);
    thd->tx_read_only = false; //将只读标识设为false
    if (tst) tst->set_read_flags(thd, TX_READ_WRITE);
  }

  DBUG_EXECUTE_IF("dbug_set_high_prio_trx", {
    DBUG_ASSERT(thd->tx_priority == 0);
    thd->tx_priority = 1;
  });

  //设置相关标识,表示显示开启了事务
  thd->variables.option_bits |= OPTION_BEGIN;
  thd->server_status |= SERVER_STATUS_IN_TRANS;
  if (thd->tx_read_only) thd->server_status |= SERVER_STATUS_IN_TRANS_READONLY;
  DBUG_PRINT("info", ("setting SERVER_STATUS_IN_TRANS"));

  if (tst) tst->add_trx_state(thd, TX_EXPLICIT);

  /* 
  如果flags开启了一致性标识,则需要进入引擎层,获取一个一致性视图
  最终会调用innodb引擎的innobase_start_trx_and_assign_read_view函数
  */
  if (flags & MYSQL_START_TRANS_OPT_WITH_CONS_SNAPSHOT) {
    if (tst) tst->add_trx_state(thd, TX_WITH_SNAPSHOT);
    res = ha_start_consistent_snapshot(thd);
  }

  DBUG_RETURN(res);
}

​ 从代码中可以看出:在执行begin等语句的时候,实际上并不会真的去引擎层开启一个事务(除非加上WITH CONSISTENT SNAPSHOT选项),MySQL仅仅在服务层做如下事情:

1、检测是否有未提交的事务,调用ha_commit_trans提交;

2、释放原来持有的MDL锁;

3、设置当前线程THD结构相关事务的标志位,表示显示开启了事务。

​ 如以下在gdb模式下,分别以BEGIN,START TRANSACTION READ ONLY,START TRANSACTION READ WRITE,START TRANSACTION WITH CONSISTENT SNAPSHOT几种模式显示开启事务的截图

事务启动flags

还有几点需要注意:

  • START TRANSACTION READ WRITE语句开启读写事物,读写事务并不意味着一定在引擎层(此时还没进入引擎层)就被认定为读写事务了,5.7版本InnoDB里总是默认一个事务开启时的状态为只读的。举个例子:如果你事务的第一条SQL是只读查询,那么在InnoDB层,它的事务状态就是只读的,如果第二条SQL是更新操作,就将事务转换成读写模式(innodb层是否为读写事物,主要开激活引擎层事务的传参,下面会说明)。

  • START TRANSACTION WITH CONSISTENT SNAPSHOT语句开启一致性视图时,注意只有你的隔离级别设置成REPEATABLE READ(可重复读)时,才会显式开启一个Read View,否则会抛出一个warning。

​ 既然运行begin等语句开启事务,仅仅是在服务层,引擎层依然没有开启过事务。那么合适innodb引擎层会真正开启一个事务呢?

2.2 innodb开启事务

​ 默认情况下,在InnoDB看来所有的事务在启动时候都是只读状态,只有接受到修改数据的SQL后(InnoDB接收到才行。因为在start transaction read only模式下,DML/DDL都被Serve层挡掉了)才调用trx_set_rw_mode函数把只读事务提升为读写事务。innodb真正激活事务,是真正操作数据前(包含修改和查看操作),通过trx_start_if_not_started_low->trx_start_low调用实现,真正开启innodb事务。精简代码如下:

/*
该函数主要是激活一个innodb层事务
参数:
    trx : innodb层线程相关的事务结构体
    read_write : 标识激活事务是否为读写事务,默认为false,只读事务
*/
void trx_start_if_not_started_low(
    trx_t *trx,      /*!< in: transaction */
    bool read_write) /*!< in: true if read write transaction */
{
  switch (trx->state) {
    case TRX_STATE_NOT_STARTED:
    case TRX_STATE_FORCED_ROLLBACK:
      //如果事务状态还没被开启,则启动一个innodb事务
      trx_start_low(trx, read_write);
      return;
    case TRX_STATE_ACTIVE:
      /*
      如果事务已经开启,但是为只读事务,并且参数read_write为true(DML语句),需要开启一个读写事务,
      调用trx_set_rw_mode函数修改为读写事务。
      */
      if (read_write && trx->id == 0 && !trx->read_only) {
        trx_set_rw_mode(trx);
      }
      return;
    case TRX_STATE_PREPARED:
    case TRX_STATE_COMMITTED_IN_MEMORY:
      break;
  }
}

/*
innodb引擎层真正激活一个事务结构体
*/
static void trx_start_low(
    trx_t *trx,      /*!< in: transaction */
    bool read_write) /*!< in: true if read-write transaction */
{
  ++trx->version;

  /* 
  检测是否为自动提交的select语句,这里的auto_commit与通过理解不一样,是是否为自动提交的select事务
  */
  trx->auto_commit = (trx->api_trx && trx->api_auto_commit) ||
                     thd_trx_is_auto_commit(trx->mysql_thd);
  /*
  检测事务是否为只读状态,其中thd_trx_is_read_only函数会查看thd->tx_read_only变量,
  即服务层事务开启时根据选项设置的是否为只读事务
  */
  trx->read_only = (trx->api_trx && !trx->read_write) ||
                   (!trx->internal && thd_trx_is_read_only(trx->mysql_thd)) ||
                   srv_read_only_mode;

  if (!trx->auto_commit) { //如果非自动提交select语句,则will_lock自加
    ++trx->will_lock;
  } else if (trx->will_lock == 0) { //will_lock=0,自然就是只读事务
    trx->read_only = true;
  }

  trx->no = TRX_ID_MAX;

  ut_a(ib_vector_is_empty(trx->autoinc_locks));
  ut_a(trx->lock.table_locks.empty());

  /* If this transaction came from trx_allocate_for_mysql(),
  trx->in_mysql_trx_list would hold. In that case, the trx->state
  change must be protected by the trx_sys->mutex, so that
  lock_print_info_all_transactions() will have a consistent view. */

  ut_ad(!trx->in_rw_trx_list);

  /* 
  这里需要区别一下:trx->read_only和传参read_write的含义。
  trx->read_only:表示此时事物的状态是否是只读状态;
  read_write:该传参表示调用者传入的读写状态,只读事物为false,DML事物为true。这里的读写和前面
              trx->read_only有区别,如果是只读事物建立临时表也是读写事物(即使在只读状态下)。
  如果是非只读事务,并且一下三个条件满足任意一个:
  1、trx->mysql_thd == 0 表示是否是MYSQL线程建立的innodb事务
  2、调用者传参read_write为true,即为DML操作(可能是针对临时表)
  3、trx->ddl_operation为true,即为ddl事务
  满足上述条件,则进入读写事务逻辑
  */

  if (!trx->read_only &&
      (trx->mysql_thd == 0 || read_write || trx->ddl_operation)) {
    trx_assign_rseg_durable(trx);  //分配回滚undo段

    /* Temporary rseg is assigned only if the transaction
    updates a temporary table */

    trx_sys_mutex_enter();
    trx_assign_id_for_rw(trx);  //为读写事务分配事务id
    trx_sys_rw_trx_add(trx);

    ut_ad(trx->rsegs.m_redo.rseg != 0 || srv_read_only_mode ||
          srv_force_recovery >= SRV_FORCE_NO_TRX_UNDO);
    UT_LIST_ADD_FIRST(trx_sys->rw_trx_list, trx);  //加入到读写事务列表
    ut_d(trx->in_rw_trx_list = true);
#ifdef UNIV_DEBUG
    if (trx->id > trx_sys->rw_max_trx_id) {
      trx_sys->rw_max_trx_id = trx->id;   //如果有必要,更新最大事务id
    }
#endif /* UNIV_DEBUG */
    trx->state = TRX_STATE_ACTIVE;  //将事务状态更新为active
    ut_ad(trx_sys_validate_trx_list());
    trx_sys_mutex_exit();

  } else {  //以下为只读事务的处理逻辑
    trx->id = 0; //对于只读事务不需要分配事务id
    if (!trx_is_autocommit_non_locking(trx)) {
      /* 
      对于只读事务,可能需要使用临时表,此时也需要分配事务id
      */
      if (read_write) {
        trx_sys_mutex_enter();
        ut_ad(!srv_read_only_mode);
        trx_assign_id_for_rw(trx); //分配id
        trx_sys->rw_trx_set.insert(TrxTrack(trx->id, trx));
        trx_sys_mutex_exit();
      }
      trx->state = TRX_STATE_ACTIVE; //将事务状态更新为active
    } else {
      ut_ad(!read_write);
      trx->state = TRX_STATE_ACTIVE; //将事务状态更新为active
    }
  }
  //设置事务开始时间
  if (trx->mysql_thd != NULL) { 
    trx->start_time = thd_start_time_in_secs(trx->mysql_thd);
  } else {
    trx->start_time = ut_time();
  }
  trx->age = 0;
  trx->age_updated = 0;
  ut_a(trx->error_state == DB_SUCCESS);
  MONITOR_INC(MONITOR_TRX_ACTIVE);
}

 从代码分析来看:对于开启innodb读写事务,innodb相对需要做更多的工作:1、分配事务id;2、分配回滚段空间;3、将事务加入到系统读写事务列表。所有在innodb中,对于事务的开启,默认是只读事务,也就是传参read_write为false,直到DML语句,才会调用trx_set_rw_mode将只读事务转变为读写事务。

三、事务提交

 事务的提交分为隐式提交和显示提交。如上文所说的在显示开启一个事务的时候,会调用ha_commit_trans对未提交事务的进行提交,这就是一种隐式提交。显示提交就更加情况,就是主动调用commit指令。

 对于MySQL服务器来说,提交相对复杂,不同情况下对应的行为动作还不一样。这主要是因为 MySQL 是一种服务器层-引擎层的架构,并存在两套日志系统:Binary log及引擎事务日志。这里仅仅各种情况简单说明:

  • 若打开binlog,且使用了事务引擎,则XA控制对象为mysql_bin_log;
  • 若关闭了binlog,且存在不止一种事务引擎时,则XA控制对象为tc_log_mmap;
  • 其他情况,使用tc_log_dummy,这种场景下就没有什么XA可言了,无需任何协调者来进行XA。

 我们这里并不打算分析整个MySQL服务器的提交过程,这里仅仅对innodb引擎层的提交进行说明。在innodb引擎层的提交入口函数为:innobase_commit。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念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

推荐阅读更多精彩内容