PostgreSQL 的多版本并发控制(MVCC:Multi-version Concurrency Control)

从用户使用角度来看,PostgreSQL的事务并发特性,应具备如下性质:

  1.      每一个事务在开始时,记录(snapshot)当时数据库的Version,事务一旦开始运行后,其他事务做了什么操作应该不影响本事务;
  2. 读永不堵塞写,写永不堵塞读——这句话老是在Oracle的文档中看到;
  3. 写操作与写操作之间,只有当修改了相同数据行时,才会出现互相堵塞——行级锁。

下面举一个最典型的并行Update相同数据行的例子:

Transaction A 执行操作:

UPDATE foo SET x = x+1 WHERE rowed = 42

在Transaction A提交或回滚之前,Transaction B执行操作:

UPDATE foo SET x = x+1 WHERE rowed = 42

很显然:

  • B需要等待A提交或者回滚
  • 如果A回滚事务,那么B继续执行,x的值也毫无争议
  • 但是如果A事务提交,那B事务会发生什么?
    • x的值仍然使用原来的值,会出现update语句执行两次,但是x的值只增加了1,这显然不是我们预期的结果    
    • 但是如果B事务使用A事务提交后的x的新值,那么B事务就使用了其开始之后才提交的数据。这和Transaction Isolation原则冲突……

回答上面的问题,我们来了解PostgreSQL的transaction isolation level实现:

PostgreSQL提供两种isolation level(这和Oracle一致):

  •   Read committed level: 针对前述问题,B事务将使用A事务提交后的新数据(A事务提交后的数据行,必须仍然满足B事务的where条件限制)
  •   Serializable level: B事务将会中断,并返回“not serializable”错误,客户端应用程序,相应的,需要重做整个B事务。

所以说,Serializable Level带来逻辑上的清晰、干净,但是更加容易因为“not serializable”错误,要求客户端程序能够处理此错误; 所以默认情况下,我们的PostgreSQL数据库运行于“Read Committed” Level下

不管是哪种Isolation Level,Select语句只会看到其开始执行前提交(Committed)的数据,update、delete操作才存在前述问题。

那么,这种MVCC是如何实现的呢?

这就涉及 “tuple visibility”这一基本概念,使用这一概念来描述: 什么事务能够看到表数据行的什么版本……

概念:a tuple is a specific stored object in a table, representing one version of some logical table row. A row may exist in multiple versions simultaneously.

tuple 是PostgreSQL中理解、描述MVCC很重要的概念。

Non-Overwriting storage management

我们为了支持MVCC,每一个数据行,可能都需要保存多个version——tuple。tuple只有在已经committed,并且没有Active Transaction需要访问他的时候,才可以移除。

所以,对于PostgreSQL来说,新修改的数据行会被添加到表中,旧版本的tuple在稍后会被删除。

当前,long-deaded tuple使用vacuum维护命令来定期删除。开发者们也正在寻找新的途径,实现on-the-fly清理dead tuples,以减少对于vacuum的依赖。

Tuple的细节:

每一个tuple的header中,包含三个ID:

  •      xmin: 数据行被创建(inserting)时的transaction ID
  • xmax: replacing/deleting transaction ID, 开始的时候为NULL
  • forward Link:指向tuple对应的logical 数据行的新版本tuple

所以,最简单的描述就是:如果xmin 有效,xmax无效,这个tuple对于transaction来说就是可见的。“有效”指已提交、或者当前事务。

如果,我们update一行数据(不是delete),我们首先会产生对应数据行的新version tuple,然后设置该数据行老的version tuple的xmax和forward link。forward link会被其他并发的update操作浸出使用…… 嘛意思??

“Snapshots” filter away active transactions

在事务开始执行的一开始,我们会去获取所有其他backends(相当于Oracle 的server process)当前正在运行的transaction,记住他们的transaction ID(这一信息会从共享内存中获取,成本相对较低)。

为什么要这样呢? 我们来举例说明:

首先,假设有一个long transaction,很久之前就开始执行了,目前还没有执行完…… 因为他启动的早,他的transaction ID是10.

现在我们开始了一个新的事务,他的transaction id是999999999——很大的数字,新transaction执行的过程中,我们的transaction id为10的事务终于提交了(Committed),这时我们的新Transaction也需要访问long transaction中修改过的tuple…… 这个tuple transaction ID很小(小于自己),是已提交的。所以如果我们的事务如果不知道情况,一定认为这一数据是可以访问的(tuple visibility);只有在事务开始执行之初,获得所有正在running的transaction id,才能避免这种情况。

必须保证,事务开始后才提交(Commited)数据,对本事务是不可见的。

Leave Comment