MySQL · 引擎特性 · InnoDB mini transation

一 序

    之前的在整理redo log  redo log用来保证事务持久性,通过undo log可以看到数据较早版本,实现MVCC,或回滚事务等功能。

二 mini transaction 简介

     innodb存储引擎中的一个很重要的用来保证持久性的机制就是mini事务,在源码中用mtr(Mini-transaction)来表示,本书把它称做“物理事务”,这样叫是相对逻辑事务而言的,对于逻辑事务,做熟悉数据库的人都很清楚,它是数据库区别于文件系统的最重要特性之一,它具有四个特性ACID,用来保证数据库的完整性——要么都做修改,要么什么都没有做。物理事务从名字来看,是物理的,因为在innodb存储引擎中,只要是涉及到文件修改,文件读取等物理操作的,都离不开这个物理事务,可以说物理事务是内存与文件之间的一个桥梁。

    

mini transation 主要用于innodb redo log 和 undo log写入,保证两种日志的ACID特性

mini-transaction遵循以下三个协议:
The FIX Rules
Write-Ahead Log
Force-log-at-commit

The FIX Rules
修改一个页需要获得该页的x-latch
访问一个页是需要获得该页的s-latch或者x-latch
持有该页的latch直到修改或者访问该页的操作完成

Write-Ahead Log
持久化一个数据页之前,必须先将内存中相应的日志页持久化
每个页有一个LSN,每次页修改需要维护这个LSN,当一个页需要写入到持久化设备时,要求内存中小于该页LSN的日志先写入到持久化设备中

Force-log-at-commit
一个事务可以同时修改了多个页,Write-AheadLog单个数据页的一致性,无法保证事务的持久性
Force -log-at-commit要求当一个事务提交时,其产生所有的mini-transaction日志必须刷到持久设备中
这样即使在页数据刷盘的时候宕机,也可以通过日志进行redo恢复

三 源码简介

  本文使用 MySQL 5.7.18 版本进行分析

mini transation 相关代码路径位于 storage/innobase/mtr/ 主要有 mtr0mtr.cc 和 mtr0log.cc 两个文件
另有部分代码在 storage/innobase/include/ 文件名以 mtr0 开头.

mini transaction 的信息保存在结构体 mtr_t 中,源码在/innobase/include/mtr0mtr.h

/** Mini-transaction handle and buffer */
struct mtr_t {
 
	/** State variables of the mtr */
	struct Impl {
 
		/** memo stack for locks etc. */
		mtr_buf_t	m_memo;
 
		/** mini-transaction log */
		mtr_buf_t	m_log;
 
		/** true if mtr has made at least one buffer pool page dirty */
		bool		m_made_dirty;
 
		/** true if inside ibuf changes */
		bool		m_inside_ibuf;
 
		/** true if the mini-transaction modified buffer pool pages */
		bool		m_modifications;
 
		/** Count of how many page initial log records have been
		written to the mtr log */
		ib_uint32_t	m_n_log_recs;
 
		/** specifies which operations should be logged; default
		value MTR_LOG_ALL */
		mtr_log_t	m_log_mode;
#ifdef UNIV_DEBUG
		/** Persistent user tablespace associated with the
		mini-transaction, or 0 (TRX_SYS_SPACE) if none yet */
		ulint		m_user_space_id;
#endif /* UNIV_DEBUG */
		/** User tablespace that is being modified by the
		mini-transaction */
		fil_space_t*	m_user_space;
		/** Undo tablespace that is being modified by the
		mini-transaction */
		fil_space_t*	m_undo_space;
		/** System tablespace if it is being modified by the
		mini-transaction */
		fil_space_t*	m_sys_space;
 
		/** State of the transaction */
		mtr_state_t	m_state;
 
		/** Flush Observer */
		FlushObserver*	m_flush_observer;
 
#ifdef UNIV_DEBUG
		/** For checking corruption. */
		ulint		m_magic_n;
#endif /* UNIV_DEBUG */
 
		/** Owning mini-transaction */
		mtr_t*		m_mtr;
	};
变量名 描述
mtr_buf_t m_memo 用于存储该mtr持有的锁类型
mtr_buf_t m_log 存储redo log记录
bool m_made_dirty 是否产生了至少一个脏页
bool m_inside_ibuf 是否在操作change buffer
bool m_modifications 是否修改了buffer pool page
ib_uint32_t m_n_log_recs 该mtr log记录个数
mtr_log_t m_log_mode Mtr的工作模式,包括四种: MTR_LOG_ALL:默认模式,记录所有会修改磁盘数据的操作;MTR_LOG_NONE:不记录redo,脏页也不放到flush list上;MTR_LOG_NO_REDO:不记录redo,但脏页放到flush list上;MTR_LOG_SHORT_INSERTS:插入记录操作REDO,在将记录从一个page拷贝到另外一个新建的page时用到,此时忽略写索引信息到redo log中。(参阅函数page_cur_insert_rec_write_log)
fil_space_t* m_user_space 当前mtr修改的用户表空间
fil_space_t* m_undo_space 当前mtr修改的undo表空间
fil_space_t* m_sys_space 当前mtr修改的系统表空间
mtr_state_t m_state 包含四种状态: MTR_STATE_INIT、MTR_STATE_COMMITTING、 MTR_STATE_COMMITTED

在修改或读一个数据文件中的数据时,一般是通过mtr来控制对对应page或者索引树的加锁,在5.7中,有以下几种锁类型(mtr_memo_type_t):

变量名 描述
MTR_MEMO_PAGE_S_FIX 用于PAGE上的S锁
MTR_MEMO_PAGE_X_FIX 用于PAGE上的X锁
MTR_MEMO_PAGE_SX_FIX 用于PAGE上的SX锁,以上锁通过mtr_memo_push 保存到mtr中
MTR_MEMO_BUF_FIX PAGE上未加读写锁,仅做buf fix
MTR_MEMO_S_LOCK S锁,通常用于索引锁
MTR_MEMO_X_LOCK X锁,通常用于索引锁
MTR_MEMO_SX_LOCK SX锁,通常用于索引锁,以上3个锁,通过mtr_s/x/sx_lock加锁,通过mtr_memo_release释放锁

四 一条insert语句涉及的 mini transaction

     InnoDB的redo log都是通过mtr产生的,先写到mtr的cache中,然后再提交到公共buffer中,本小节以INSERT一条记录对page产生的修改为例,阐述一个mtr的典型生命周期。关于insert 的执行过程,参见之前整理的https://blog.csdn.net/bohu83/article/details/82903976
    入口函数在row_ins_clust_index_entry_low,innobase/row/row0ins.cc

开启MTR

row_ins_clust_index_entry_low(
/*==========================*/
	ulint		flags,	/*!< in: undo logging and locking flags */
	ulint		mode,	/*!< in: BTR_MODIFY_LEAF or BTR_MODIFY_TREE,
				depending on whether we wish optimistic or
				pessimistic descent down the index tree */
	dict_index_t*	index,	/*!< in: clustered index */
	ulint		n_uniq,	/*!< in: 0 or index->n_uniq */
	dtuple_t*	entry,	/*!< in/out: index entry to insert */
	ulint		n_ext,	/*!< in: number of externally stored columns */
	que_thr_t*	thr,	/*!< in: query thread */
	bool		dup_chk_only)
				/*!< in: if true, just do duplicate check
				and return. don't execute actual insert. */
{
	btr_pcur_t	pcur;
	btr_cur_t*	cursor;
	dberr_t		err		= DB_SUCCESS;
	big_rec_t*	big_rec		= NULL;
	mtr_t		mtr;
	mem_heap_t*	offsets_heap	= NULL;
	ulint           offsets_[REC_OFFS_NORMAL_SIZE];
	ulint*          offsets         = offsets_;
	rec_offs_init(offsets_);

	DBUG_ENTER("row_ins_clust_index_entry_low");

	ut_ad(dict_index_is_clust(index));
	ut_ad(!dict_index_is_unique(index)
	      || n_uniq == dict_index_get_n_unique(index));
	ut_ad(!n_uniq || n_uniq == dict_index_get_n_unique(index));
	ut_ad(!thr_get_trx(thr)->in_rollback);

	mtr_start(&mtr);
	mtr.set_named_space(index->space);

	if (dict_table_is_temporary(index->table)) {
		/* Disable REDO logging as the lifetime of temp-tables is
		limited to server or connection lifetime and so REDO
		information is not needed on restart for recovery.
		Disable locking as temp-tables are local to a connection. */

		ut_ad(flags & BTR_NO_LOCKING_FLAG);
		ut_ad(!dict_table_is_intrinsic(index->table)
		      || (flags & BTR_NO_UNDO_LOG_FLAG));

		mtr.set_log_mode(MTR_LOG_NO_REDO);
	}
...

mtr_start(&mtr);
mtr.set_named_space(index->space);
就是开启mtr。
mtr_start主要包括:

  • 初始化mtr的各个状态变量
  • 默认模式为MTR_LOG_ALL,表示记录所有的数据变更
  • mtr状态设置为ACTIVE状态(MTR_STATE_ACTIVE)
  • 为锁管理对象和日志管理对象初始化内存(mtr_buf_t),初始化对象链表

   mtr.set_named_space 是5.7新增的逻辑,将当前修改的表空间对象fil_space_t保存下来:如果是系统表空间,则赋值给m_impl.m_sys_space, 否则赋值给m_impl.m_user_space。
在5.7里针对临时表做了优化,直接关闭redo记录: mtr.set_log_mode(MTR_LOG_NO_REDO)

定位插入位置

if (mode == BTR_MODIFY_LEAF && dict_index_is_online_ddl(index)) {
		mode = BTR_MODIFY_LEAF | BTR_ALREADY_S_LATCHED;
		mtr_s_lock(dict_index_get_lock(index), &mtr);
	}

	/* Note that we use PAGE_CUR_LE as the search mode, because then
	the function will return in both low_match and up_match of the
	cursor sensible values */
	btr_pcur_open(index, entry, PAGE_CUR_LE, mode, &pcur, &mtr);
	cursor = btr_pcur_get_btr_cur(&pcur);
	cursor->thr = thr;

	ut_ad(!dict_table_is_intrinsic(index->table)
	      || cursor->page_cur.block->made_dirty_with_no_latch);

#ifdef UNIV_DEBUG
	{
		page_t*	page = btr_cur_get_page(cursor);
		rec_t*	first_rec = page_rec_get_next(
			page_get_infimum_rec(page));

		ut_ad(page_rec_is_supremum(first_rec)
		      || rec_n_fields_is_sane(index, first_rec, entry));
	}
#endif /* UNIV_DEBUG */
...

    btr_pcur_open方法,获取到这个新生成的index到底放到btr的哪个位置。这个位置,就由Cursor来标记标记。pcur是persistent cursor。因为btr是会分裂和变动的,当btr被分裂时,cursor的位置也会对应的进行变化。因此通过一层pcur的封装,将cursor的变化对外屏蔽,针对一个index,我们只需要通过一个固定的pcur去获取当前的cursor就可以了.(btr_pcur_open_low->btr_cur_search_to_nth_level)

获取到了真实的cursor后,就可以拿到对应的leaf节点,就是具体的page。就是btr_cur_get_page。

   我们看看btr_cur_search_to_nth_level 对应的源码在 storage/innobase/btr/btr0cur.cc

函数的主要作用是将cursor移动到索引上待插入的位置,不展开看。

     不管插入还是更新操作,都是先以乐观方式进行,因此先加索引S锁 mtr_s_lock(dict_index_get_lock(index),&mtr),对应mtr_t::s_lock函数 如果以悲观方式插入记录,意味着可能产生索引分裂,在5.7之前会加索引X锁,而5.7版本则会加SX锁(但某些情况下也会退化成X锁) 加X锁: mtr_x_lock(dict_index_get_lock(index), mtr),对应mtr_t::x_lock函数 加SX锁:mtr_sx_lock(dict_index_get_lock(index),mtr),对应mtr_t::sx_lock函数,源码在 storage/innobase/include/mtr0mtr.ic

/**
Locks a lock in x-mode. */

void
mtr_t::x_lock(rw_lock_t* lock, const char* file, ulint line)
{
	rw_lock_x_lock_inline(lock, 0, file, line);

	memo_push(lock, MTR_MEMO_X_LOCK);
}

/**
Locks a lock in sx-mode. */

void
mtr_t::sx_lock(rw_lock_t* lock, const char* file, ulint line)
{
	rw_lock_sx_lock_inline(lock, 0, file, line);

	memo_push(lock, MTR_MEMO_SX_LOCK);
}

    实际上就是加上对应的锁对象,然后将该锁的指针和类型构建的mtr_memo_slot_t对象插入到mtr.m_impl.m_memo中。
当找到预插入page对应的block,还需要加block锁,并把对应的锁类型加入到mtr:mtr_memo_push(mtr, block, fix_type)
   如果对page加的是MTR_MEMO_PAGE_X_FIX或者MTR_MEMO_PAGE_SX_FIX锁,并且当前block是clean的,则将m_impl.m_made_dirty设置成true,表示即将修改一个干净的page。
     如果加锁类型为MTR_MEMO_BUF_FIX,实际上是不加锁对象的,但需要判断临时表的场景,临时表page的修改不加latch,但需要将m_impl.m_made_dirty设置为true(根据block的成员m_impl.m_made_dirty来判断),这也是5.7对InnoDB临时表场景的一种优化。
    同样的,根据锁类型和锁对象构建mtr_memo_slot_t加入到m_impl.m_memo中。

插入数据

    先进性乐观插入,失败在执行悲观插入。

err = btr_cur_optimistic_insert(
				flags, cursor,
				&offsets, &offsets_heap,
				entry, &insert_rec, &big_rec,
				n_ext, thr, &mtr);

			if (err == DB_FAIL) {
				err = btr_cur_pessimistic_insert(
					flags, cursor,
					&offsets, &offsets_heap,
					entry, &insert_rec, &big_rec,
					n_ext, thr, &mtr);
			}

在插入数据过程中,包含大量的redo写cache逻辑,例如更新二级索引页的max trx id、写undo log产生的redo(嵌套另外一个mtr)、修改数据页产生的日志。这里我们只讨论修改数据页产生的日志,进入函数page_cur_insert_rec_write_log:源码在innobase/page/page0cur.cc。这里不贴了。

Step 1: 调用函数mlog_open_and_write_index记录索引相关信息
Step 2: 写入记录在page上的偏移量,占两个字节
mach_write_to_2(log_ptr, page_offset(cursor_rec));
Step 3: 写入记录其它相关信息 (rec size, extra size, info bit,关于InnoDB的数据文件物理描述,参见淘宝数据库月报)
Step 4: 将插入的记录拷贝到redo文件,同时关闭mlog
memcpy(log_ptr, ins_ptr, rec_size);
mlog_close(mtr, log_ptr + rec_size);

   通过上述流程,我们写入了一个类型为MLOG_COMP_REC_INSERT的日志记录。由于特定类型的记录都基于约定的格式,在崩溃恢复时也可以基于这样的约定解析出日志。

  更多的redo log记录类型参见enum mlog_id_t  源码在innobase/include/mtr0types.h
在这个过程中产生的redo log都记录在mtr.m_impl.m_log中,只有显式提交mtr时,才会写到公共buffer中。

提交MTR log

  当提交一个mini transaction时,需要将对数据的更改记录提交到公共buffer中,并将对应的脏页加到flush list上。
入口函数为mtr_t::commit(),当修改产生脏页或者日志记录时,调用mtr_t::Command::execute 源码在innobase/mtr/mtr0mtr.cc 

/** Write the redo log record, add dirty pages to the flush list and release
the resources. */
void
mtr_t::Command::execute()
{
	ut_ad(m_impl->m_log_mode != MTR_LOG_NONE);
 
	if (const ulint len = prepare_write()) {
		finish_write(len);
	}
 
	if (m_impl->m_made_dirty) {
		log_flush_order_mutex_enter();
	}
 
	/* It is now safe to release the log mutex because the
	flush_order mutex will ensure that we are the first one
	to insert into the flush list. */
	log_mutex_exit();
 
	m_impl->m_mtr->m_commit_lsn = m_end_lsn;
 
	release_blocks();
 
	if (m_impl->m_made_dirty) {
		log_flush_order_mutex_exit();
	}
 
	release_latches();
 
	release_resources();
}

Step 1: mtr_t::Command::prepare_write()
     主要是持有log_sys->mutex,做写入前检查
Step 2: mtr_t::Command::finish_write
将日志从mtr中拷贝到公共log buffer。
Step 3:如果本次修改产生了脏页,获取log_sys->log_flush_order_mutex,随后释放log_sys->mutex。
Step 4. 将当前Mtr修改的脏页加入到flush list上,脏页上记录的lsn为当前mtr写入的结束点lsn。基于上述加锁逻辑,能够保证flush list上的脏页总是以LSN排序。
Step 5. 释放log_sys->log_flush_order_mutex锁
Step 6. 释放当前mtr持有的锁(主要是page latch)及分配的内存,mtr完成提交。

至此 insert 语句涉及的 mini transaction 全部结束.

五 总结

上面可以看到加锁、写日志到 mlog 等操作在 mini transaction 过程中进行。解锁、把日志刷盘等操作全部在 mtr_commit 中进行,和事务类似。mini transaction 没有回滚操作, 因为只有在 mtr_commit 才将修改落盘,如果宕机,内存丢失,无需回滚;如果落盘过程中宕机,崩溃恢复时可以看出落盘过程不完整,丢弃这部分修改。
mtr_commit 主要包含以下步骤

  1. mlog 中日志刷盘
  2. 释放 mtr 持有的锁,锁信息保存在 memo 中,以栈形式保存,后加的锁先释放
  3. 清理 mtr 申请的内存空间,memo 和 log
  4. mtr—>state 设置为 MTR_COMMITTED

上面的步骤 1. 中,日志刷盘策略和 innodb_flush_log_at_trx_commit 有关
当设置该值为1时,每次事务提交都要做一次fsync,这是最安全的配置,即使宕机也不会丢失事务
当设置为2时,则在事务提交时只做write操作,只保证写到系统的page cache,因此实例crash不会丢失事务,但宕机则可能丢失事务
当设置为0时,事务提交不会触发redo写操作,而是留给后台线程每秒一次的刷盘操作,因此实例crash将最多丢失1秒钟内的事务

这篇也算是上篇 insert 执行过程的一个补充。

参考:

http://mysql.taobao.org/monthly/2017/10/03/

http://mysql.taobao.org/monthly/2015/05/01

猜你喜欢

转载自blog.csdn.net/bohu83/article/details/82959828
今日推荐