Postgresql MVCC

MVCC原理

多版本并发控制(Multi-Version Concurrency Control, MVCC) , 是数据库中并发访问数据时保证数据一致性的一种方法。

在并发操作中, 当正在写时, 如果有用户在读, 这时写可能只写了一半, 如一行的前半部分刚写入, 后半部分还没有写入, 这时可能读的用户读取到的数据行的前半部分数据是新的, 后半部分数据是原来的, 这就导致了数据一致性问题。

解决这个问题的最简单的方法是使用读写锁, 写的时候不允许读, 正在读的时候也不允许写, 但这种方法会导致读和写的操作不能并发执行。
于是, 有人想到了一种能够让读写并发执行的方法, 这种方法就是MVCC。
MVCC方法是写数据时, 原数据并不删除, 并发的读还能读到原数据, 这样就不会有数据一致性问题了。

实现MVCC的方法有以下两种:

  • 第一种: 写新数据时, 把原数据移到一个单独的位置, 如回滚段中, 其他用户读数据时, 从回滚段中把原数据读出来。
  • 第二种: 写新数据时, 原数据不删除, 而是把新数据插入进来。

PostgreSQL数据库使用的是第二种方法, 而Oracle数据库和MySQL数据库中的InnoDB引擎使用的是第一种方法。

PostgreSQL中的多版本并发控制

PostgreSQL中的多版本实现是通过把原数据留在数据文件中, 新插入一条数据来实现多版本的功能的。
每张表上都有4个系统字段“xmin”“xmax”“cmin”“cmax”, 这4个字段就是为多版本的功能而添加的。(参见《Postgresql表中系统字段详解》

当两个事务同时访问记录时, 通过参考xmin和xmax的标记判断记录的版本, 根据版本号与自己当前的事务标识进行比较, 确定自己的数据权限。
当删除数据时, 记录并没有从数据块中被删除, 空间也没有立即释放。

PostgreSQL的多版本实现中首先要解决的是原数据的空间释放问题。
PostgreSQL通过运行Vaccum进程来回收之前的存储空间, 默认PostgreSQL数据库中的AutoVacuum是打开
的, 也就是说, 当一个表的更新量达到一定值时, AutoVacuum自动回收空间。
当然也可以关闭AutoVacuum进程, 然后在业务低峰期手动运行VACUUM命令来回收空间。

在PostgreSQL中, 若一个事务执行失败, 在数据文件中该事务产生的数据并不会在事务回滚时被清理掉。 为什么要这样做呢? 为什么不在事务提交时把这些数据标记成有效,而在事务回滚时把这些数据标记成无效呢?
这是出于效率的考虑。
若事务提交或回滚时再次标记数据, 那这些数据就有可能会被刷新到磁盘中, 再次标记会导致另一次I/O, 从而降低性能。

那么如何知道这些数据是有效还是无效呢?
PostgreSQL通过记录事务的状态来实现。
数据行上记录了xmin和xmax, 只需了解xmin和xmax对应的事务是成功提交还是回滚了, 就可以知道这些数据行是否有效。

PostgreSQL把事务状态记录在Commit Log中,简称CLOG, CLOG在数据目录的pg_clog子目录下, 示例如下:

osdba@osdba-VirtualBox:~/pgdata$ ls -l pg_clog
total 8
-rw------- 1 osdba osdba 8192 Nov 30 21:43 0000

事务的状态有以下4种:

  • TRANSACTION_STATUS_IN_PROGRESS=0x00: 表示事务正在进行中。
  • TRANSACTION_STATUS_COMMITTED=0x01: 表示事务已提交。
  • TRANSACTION_STATUS_ABORTED=0x02: 表示事务已回滚。
  • TRANSACTION_STATUS_SUB_COMMITTED=0x03: 表示子事务已提交。

事务ID, 在PostgreSQL中有时缩写为xid, 是一个32bit的数字。
有以下3个特殊的事务ID是给系统内部使用的, 代表特殊的含义:

  • InvalidTransactionId=0: 表示是无效的事务ID。
  • BootstrapTransactionId=1: 表示系统表初使化时的事务ID。
  • FrozenTransactionId=2: 冻结的事务ID。

所以数据库系统第一个正常的事务ID是从3开始的, 然后连续递增, 达到最大值后,再从3开始。
事务ID为0、 1、 2的始终保留。

通常, 使用值为0的事务ID是为了让内部编程更为方便, 当PostgreSQL内部的事务ID设置为0时, 表示它是一个无效的事务ID。
比如, 使用函数GetCurrentTransactionIdIfAny查询当前的事务ID时, 如果返回的事务ID为0, 则表示当前还没有分配事务ID。

值为1的事务ID是Initdb服务初始化系统表时在表上填写的事务ID, 此时数据库还没有启动, 但在系统表中的cmin下也需要一个有效的事务ID, 这个事务ID就为1, 示例如下:

os dba=# select cmin, cmax, relname from pg_class where relname in ('pg_type','pg_attribute');
cmin | cmax | relname
------+------+--------------
1 | 1 | pg_type
1 | 1 | pg_attribute
(2 rows)

事务回卷问题
事务ID一直递增, 总会到达4字节整数的最大值, 到达最大值后再从头开始时, 以前的事务ID都会比当前的事务ID大, 在进行比较时, 会认为以前的事务ID是将来的事务ID, 这会导致严重的问题, 即事务ID回卷的问题。
另外, PostgreSQL中多版本实现中经常需要判断事务之间的新旧关系, 例如: 如果数据行中的已提交的事务比当前事务更早, 则在当前事务中这行数据应该是可见的。
在事务ID没有回卷时, 简单比较两个事务ID的大小就可以知道事务之间的先后关系。
如4294967290<4294967295, 所以事务ID为4294967290的事务必然比事务ID4294967295的事务更早。
但在事务ID回卷后, 事务ID为5的事务应该比事务ID4294967295的事务更新, 再简单地比较大小就行不通了。

为了解决事务回卷问题和满足比较事务新旧的需求, PostgreSQL中规定, 存在的最早和最新两个事务之间的年龄差最多是231, 而不是无符号整数的最大范围232, 只有该范围的一半, 当要超过231时, 就把旧的事务换成一个特殊的事务ID, 也就是前面介绍的名为“FrozenTransactionId”的特殊事务。
当正常事务ID与冻结事务ID进行比较时, 会认为正常事务ID比冻结事务ID更新。

做了以上的规定后, 两个普通的事务ID比较新旧就可以使用如下公式:

((int32) (id1 - id2)) < 0

如果该公式的返回结果为真, 则表明事务id1比事务id2更早。
从这个公式中可以看出, 当事务ID没有回卷时, 上面的公式相当于直接比较大小, 在事务ID回卷后, 如id1=4294967295, id2=5, id1-id2=4294967290, 这是一个正数, 但转换成有符号的int32时, 由于超出了有符号数的取值范围, 会转换成一个负数, 这样的结果对于事务ID回卷后的情况也适用。

PostgreSQL多版本的优劣分析

Oracle数据库和MySQL数据库的InnoDB引擎也都实现了多版本的功能, 但它们与PostgreSQL的实现方式是不一样的, 在这两个数据库中, 旧版本的数据并不记录在原先的数据块中, 而是被记录在回滚段中, 如果要读取旧版本的数据, 需要根据回滚段的数据重构旧版本数据。

PostgreSQL的多版本机制与Java虚拟机的垃圾回收机制比较相像。
事务提交前, 只需要访问原来的数据即可; 提交后, 系统更新元组的存储标识, 直到Vaccum进程收回为止。

相对于InnoDB和Oracle, PostgreSQL的多版本的优势在于以下几点:

  • 事务回滚可以立即完成, 无论事务进行了多少操作。
  • 数据可以进行很多更新, 不必像Oracle和InnoDB那样需要经常保证回滚段不会被用完, 也不会像Oracle数据库那样, 经常遇到“ORA-1555”错误的困扰。

相对于InnoDB和Oracle, PostgreSQL的多版本的劣势在于以下几点:

  • 旧版本数据需要清理。 PostgreSQL清理旧版本称为VACUUM, 并提供了VACUUM命令进行清理。
  • 旧版本的数据会导致查询更慢一些, 因为旧版本的数据存储于数据文件中, 查询时需要扫描更多的数据块。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,039评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,223评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,916评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,009评论 1 291
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,030评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,011评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,934评论 3 416
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,754评论 0 271
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,202评论 1 309
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,433评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,590评论 1 346
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,321评论 5 342
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,917评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,568评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,738评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,583评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,482评论 2 352

推荐阅读更多精彩内容