MVCC的介绍
MVCC技术,是multiversion concurrency control的缩写,翻译成中文是多版本并发控制的意思。目前多数DB都采用了这一技术,比如Oracle,Innodb,Berkeley DB等,采用这种方法可有效降低锁的开销。
MVCC的实现
MVCC是干嘛的呢?举例来说,当数据库做一个更新操作时,并不是将老的数据删除,再将新的数据覆盖上去,相反,它会把老的数据做一个标记隔离出去(old version),然后再新增新的数据作为一个新的版本,这个时候新老数据都是存在数据库里的。但在不同的数据库里,其存储形式是不一样的。在Postgresql里,新老版本数据是混在一起的,Oracle和Innodb里是分开存储的,像Oracle有UNDO作为区分。或者说得更简单一点,读不会阻塞写操作,反过来也是一样。两种形式各有优缺点。
MVCC下的读事务通常以时间点或增长的事务ID来标记数据库里从个点开始读以及这个点所对应的数据。在Postgresql中,每一个事务都会获得一个事务ID,叫 XID, 这个可以是一个insert,update,delete或者是用begin...end包裹起来的一组sql。当这个事务开始的时候,Postgres会增加XID值并赋给当前的事务,Postgres也会对系统里的每一行附上上事务信息,这可以用来判断该行在其他事务中是否可见。
MVCC技术,是multiversion concurrency control的缩写,翻译成中文是多版本并发控制的意思。目前多数DB都采用了这一技术,比如Oracle,Innodb,Berkeley DB等,采用这种方法可有效降低锁的开销。
MVCC的实现
MVCC是干嘛的呢?举例来说,当数据库做一个更新操作时,并不是将老的数据删除,再将新的数据覆盖上去,相反,它会把老的数据做一个标记隔离出去(old version),然后再新增新的数据作为一个新的版本,这个时候新老数据都是存在数据库里的。但在不同的数据库里,其存储形式是不一样的。在Postgresql里,新老版本数据是混在一起的,Oracle和Innodb里是分开存储的,像Oracle有UNDO作为区分。或者说得更简单一点,读不会阻塞写操作,反过来也是一样。两种形式各有优缺点。
MVCC下的读事务通常以时间点或增长的事务ID来标记数据库里从个点开始读以及这个点所对应的数据。在Postgresql中,每一个事务都会获得一个事务ID,叫 XID, 这个可以是一个insert,update,delete或者是用begin...end包裹起来的一组sql。当这个事务开始的时候,Postgres会增加XID值并赋给当前的事务,Postgres也会对系统里的每一行附上上事务信息,这可以用来判断该行在其他事务中是否可见。
XID在Postgresql中是怎么做的呢, 或者说怎么获取的呢?当插入一行的时候,Postgres存储XID值并叫它
XMIN
。之前也有介绍,这是一个隐藏字段。对当前事务来说,每一行被提交的数据其对应的XMIN值比当前的XID小,那么都是可见的。举个很简单的例子:你可以开启一个事务,假如以begin开始,然后插入几行数据,在Commit之前,这些数据对其他事务来说都是不可见的,直到你做了Commit。一旦我们做了Commit操作(XID会增长),对其他事务来说已经满足XMIN<XID,所以其他事务就能看到在该事务提交后的东西。获得当前事务的XID值比较简单:SELECT txid_current();
对于DELETE和UPDATE来说,MVCC的机制也是类似的,略有不同的是Postgres在执行DELETE和UPDATE操作时对每一行还存储了XMAX这一隐藏列。也是通过这个字段来决定当前的删除或者更新对其他事务来说是否可见。
具体示例可以参考:http://my.oschina.net/Kenyon/blog/63668
当两个事务在某个时间点同时更新某一行数据时,是怎么处理的呢?这里将引入事务的隔离级别这一概念。Postgres可满足四种模式级别来处理这种场景,默认的是
READ COMMITTED
,它是完成初始化事务,然后执行语句获取行数据。如果在等待过程中行有了变化,他将重新开始。举例来说,你正在执行一个带where条件的UPDATE操作,这个where条件将在你初始化事务提交后返回,如果where满足条件,那么Update将会执行,不满足则会Hang住(很容易模拟这个场景,开启两个会话,对同一行数据进行UPDATE,只要不提交即可,后面的UPDATE会挂住,直到前面的事务提交或回滚)
如果想更清楚地看清过程,可以设置事务隔离级别为
SERIALIZABLE
,在这种模式下同样的两个UPDATE其中一个将会报错:
ERROR: could not serialize access due to concurrent update
ERROR: could not serialize access due to concurrent update
MVCC的问题:
在Postgres中,UPDATE实际上会复制一份修改记录,而DELETE不会立即移除那条被删的记录,其新旧版本是共存的。所以UPDATE全表时,其大小通常会增长一倍,不知道内部结构的人往往很迷惑,但现在可以释然了。对于DELETE掉的数据,只是标注它被删除了,然后更新了一下XID的值。当事务结束完了以后,这些数据仍然存在,但是是不可见的,我们称之为DEAD ROWS。这样会带来另一个问题,事务ID(XID)是32bit的,只能支持大约40亿(2^32 = 4 294 967 296)次事务,当XID达到最大值时,它会自动回零,这会带来一个很严重的问题,因为所有行的XID都比0大,这会导致行数据不可见(就好比现在的数据成了将来的数据了)。
在Postgres中,UPDATE实际上会复制一份修改记录,而DELETE不会立即移除那条被删的记录,其新旧版本是共存的。所以UPDATE全表时,其大小通常会增长一倍,不知道内部结构的人往往很迷惑,但现在可以释然了。对于DELETE掉的数据,只是标注它被删除了,然后更新了一下XID的值。当事务结束完了以后,这些数据仍然存在,但是是不可见的,我们称之为DEAD ROWS。这样会带来另一个问题,事务ID(XID)是32bit的,只能支持大约40亿(2^32 = 4 294 967 296)次事务,当XID达到最大值时,它会自动回零,这会带来一个很严重的问题,因为所有行的XID都比0大,这会导致行数据不可见(就好比现在的数据成了将来的数据了)。
Postgres对此有一个进程是专门处理这个问题的,那就是VACUUM,高版本的Postgres已经是自动运行的了(auto_vacuum),是内置进程之一。通常倒也不用怎么关注,但是如果该进程异常就要注意了,所以平时的监控也是很要紧的。vacuum full Obj时会对该对象的DEAD ROWS进行清理,这个可以很容易模拟,UPDATE一个大表,VACUUM前后查看表的大小可看出变化。
检查数据库对象XID值的方法,age的值异常大的话就要注意了,可作为日常监控sql之一:
SELECT relname,age(relfrozenxid) FROM pg_class WHERE relkind = 'r' order by 2 desc limit 20;
SELECT datname,age(datfrozenxid) FROM pg_database order by 2 desc limit 20;
转译参考:
检查数据库对象XID值的方法,age的值异常大的话就要注意了,可作为日常监控sql之一:
SELECT relname,age(relfrozenxid) FROM pg_class WHERE relkind = 'r' order by 2 desc limit 20;
SELECT datname,age(datfrozenxid) FROM pg_database order by 2 desc limit 20;
转译参考:
http://en.wikipedia.org/wiki/Multiversion_concurrency_control
https://devcenter.heroku.com/articles/postgresql-concurrency