PostgreSQL 的 MVCC 机制解析

[[200391]]

导语

PostgreSQL是通过MVCC(Multi-Version Concurrency Control)来保证事务的原子性和隔离性,具体MVCC机制是怎样实现的,下面举些示例来做个简单解析以加深理解。

前提

表中隐藏的系统字段

PostgreSQL的每个表中都有些系统隐藏字段,包括:

  • oid: 对象标识符,生成的值是全局唯一的,表、索引、视图都带有oid,如果需要在用户创建的表中使用oid字段,需要显示指定“with oids”选项。
  • ctid: 每条记录(称为一个tuple)在表中的物理位置标识。
  • xmin: 创建一条记录(tuple)时,记录此值为当前事务ID。
  • xmax: 创建tuple时,默认为0,删除tuple时,记录此值为当前事务ID。
  • cmin/cmax: 标识在同一个事务中多个语句命令的序列值,从0开始,用于同一个事务中实现版本可见性判断

MVCC机制

MVCC机制通过这些隐藏的标记字段来协同实现,下面举几个示例来解释MVCC是如何实现的

  1. //seesion1: 
  2.  
  3. 创建表,显示指定oid字段: 
  4. testdb=# create table t1(id intwith oids; 
  5. CREATE TABLE 
  6.  
  7. 插入几条记录 
  8. testdb=# insert into t1 values(1); 
  9. INSERT 17569 1 
  10. testdb=# insert into t1 values(2); 
  11. INSERT 17570 1 
  12. testdb=# insert into t1 values(3); 
  13. INSERT 17571 1 

查询当前表中的tuple信息,xmin为创建tuple时的事务ID,xmax默认为0

  1. testdb=# select ctid, xmin, xmax, cmin, cmax, oid, id from t1; 
  2.  ctid  |   xmin   | xmax | cmin | cmax |  oid  | id 
  3. -------+----------+------+------+------+-------+---- 
  4.  (0,1) | 80853357 |    0 |    0 |    0 | 17569 |  1 
  5.  (0,2) | 80853358 |    0 |    0 |    0 | 17570 |  2 
  6.  (0,3) | 80853359 |    0 |    0 |    0 | 17571 |  3 
  7. (3 rows

接下来,我们更新某个tuple的字段,将tuple中id值为1更新为4,看看会发生什么

  1. testdb=# begin
  2. BEGIN 
  3. testdb=# select txid_current(); 
  4.  txid_current 
  5. -------------- 
  6.      80853360 
  7. (1 row) 
  8.  
  9. testdb=# update t1 set id = 4 where id = 1; 
  10. UPDATE 1 

查看tuple详细信息

  1. testdb=# select ctid, xmin, xmax, cmin, cmax, oid, id from t1; 
  2.  ctid  |   xmin   | xmax | cmin | cmax |  oid  | id 
  3. -------+----------+------+------+------+-------+---- 
  4.  (0,2) | 80853358 |    0 |    0 |    0 | 17570 |  2 
  5.  (0,3) | 80853359 |    0 |    0 |    0 | 17571 |  3 
  6.  (0,4) | 80853360 |    0 |    0 |    0 | 17569 |  4 
  7. (3 rows

可以看到id为1的tuple(oid=17569)已经被修改了,id值被更新为4,另外ctid、xmin字段也被更新了,ctid值代表了该tuple的物理位置,xmin值是创建tuple时都已经写入,这两个字段都不应该被更改才对,另起一个seesion来看下(当前事务还未提交)

  1. //seesion2: 
  2.  
  3. testdb=# select ctid, xmin, xmax, cmin, cmax, oid, id from t1; 
  4.  ctid  |   xmin   |   xmax   | cmin | cmax |  oid  | id 
  5. -------+----------+----------+------+------+-------+---- 
  6.  (0,1) | 80853357 | 80853360 |    0 |    0 | 17569 |  1 
  7.  (0,2) | 80853358 |        0 |    0 |    0 | 17570 |  2 
  8.  (0,3) | 80853359 |        0 |    0 |    0 | 17571 |  3 
  9. (3 rows

可以看到id为1的tuple(oid=17569)还存在,只是xmax值被标记为当前事务Id。 原来更新某个tuple时,会新增一个tuple,填入更新后的字段值,将原来的tuple标记为删除(设置xmax为当前事务Id)。同理,可以看下删除一个tuple的结果

  1. //seesion1: 
  2. testdb=# delete from t1 where id = 2; 
  3. DELETE 1 
  4.  
  5. //seesion2: 
  6. testdb=# select ctid, xmin, xmax, cmin, cmax, oid, id from t1; 
  7.  ctid  |   xmin   |   xmax   | cmin | cmax |  oid  | id 
  8. -------+----------+----------+------+------+-------+---- 
  9.  (0,1) | 80853357 | 80853360 |    0 |    0 | 17569 |  1 
  10.  (0,2) | 80853358 | 80853360 |    1 |    1 | 17570 |  2 
  11.  (0,3) | 80853359 |        0 |    0 |    0 | 17571 |  3 
  12. (3 rows

删除某个tuple时也是将xmax标记为当前事务Id,并不做实际的物理记录清除操作。另外cmin和cmax值递增为1,表明了同一事务中操作的顺序性。在该事务(seesion1)未提交前,其他事务(seesion2)可以看到之前的版本信息,不同的事务拥有各自的数据空间,其操作不会对对方产生干扰,保证了事务的隔离性。

提交事务,查看最终结果如下:

  1. //seesion1: 
  2. testdb=# commit
  3. COMMIT 
  4. testdb=# select ctid, xmin, xmax, cmin, cmax, oid, id from t1; 
  5.  ctid  |   xmin   | xmax | cmin | cmax |  oid  | id 
  6. -------+----------+------+------+------+-------+---- 
  7.  (0,3) | 80853359 |    0 |    0 |    0 | 17571 |  3 
  8.  (0,4) | 80853360 |    0 |    0 |    0 | 17569 |  4 
  9. (2 rows

但是,如果我们不提交事务而是回滚,结果又是如何?

  1. testdb=# begin ; 
  2. BEGIN 
  3. testdb=# update t1 set id = 5 where id = 4; 
  4. UPDATE 1 
  5. testdb=# rollback
  6. ROLLBACK 
  7. testdb=# select ctid, xmin, xmax, cmin, cmax, oid, id from t1; 
  8.  ctid  |   xmin   |   xmax   | cmin | cmax |  oid  | id 
  9. -------+----------+----------+------+------+-------+---- 
  10.  (0,3) | 80853359 |        0 |    0 |    0 | 17571 |  3 
  11.  (0,4) | 80853360 | 80853361 |    0 |    0 | 17569 |  4 
  12. (2 rows
  13. xmax标记并未清除,继续新增一条记录: 
  14.  
  15. testdb=# insert into t1 values(5); 
  16. INSERT 17572 1 
  17. testdb=# select ctid, xmin, xmax, cmin, cmax, oid, id from t1; 
  18.  ctid  |   xmin   |   xmax   | cmin | cmax |  oid  | id 
  19. -------+----------+----------+------+------+-------+---- 
  20.  (0,3) | 80853359 |        0 |    0 |    0 | 17571 |  3 
  21.  (0,4) | 80853360 | 80853361 |    0 |    0 | 17569 |  4 
  22.  (0,6) | 80853362 |        0 |    0 |    0 | 17572 |  5 
  23. (3 rows

发现没有清理掉新增的tuple,消除原有tuple上的xmax标记,这是为何?处于效率的原因,如果事务回滚时也进行清除标记,可能会导致磁盘IO,降低性能。那如何判断该tuple的是否有效呢?答案是PostgreSQL会把事务状态记录到clog(commit log)位图文件中,每读到一行时,会到该文件中查询事务状态,事务的状态通过以下四种来表示:

  • #define TRANSACTION_STATUS_IN_PROGRESS=0x00 正在进行中
  • #define TRANSACTION_STATUS_COMMITTED=0x01 已提交
  • #define TRANSACTION_STATUS_COMMITTED=0x02 已回滚
  • #define TRANSACTION_STATUS_SUB_COMMITTED=0x03 子事务已提交

MVCC保证原子性和隔离性

原子性

事务的原子性(Atomicity)要求在同一事务中的所有操作要么都做,要么都不做。根据PostgreSQL的MVCC规则,插入数据时,会将当前事务ID写入到xmin中,删除数据时,会将事务ID写入xmax中,更新数据相当于先删除原来的tuple再新增一个tuple,增删改操作都保留了事务ID,根据事务ID提交或撤销该事务中的所有操作,从而保证了事务的原子性。

隔离性

事务的隔离性(Isolation)要求各个并行事务之间不能相互干扰,事务之间是隔离的。PostgreSQL可读取的数据是xmin小于当前的事务ID且已经提交。对某个tuple进行更新或删除时,其他事务读取的就是这个tuple之前的版本。

MVCC的优势

读写不会相互阻塞,写操作并没有堵塞其他事务的读,在写事务未提交前,读取的都是之前的版本,提高了并发的访问效率。

事务可以快速回滚,操作后的tuple都带有当前事务ID,直接标记clog文件中对应事务的状态就可达到回滚的目的。

MVCC带来的问题

事务ID回卷问题

PostgreSQL也需要事务ID来确定事务的先后顺序,PostgreSQL中,事务被称为XID,获取当前XID:

  1. testdb=# select txid_current(); 
  2.  txid_current 
  3. -------------- 
  4.      80853335 
  5. (1 row) 

事务ID由32bit数字表示,当事务ID用完时,就会出现新的事务ID会比老ID小,导致事务ID回卷问题(Transaction

ID Wraparound)。 PostgreSQL的事务ID规则:

  • 0: InvalidXID,无效事务ID
  • 1: BootstrapXID,表示系统表初使化时的事务
  • 2: FrozenXID,冻结的事务ID,比任务普通的事务ID都旧。

– 大于2的事务ID都是普通的事务ID。

当***和最旧事务之差达到2^31时,就把旧事务换成FrozenXID,然后通过公式((int32)(id1 – id2)) < 0比较大小即可

垃圾数据问题

根据MVCC机制,更新和删除的记录都不会被实际删除,操作频繁的表会积累大量的过期数据,占用磁盘空间,当扫描查询数据时,需要更多的IO,降低查询效率。PostgreSQL的解决方法是提供vacuum命令操作来清理过期的数据。

原文链接:https://www.qcloud.com/community/article/528634,作者:黄辉

【本文是专栏作者“腾讯云技术社区”的原创稿件,转载请通过联系原作者获取授权】

戳这里,看该作者更多好文

文章来源网络,作者:管理,如若转载,请注明出处:https://shuyeidc.com/wp/263420.html<

(0)
管理的头像管理
上一篇2025-05-04 13:39
下一篇 2025-05-04 13:40

相关推荐

  • 自营机房和代理机房有什么区别,哪个更稳定可靠

    自营机房和代理机房最根本的区别在于基础设施的归属权与运营主体,自营机房由服务商自主建设、维护并持证运营,提供全程可控的IDC服务;代理机房则通过转售第三方资源,缺乏底层控制力, 选择自营还是代理,直接关系到业务的稳定性、安全边际和售后深度,近年来,随着工信部对IDC行业持证经营的要求逐步收紧,越来越多企业开始关……

    2026-07-26
    0
  • 高防服务器常见的套路猫腻如何辨别,有哪些常见的坑?

    辨别高防服务器猫腻的关键在于核实防御能力真实性、检查机房资质与服务条款,避免被低价宣传和虚假承诺误导,常见猫腻有哪些虚假防御能力不少服务商声称提供单机百G防御,实际可能只有几十G,甚至多个用户共享同一防御带宽,一旦遭遇真实攻击,防护效果远低于宣传值,部分商家还会利用“突发清洗”概念,只在攻击峰值时短暂启用清洗设……

    2026-07-26
    0
  • 选择IDC服务商千万不能忽略哪几点,有哪些注意事项?

    选择IDC服务商,资质和合规性是最容易被忽视却至关重要的环节,一家没有完整资质的服务商,无论价格多低都不值得选择, 近年来,企业数字化转型加速,对IDC服务的需求持续增长,但服务商水平参差不齐,如果你正在挑选IDC服务商,需要从资质、机房、服务、成本等几个核心维度仔细考察,才能避免业务埋雷,资质认证:合规经营的……

    2026-07-26
    0
  • 低价服务器到底为什么不能买,有什么风险?

    低价服务器看似省钱,实则隐藏性能差、不稳定、数据安全无保障、售后缺失甚至跑路等风险,最终可能让你付出更高代价,低价服务器常见的“坑”性能陷阱:超售严重低价服务器的核心套路是超售,一台物理机卖给几十甚至上百个用户,CPU、内存、带宽全面拥挤,你买到的所谓“独享”资源,实际上与邻居争抢,高峰期响应延迟直接拉满,用……

    2026-07-26
    0
  • 新手买站群服务器怎么避坑不被坑,推荐哪家服务商比较好?

    新手买站群服务器,踩坑的根源往往在于只看价格不看资质,要避开风险,必须从持牌经营、机房实地、IP池质量三个维度入手,缺一不可,站群服务器常见的“坑”有哪些IP被“污染”或“墙”了多数新手贪便宜,买到被滥用过的IP段,这类IP发出去的邮件被拒,收录慢,甚至直接被墙,你花时间搭起来的站群,还没开始跑流量就废了,IP……

    2026-07-26
    0

发表回复

您的邮箱地址不会被公开。必填项已用 * 标注