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

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

PostgreSQL备份

PostgreSQL也支持逻辑备库和物理备份两种方式。物理备份可以和Oracle一样实现联机热备份,并且同样也需要将数据库设置为归档模式。

逻辑备份

PostgreSQL提供了pg_dump/pg_dumpall两个程序可以用来将数据dump成文本文件,实现数据的逻辑备份。使用不同的参数,可以将数据dump成PostgreSQL专用的数据格式(生成copy语句)或者标准SQL语句(生成insert语句)格式。恢复只需要简单的使用psql将文件执行一遍即可。另外也可以使用pg_restore工具来恢复数据。

利用pg_dump备份test数据库(只有一张test表),包括重建表的DDL语句,授权语句等所有信息,生成copy格式的文件:

$ ./pg_dump test
--
-- PostgreSQL database dump
--
SET statement_timeout = 0;
SET client_encoding = 'UTF8';
SET standard_conforming_strings = off;
SET check_function_bodies = false;
SET client_min_messages = warning;
SET escape_string_warning = off;
SET search_path = public, pg_catalog;
SET default_tablespace = '';
SET default_with_oids = false;
--
-- Name: test; Type: TABLE; Schema: public; Owner: postgres; Tablespace: 
--
CREATE TABLE test (
    i integer
);
ALTER TABLE public.test OWNER TO postgres;
--
-- Data for Name: test; Type: TABLE DATA; Schema: public; Owner: postgres
--
COPY test (i) FROM stdin;
1
2
\.
--
-- Name: public; Type: ACL; Schema: -; Owner: postgres
--
REVOKE ALL ON SCHEMA public FROM PUBLIC;
REVOKE ALL ON SCHEMA public FROM postgres;
GRANT ALL ON SCHEMA public TO postgres;
GRANT ALL ON SCHEMA public TO PUBLIC;
--
-- PostgreSQL database dump complete
--

利用pg_dump备份test库,只保存数据,insert语句格式:

[postgres@dbconsole bin]$ ./pg_dump --inserts -a test
--
-- PostgreSQL database dump
--
SET statement_timeout = 0;
SET client_encoding = 'UTF8';
SET standard_conforming_strings = off;
SET check_function_bodies = false;
SET client_min_messages = warning;
SET escape_string_warning = off;
SET search_path = public, pg_catalog;
--
-- Data for Name: test; Type: TABLE DATA; Schema: public; Owner: postgres
--
INSERT INTO test VALUES (1);
INSERT INTO test VALUES (2);
--
-- PostgreSQL database dump complete
--

物理备份

和Oracle一样,物理备份可以分为冷备份和热备份。冷备份就是将数据库实例关闭,然后直接复制data目录的所有文件,恢复时只需要将文件copy到data目录,无须利用日志(WAL–Write-Ahead Logging)进行恢复。而热备份,则需要利用日志将数据库恢复到一致状态,因此需要先将数据库置于归档模式。

PostgreSQL归档模式使用参数archive_mode控制,这是一个静态参数,修改postgresql.conf文件,加入如下参数,然后重启生效:

archive_mode = on
archive_command = 'cp "%p" /u01/postgresql/arch/"%f"'
archive_timeout = 600
postgres=# show archive_mode;
 archive_mode 
--------------
 on
(1 row)

由于postgresql没有归档进程,因此归档是通过外部命令(OS)完成的,archive_command用于指定该外部命令,具体格式请参考文档。如果是linux归档到本地,使用cp即可,如果是到远程,则可以使用scp或者rsync。archive_timeout强制N秒以后进行一次归档,这个和Oracle的archive_lag_target参数的作用一样。当然也可以手工进行日志切换:

postgres=# select pg_switch_xlog();
 pg_switch_xlog 
----------------
 0/7000000
(1 row)

热备份前,需要先将数据库置于备份状态,这一点和Oracle的手工也备份也是一样的。该命令会导致一次checkpoint,可能在业务高峰期会带来一些压力和风险,因此备份还是需要安排在业务低谷期间执行比较稳妥。

postgres=# select pg_start_backup('test_backup');
 pg_start_backup 
-----------------
 0/8000020
(1 row)

然后在os层面将data目录进行复制备份。完成后,再取消备份状态,该命令同时会执行一次日志切换,并进行归档,以保证热备份期间的日志都已经归档,保证恢复数据库到一致状态的所有日志都能从归档中找到:

postgres=# select pg_stop_backup();
 pg_stop_backup 
----------------
 0/8000088
(1 row)

备份完成后,可以在归档目录找到一个记录了本次备份信息的文件:

$ more 000000010000000000000008.00000020.backup
START WAL LOCATION: 0/8000020 (file 000000010000000000000008)
STOP WAL LOCATION: 0/8000088 (file 000000010000000000000008)
CHECKPOINT LOCATION: 0/8000020
START TIME: 2010-02-03 12:24:57 CST
LABEL: test_backup
STOP TIME: 2010-02-03 12:24:59 CST

PostgreSQL官方并没有提供和Oracle的rman一样的备份工具,不过因为PostgreSQL是开源的数据库,因此也有一个开源的工具pg_rman可以使用,从命令行来看功能已经非常强大,暂时还没有测试,有兴趣的可以研究一下。

pg_rman [ OPTIONS ] { init | backup | restore | show [ DATE | timeline ] |
 validate [ DATE ] | delete DATE }

PostgreSQL安装

PostgreSQL8.x版本的安装已经非常的简单了。EnterpriseDB制作了一键安装的版本,包括FreeBSD/Linux/Mac OS X/Solaris/Windows平台都有。不过即使使用源码编译,也非常的简单。各个版本的源码可以点这里下载

创建os用户

#useradd -g dba postgres
#su - postgres

编译

$tar -zxvf postgresql-8.4.2.tar.gz
$cd postgresql-8.4.2
$./configure --prefix=/OPT/postgresql --enable-profiling 
--with-blocksize=8 --with-wal-blocksize=8
$make && make install

其中with-blocksize指定数据块大小,默认8k,with-wal-blocksize指定日志块大小,默认也是8k。更多编译配置选项,可以通过./configure –help查看。

初始化database,注意PostgreSQL在服务端不支持GBK编码。

$cd /opt/postgresql/bin
$ ./initdb --encoding=utf8 -D /opt/postgresql/data
The files belonging to this database system will be owned by user "postgres".
This user must also own the server process.

The database cluster will be initialized with locale en_US.UTF-8.
The default text search configuration will be set to "english".

creating directory /opt/postgresql/data ... ok
creating subdirectories ... ok
selecting default max_connections ... 100
selecting default shared_buffers ... 32MB
creating configuration files ... ok
creating template1 database in /opt/postgresql/data/base/1 ... ok
initializing pg_authid ... ok
initializing dependencies ... ok
creating system views ... ok
loading system objects' descriptions ... ok
creating conversions ... ok
creating dictionaries ... ok
setting privileges on built-in objects ... ok
creating information schema ... ok
vacuuming database template1 ... ok
copying template1 to template0 ... ok
copying template1 to postgres ... ok

WARNING: enabling "trust" authentication for local connections
You can change this by editing pg_hba.conf or using the -A option the
next time you run initdb.

Success. You can now start the database server using:

    ./postgres -D /opt/postgresql/data
or
    ./pg_ctl -D /opt/postgresql/data -l logfile start

初始化完成后,会在 /opt/postgresql/data目录生成数据库的文件,至此,软件安装完毕,数据库创建完毕。

启动数据库

./pg_ctl -D /opt/postgresql/data/ -l /opt/postgresql/log/alert.log start

启动后,可以发现PostgreSQL实例一共运行了5个进程

$ ps -ef | grep postgres
postgres 17572     1  0 16:41 pts/2    00:00:00 /opt/postgresql/bin/postgres 
-D /opt/postgresql/data
postgres 17574 17572  0 16:41 ?        00:00:00 postgres: writer process                            
postgres 17575 17572  0 16:41 ?        00:00:00 postgres: wal writer process                        
postgres 17576 17572  0 16:41 ?        00:00:00 postgres: autovacuum launcher process               
postgres 17577 17572  0 16:41 ?        00:00:00 postgres: stats collector process                   
postgres 18679 20791  0 16:47 pts/2    00:00:00 grep postgres

其中wal writer process是日志写进程。

PostgreSQL简介

上个周末,无聊的时候关注了一下PostgreSQL。第一次尝试去安装PostgreSQL,还是好几年前的事了,那是8.0版本刚出来,终于开始原生的支持windows了,所以在自己电脑上折腾了一个。不过那时候也仅限于安装了一次而已,甚至psql的命令行都不知道怎么用。

同样作为开源关系型数据库,MySQL在这几年获得了更多的关注。大量的互联网公司都基于MySQL来构架系统,也导致MySQL DBA开始火热,一大堆年轻有为的同学投入到其中,渐成燎原之势。MySQL数据库火热了,MySQL AB公司却被sun收购,现在又随着sun要投入Oracle的怀抱,而且欧盟已经无条件批准这个收购,只剩下中国和俄罗斯,大局已定。作为商业数据库的绝对老大,Oracle的这次收购,让MySQL的支持者感到了威胁,其创始人甚至发起了一场保护MySQL(有墙),阻击Oracle收购的运动。

这也是PostgreSQL的机会,最近PostgreSQL的开发节奏很快,8.5已经连续出到了alpha3版,在这个版本中,最吸引我的是hot standby,类似于Oracle11g的active data guard,hot standby也可以在恢复的同时提供读服务,而以往版本,PostgreSQL的物理备库warm standby,则只能处于恢复状态,一旦open,则需要重做,比较痛苦。PostgreSQL的很多特性,都和Oracle相当的类似,甚至有一家商业化的公司EnterpriseDB,在致力于将PostgreSQL打包,使得应用程序从Oracle迁移到PostgreSQL更方便,据说80%的Oracle应用代码甚至不需要做修改就能在PostgreSQL运行。因此,我在twitter上说,如果PostgreSQL在人机交互的工具和配置部分,能够更加友好一点,完全是一个影子版本的Oracle。

PostgreSQL也支持mvcc多版本一致性控制。不过其实现的机制和Oracle的不一样。Oracle是将变化的前映像记录到单独的undo段中,而PostgreSQL则只是将前映像(Tuples)上做个标记,如果是delete,则相当于是逻辑删除,实际的数据还是在原来的段中,如果是insert,相当于先delete,再insert,而且会在原来的记录上加一条指向新记录的指针,形成一个链表,查询的时候需要沿着这个链表找到一致的数据。这样会造成一个问题,一段时间以后,dml操作使得数据段和索引段中都有大量的前映像信息存在,会严重影响数据查询的效率。PostgreSQL的mvcc的这种实现方式,带来的一个好处是回滚非常快,只需要修改前映像上的几个标志位即可,而不像oracle需要从undo段将前映像再复制回来。但是,这种方便回滚,却会损失查询性能的设计思路,真的比较诡异。PostgreSQL中有一个专门用来清理这些旧版本数据的程序,叫做vacuum。在以前的版本中,需要定期执行vacuum来优化数据存储结构。这对于DBA来说,无疑是一件痛苦的事情。直到8.1版本,引入了autovacuum,系统可以自动来进行这些清理工作,终于人性化了一点点。

在8.3版本,引入了一个新的特性HOT(Heap Only Tuples),主要的目的是努力避免update造成的性能低下的问题。其实这个HOT,说白了很简单,对于update,要实现mvcc,其机制还是一样的,区别在于select,在沿着链表找一致性数据的过程中,如果发现这个检查过的版本已经没有任何事物在引用了,就会顺便把清理工作做掉,而不是像以前要等vacuum来做。因此这会加大一点select的压力,但前人栽树,后人乘凉,接下来需要访问这些数据的其他select就会快很多了,这和Oracle的延迟块清除其实有些类似的,当然两者的设计目的并不一样。

无觅相关文章插件,快速提升流量