君哥聊技术

面试官:PostgreSQL的MVCC是怎样实现的?

大家好,我是君哥。

为了解决读写冲突,数据库提供了多种手段,比如表级和行级锁。而为了进一步提升并发性能,引入了 MVCC(多版本并发控制)。今天来聊一聊 PostgreSQL 的 MVCC 是怎样实现的。

1.隔离级别

1.1 四类问题

使用数据库时,事务的并发常常会遇到四类问题:

  • 脏读:事务 A 读取了事务 B 未提交的修改数据。如果事务 B 回滚,事务 A 读取的数据就是无效的脏数据。 ‌
  • 不可重复读:同一事务内多次读取同一行数据,这条数据因为被其他事务修改过并且已经提交事务,导致多次读取到的结果不一致。
  • 幻读:同一事务内多次查询同一范围内的数据,因其他事务插入或删除符合条件的数据,导致事务在后面读取到的结果集不一样,像产生了幻觉。
  • 序列化异常:成功提交一个事务,结果可能跟按照不同顺序提交这组事务的结果不一致。

1.2 隔离级别

针对脏读、幻读、不可重复读的问题,数据库引入了 4 种标准隔离级别:

  • 串行化(Serializable):事务对数据读写都是串行化的。
  • 可重复读(Repeatable Read):事务执行过程中,多次读取同一行数据,读取结果一致。
  • 读已提交数据(Read Committed):事务执行过程中,如果有其他事务修改了数据并且提交事务,当前事务可以读取到最新提交的数据。
  • 读未提交数据(Read Uncommitted):事务执行过程中,可以读取到其他事务未提交的数据。

在多数数据库中,我们可以使用四个标准事务隔离级别中的任何一个。 但其实 PostgreSQL 内部只实现了三个不同的隔离级别,PostgreSQL 的 Read Uncommitted 级别跟 Read Committed 类似。这是将标准隔离级别映射到 PostgreSQL 的 MVCC 架构的唯一合理方法。如下表格:

Isolation Level
Dirty Read
Nonrepeatable Read
Phantom Read
Serialization Anomaly
Read uncommitted
✗(pg不允许)
✔
✔
✔
Read committed
✗
✔
✔
✔
Repeatable read
✗
✗
✗(pg不允许)
✔
Serializable
✗
✗
✗
✗

这个表格也可以看出,在 Repeatable read 这个隔离级别下,PostgreSQL 是不允许出现幻读的。这个也可以看出 PostgreSQL 的隔离级别比 SQL 标准有更高要求的保障。

PostgreSQL 的一些数据类型和函数对事务有特殊的规则,比如 sequence 的修改对其他事务立即可见,即使修改 sequence 的事务回滚,sequence 的修改也不能回滚。

2.MVCC 原理

我们知道,在 MySQL 中,InnoDB 采用聚簇索引保存数据,MVCC 则通过数据的版本链来实现,版本链中旧版本的数据保存在 Undo Log,数据表中只保留最新版本的数据。

PostgreSQL 采用堆表存储数据,MVCC 实现机制也有所不同。当数据被修改时,旧版本的数据仍然保存在数据表中,新版本的数据作为新的行插入。通过在每行数据中额外维护版本相关字段,实现 MVCC。

2.1 存储结构

在 PostgreSQL 的堆表结构中,存储数据的单元是 tuple(元组)。我们可以把每个 tuple 比作一行数据的一个版本,结构如下:

Image

在用户数据之前,会增加 8 个字段,其中 null bitmap 和 object ID 是可选,就是说会固定有 23 个字节。

  • t_xmin:最小事务 id,也就是插入这一行数据的事务 id。
  • t_xmax:最大事务 id,也就是删除这一行数据的事务 id。
  • t_cid:CommandId,用于记录插入或删除操作是事务中的第几条命令。
  • t_xvac:事务 id,用来记录 VACUUM 移动行的版本。
  • t_ctid:数据指针,指向事务对此行数据修改的当前版本或者更新的版本。结构为(页号, 行偏移),例如 (0,1) 表示第 0 页的第 1 行。
  • t_infomask2:表中字段数量和各种标志位。
  • t_infomask:各种标志位。
  • t_hoff:记录用户数据的偏移量。
  • null bitmap:记录表中的空值,只有 HEAP_HASNULL 标识被设置在 t_infomask,null bitmap 才会有值。如果有值,则为表中每个字段分配一个 bit(1 代表非空,0 代表空),也就是说这里定义的 bit 的数量等于 t_infomask2 定义的字段数量。如果 null bitmap 没有值,则表中所有字段都是非空。
  • object ID:兼容老版本,记录 OID(每个表、索引、视图、函数、类型等都会被分配一个唯一的 OID)。如果 HEAP_HASOID_OLD 标识被设置在 t_infomask,则 object ID 就会有值。

2.2 更新操作

下面我们看一下 PostgreSQL 存储结构的使用。首先我们执行一条插入 SQL:

insertinto tb_test(id, name) value(1, 'jam');

这条 SQL 执行的事务 xid=10,执行这个事务后,插入一条 tuple,记为 tuple1, tuple1 的值是 [t_xmin:10, t_xmax:0, t_cid:0, t_ctid: (0,1)]

  • t_xmin:10,插入这个 tuple 的事务 xid=10。
  • t_xmax:0,这行数据未被删除。
  • t_cid:0,事务 xid=10 这个事务的第一个 SQL 命令。
  • t_ctid:设置为 (0,1),指向自身,因为这是该 tuple 的最新版本。

接着,我们执行第二个事务 xid=11,里面包含一条更新语句。

update tb_test setname = 'tom'whereid = 1;

这条 SQL 更新这条数据的 name 为 tom,这时需要插入一条新的 tuple,记为 tuple2,tuple2 的值是 [t_xmin:11, t_xmax:0, t_cid:0, t_ctid: (0,2)]

  • t_xmin:11,插入这个 tuple 的事务 xid=11。
  • t_xmax:0,这行数据未被删除。
  • t_cid:0,事务 id xid=11 这个事务的第 1 个 SQL 命令。
  • t_ctid:设置为 (0,2),指向自身。

这时 tuple1 会变成 [t_xmin:10, t_xmax:11, t_cid:0, t_ctid: (0,2)]

  • t_xmax:11,这个 tuple 被 xid=11 这个事务逻辑删除。
  • t_ctid:设置为 (0,2),指向 tuple2。

接着再执行第三个事务 xid=12,里面包含一条删除语句:

deletefrom tb_test whereid = 1;

这条 SQL 删除了 id=1 的这条数据,这时 tuple2 变成 [t_xmin:11, t_xmax:12, t_cid:0, t_ctid: (0,2)]

  • t_xmin:11,插入个 tuple 的事务 xid=11。
  • t_xmax:12,这行数据被 xid=12 这个事务删除。
  • t_cid:0,事务 id 等于 xid=12 这个事务的第 1 个 SQL 命令。
  • t_ctid:(0,2),指向自身。

整个过程如下图:

Image

2.3 可见性

当隔离级别是 Read committed 时,判断 tuple 对一个事务 x 是否可见,需要满足下面条件:

  1. 创建这个 tuple 的事务在事务 x 中查询语句执行之前已经提交;
  2. 删除这个 tuple 的事务在事务 x 中查询语句执行之前未提交或者已经回滚;
  3. 事务 x 创建的 tuple,未提交也能看到。

当隔离级别是 Repeatable Read 时,可见性需要满足创建这个 tuple 的事务在事务 x 开始之前已经提交。事务 x 开始后,即使这个 tuple 被其他事务修改或删除,这些变化对事务 x 也是不可见的。

3.总结

PostgreSQL 基于数据库表存储历史版本的方式实现了 MVCC,这种实现方式的优点是读取历史版本更加方便,不用像 MySQL 那样依赖额外的 Undo Log 存储。缺点是在高并发的场景下,数据库表会保存大量的过期 tuple(dead tuples),可能会导致表膨胀。

PostgreSQL 使用 VACUUM 机制清理过期的 tuple 数据,步骤包括识别过期 tuple、清理过期 tuple、标记过期 tuple 空间可重用、更新系统统计信息。

精品专栏 70 篇,推荐阅读。
又老性能又差,为什么好多公司依然选择 RabbitMQ?

45 个知识点,带你入门消息队列!

引入了 Disruptor 后,系统性能大幅提升!

 从 MySQL 迁移到 GoldenDB,上来就踩了一个坑。

面试官:MySQL Redo Log 和 Undo Log 有什么区别?分别用在什么场景?

感谢阅读,如果对你有帮助,请点赞和在看。欢迎加我微信:zhujinjun86。

号内回复 seata,下载《阿里分布式中间件Seata从入门到精通》

号内回复 beijing,下载我总结的北京上百家知名科技公司

号内回复 aqs,下载《40张图精通Java AQS》