IT技术博客大学习 共学习 共进步

InnoDB的多版本一致性读的实现

NinGoo.net 2011-03-30 13:59:16 浏览 3,224 次

InnoDB是支持MVCC多版本一致性读的,因此和其他实现了MVCC的系统如Oracle,PostgreSQL一样,读不会阻塞写,写也不会阻塞读。虽然同样是MVCC,各家的实现是不太一样的。Oracle通过在block头部的事务列表,和记录中的锁标志位,加上回滚段,个人认为实现上是最优雅的方式。 而PostgreSQL则更是将多个版本的数据都放在表中,而没有单独的回滚段,导致的一个结果是回滚非常快,却付出了查询性能降低的代价。

InnoDB的实现更像Oracle,同样是将数据的旧版本存放在单独的回滚段中,但是也有不同。之前还以为整体实现都会跟Oracle不会有太大的出入,也一直没有太在意去看具体实现。今晚晚上下班准备回家时,刚好路过几个同事在交流分享这个问题,遇到一个疑问:

我们知道,InnoDB表会有三个隐藏字段,6字节的DB_ROW_ID,6字节的DB_TX_ID,7字节的DB_ROLL_PTR(指向对应回滚段的地址),这个可以通过innodb monitor看到,当然如果你熟悉innodb文件结构,也可以直接od ibd文件来验证。一致性读主要跟后两者有关系。InnoDB内部维护了一个递增的tx id counter,其当前值可以通过show engine innodb status获得

echo "show engine innodb status\G" | mysql -uroot | grep "Trx id counter"

假设有一个表,当前已经有两条记录。这时候我们开始一个实验,开启两个session,A和B,都设置autocommit=0

MySQL>set global autocommit=0;

T1时间:
A开始一个事务,执行一条select,可以看到已有的两条记录,show engine innodb status可以知道A的tx id,假设为7430
T2时间:
B开始一个事务,执行一条select,可以看到已有的两条记录,可以知道B的tx id,为7431
T3时间:
A中insert一条记录,此时A再select能看到,所以返回三条记录,而B无法看到,还是返回两条记录。
T4时间:
A中执行commit提交事务,分别在A和B中select,得到的结果和T3时间相同。

Ok,假设一致性读是根据事务先后,也就是tx id来比较的话,如果B事务的一致性读是通过B的tx id即7431来和A事务中insert的这条记录的tx id即7430来比较的话,由于A.tx_id < B.tx_id,那么B应该能都到A的记录(tx id是递增的,所以越小说明事务越早开始),但如果能读到,则显然不符合多版本一致性。

因此结果是正确的,那么就是InnoDB的一致性读的实现方式不是像我们按照经验来测试的那样了。通过google和察看代码,原来InnoDB还真是有一个感觉上很山寨的设计,由于tx id是事务一开始就分配的,事务中的变化也没有记录一个类似于Oracle的SCN的逻辑时钟,于是由了如下的实现:

InnoDB每个事务在开始的时候,会将当前系统中的活跃事务列表(trx_sys->trx_list)创建一个副本(read view),然后一致性读去比较记录的tx id的时候,并不是根据当前事务的tx id,而是根据read view最早一个事务的tx id(read view->up_limit_id)来做比较的,这样就能确保在事务B之前没有提交的所有事务的变更,B事务都是看不到的。当然,这里还有个小问题要处理一下,就是当前事务自身的变更还是需要看到的。

有兴趣的可以去仔细看看代码的实现,在storage/innobase/read/read0read.c中实现了创建read view的函数read_view_open_now,在storage/innobase/include/read0read.ic中实现了判断一致性读是否可见的read_view_sees_trx_id。以下代码摘自5.5.8:

UNIV_INTERN
read_view_t*
read_view_open_now(
/*===============*/
	trx_id_t	cr_trx_id,	/*!< in: trx_id of creating
					transaction, or 0 used in purge */
	mem_heap_t*	heap)		/*!< in: memory heap from which
					allocated */
{
	read_view_t*	view;
	trx_t*		trx;
	ulint		n;
	ut_ad(mutex_own(&kernel_mutex));
	view = read_view_create_low(UT_LIST_GET_LEN(trx_sys->trx_list), heap);
	view->creator_trx_id = cr_trx_id;
	view->type = VIEW_NORMAL;
	view->undo_no = 0;
	/* No future transactions should be visible in the view */
	view->low_limit_no = trx_sys->max_trx_id;
	view->low_limit_id = view->low_limit_no;
	n = 0;
	trx = UT_LIST_GET_FIRST(trx_sys->trx_list);
	/* No active transaction should be visible, except cr_trx */
	while (trx) {
		if (trx->id != cr_trx_id
		    && (trx->conc_state == TRX_ACTIVE
			|| trx->conc_state == TRX_PREPARED)) {
			read_view_set_nth_trx_id(view, n, trx->id);
			n++;
			/* NOTE that a transaction whose trx number is <
			trx_sys->max_trx_id can still be active, if it is
			in the middle of its commit! Note that when a
			transaction starts, we initialize trx->no to
			IB_ULONGLONG_MAX. */
			if (view->low_limit_no > trx->no) {
				view->low_limit_no = trx->no;
			}
		}
		trx = UT_LIST_GET_NEXT(trx_list, trx);
	}
	view->n_trx_ids = n;
	if (n > 0) {
		/* The last active transaction has the smallest id: */
		view->up_limit_id = read_view_get_nth_trx_id(view, n - 1);
	} else {
		view->up_limit_id = view->low_limit_id;
	}
	UT_LIST_ADD_FIRST(view_list, trx_sys->view_list, view);
	return(view);
}
UNIV_INLINE
ibool
read_view_sees_trx_id(
/*==================*/
	const read_view_t*	view,	/*!< in: read view */
	trx_id_t		trx_id)	/*!< in: trx id */
{
	ulint	n_ids;
	ulint	i;
	if (trx_id < view->up_limit_id) {
		return(TRUE);
	}
	if (trx_id >= view->low_limit_id) {
		return(FALSE);
	}
	/* We go through the trx ids in the array smallest first: this order
	may save CPU time, because if there was a very long running
	transaction in the trx id array, its trx id is looked at first, and
	the first two comparisons may well decide the visibility of trx_id. */
	n_ids = view->n_trx_ids;
	for (i = 0; i < n_ids; i++) {
		trx_id_t	view_trx_id
			= read_view_get_nth_trx_id(view, n_ids - i - 1);
		if (trx_id <= view_trx_id) {
			return(trx_id != view_trx_id);
		}
	}
	return(TRUE);
}

参考:
http://dev.mysql.com/doc/refman/5.1/en/innodb-multi-versioning.html
http://wangyuanzju.blog.163.com/blog/static/130292009107101544125/
http://bbs.chinaunix.net/thread-1773206-1-1.html

建议继续学习

  1. 一致性哈希算法及其在分布式系统中的应用 (阅读 9,043)
  2. 分布式哈希和一致性哈希 (阅读 8,665)
  3. Innodb IO优化-配置优化 (阅读 7,604)
  4. Innodb分表太多或者表分区太多,会导致内存耗尽而宕机 (阅读 7,565)
  5. Innodb 表和索引结构 (阅读 6,041)
  6. InnoDB线程并发检查机制 (阅读 5,602)
  7. Innodb如何使用内存 (阅读 5,102)
  8. Innodb文件表空间结构 (阅读 5,064)
  9. 快速预热Innodb Buffer Pool的方法 (阅读 4,983)
  10. InnoDB的缓存替换策略及其效果 (阅读 4,782)