PostgreSQL相比oracle有一个很明显的数据库对象不见了——undo表空间。而在pg中用来解决没有undo表空间带来的问题,便引入了vacuum这一机制。
pg中之所以没有undo表空间,是因为其和oracle的mvcc机制不同。oracle中对于类似更新数据的操作,会将原先旧版本数据放到回滚段中,保证了隔离性,也保证了读写不冲突。但是pg中的做法是:对旧版本数据做一个标记,仍然和新版本数据存放在一起。那这些旧版本数据什么时候删除呢?这便是vacuum所要做的事情。
看上去并没有太大问题,但是在pg中实现起来却有很多地方需要考虑。
因为在pg中事务ID(XID)使用32位无符号数来表示,顺序产生,依次递增。也就是说当事务id达到40亿就会被用完,那新的XID 为0,这样就会带来一个很大的问题:因为事务的可见性要求:每个事务只能看到它之前的事务(即xid小于自己的),如果最新的事务XID为0,那么对于旧事务来说它就是可见的了,这样便违反了事务的可见性。
那么pg是如何解决这一问题的呢?
PostgreSQL保留以下三个特殊的txid:
- 0表示无效的 txid。
- 1表示Bootstrap txid,仅用于数据库集群的初始化。
- 2表示冻结的 txid
当事务id达到2^31时,就会将旧的事务id都标记为冻结,这样新的事务id哪怕比旧的事务id小也是不可见的。
例如下图:
从txid 100的角度来看,大于100的txid是“将来的”,并且它们在txid 100中是不可见的。小于100的txid是“过去的”并且可见。
由于在实际系统中txid空间不足,因此PostgreSQL将txid空间视为一个圆。之前的21亿txid是“过去的”,接下来的21亿txid是“未来的”。
我们可以查询当前数据库的xid:
bill=# SELECT datname, age(datfrozenxid) FROM pg_database;
datname | age
-------------+----------
postgres | 62424
bill | 8072
template0 | 34064385
bosswg_test | 61
template1 | 34064385
citus | 8072
(6 rows)
执行freeze操作:
bill=# vacuum FREEZE ;
VACUUM
–可以发现数据库的xid变成了4
bill=# SELECT datname, age(datfrozenxid) FROM pg_database where datname='bill';
datname | age
---------+-----
bill | 4
(1 row)
数据库后台的autovacuum进程便是用来完成这一工作的,但是有一点需要注意,事务的冻结操作是会有非常大的IO消耗以及cpu消耗(所有表的所有行读一遍,重置标记)。如果冻结操作很慢(比如系统资源不足),导致事务id耗净,最终的结果就是,数据库拒绝所有事务的执行,直到冻结操作结束。
如果事务id限制则会出现数据库无法连接,这个时候只能通过单用户进入执行freeze操作了:
pg12@test180-> postgres --single -D $PGDATA bill
PostgreSQL stand-alone backend 12beta2
backend> vacuum freeze;
backend>
因此从pg12开始支持了zheap的插件,用来实现oracle的多版本机制。