关于Greenplum数据库中的并发控制

关于Greenplum数据库中的并发控制

Greenplum数据库使用了PostgreSQL的多版本并发控制(MVCC)模型来管理对于堆表的并发事务。

数据库管理系统中的并发控制允许在确保数据库的完整性的前提下,并发查询能够完成并且得到正确的结果。 传统的数据库使用两阶段锁协议来阻止一个事务修改已经被另一个并发事务读取的数据并且阻止任何并发事务读取或者写入另一个事务已经更新的数据。 协调事务所需的锁增加了数据库中的竞争,降低了总体事务吞吐量。

Greenplum数据库使用PostgreSQL的多版本并发控制(MVCC)模型来管理堆表的并发。 通过MVCC,每一个查询都在它开始时的一个数据库快照上操作。 在执行时,一个查询不能看到其他并发事务所作出的更改。 这确保了一个查询看到的是数据库的一个一致的视图。 读取行的查询不会被写入行的事务所阻塞。 反过来,写入行的查询也不会被读取行的事务所阻塞。 这使得Greenplum可以达到比使用锁来协调读写事务的传统数据库系统更高的并发度。

Note: 追加优化表使用一种不同于MVCC的并发控制模型管理。它们是为了“一次写,多次读”的应用而设计,这些应用从不或者很少会进行行级更新。

快照

MVCC模型依赖于系统能够管理数据行的多个版本的能力。一个查询其实是在该查询开始时的数据库快照上操作。 快照就是在一个语句或者事务开始时可见的行的集合。快照保证查询在其执行期间看到的是数据库的一个一致且合法的视图。

每一个事务都会被分配一个唯一的事务ID(XID),它是一个增量式的32位值。 当一个新事务开始时,它被分配下一个XID。没有被包裹在一个事务中的一个SQL语句会被当做一个单语句事务,即会给它隐式地加上BEGIN和COMMIT。 这和一些数据库系统中的自动提交概念类似。

Note: Greenplum数据库只为涉及DDL或者DML操作的事务分配XID值,它们通常是唯一需要XID的事务。

当一个事务插入一行时,其XID会被保存在该行的 xmin系统列中。 当一个事务删除一行时,其XID会被保存在xmax系统列中。 更新一行被视为一次删除加上一次插入,因此XID会被保存在当前行的xmax中以及新插入行的xmin中。 xmin和 xmax列再加上事务完成状态就指定了一个事务的范围,行的这个版本对于其中的事务可见。 一个事务可以看到所有小于xmin的事务的效果,这些事务确保已经被提交,但它无法看到任何大于等于xmax的事务的效果。

多语句事务还必须记录一个事务中哪个命令插入了一行(cmin)或者删除了一行(cmax),这样事务能够看到事务中先前的命令所作的更改。 命令序列只在事务期间有意义,因此在一个事务开始时该序列被重置为0。

XID是数据库的一个性质。每一个Segment数据库都有其自己的XID序列,因此不能拿它和其他Segment数据库的XID进行比较。 Master会使用一个集群范围的会话ID号来与Segment协调分布式事务,会话ID号被称为gp_session_id。 Segment会会维护一个分布式事务ID到其本地XID的映射。 Master用两阶段提交协议在所有Segment之间协调分布式事务。 如果一个事务在任一一个Segment上失败,它将会在所有Segment上回滚。

用户可以用一个SELECT语句查看任意行的xmin、xmax、cmin和 cmax列:
SELECT xmin, xmax, cmin, cmax, * FROM tablename;

因为用户是在Master上运行该SELECT命令,看到的XID都是分布式事务ID。如果用户能在一个Segment数据库上执行该命令,xmin和xmax值将是该Segment的本地XID。

Note: Greenplum数据库会将复制表的每行分发到所有节点上,所以每一行在所有节点上都是重复的。 每个segment节点维护自己的xminxmaxcmincmax,和gp_segment_idctid系统列一样。 Greenplum数据库不允许用户访问复制表的这些列,因为他们在查询中没有单一、明确的值。

事务ID

MVCC模型使用事务ID(XID)来判断哪些行在一个查询或者事务开始时是可见的。 XID是一个32位值,因此在该值溢出并且回卷到零之前,一个数据库理论上可以执行超过四十亿个事务。 不过,Greenplum数据库使用模 232的计算方式来使用XID,这允许事务ID回卷,就像时钟会在十二点回卷一样。 对于任何给定的XID,有大约二十亿个过去的XID和二十亿个未来的XID。 直到一行的一个版本存在大约二十亿个事务之前,这一套机制都有效,当这种情况发生时那个版本就会突然变成一个新行。 为了阻止这种情况的发生,Greenplum有一个被称为 FrozenXID的特殊XID,当把它和任何其他常规XID比较时它都是较老的哪一个。 如果行中的xmin位于那二十亿个事务之中,就必须被替换为FrozenXID,这也是VACUUM命令执行的功能之一。

至少在每二十亿个事务时清理数据库可以阻止XID回卷。Greenplum数据库会监控事务ID并且在需要一次VACUUM操作时做出告警。

当不再可用的事务ID达到可观的比例并且事务ID回卷还没发生时,将会发出一个警告:
WARNING: database "database_name" must be vacuumed within number_of_transactions transactions

当该警告被发出时,就需要一次VACUUM操作。如果没有执行所需的VACUUM操作,当Greenplum数据库达到事务ID回卷发生之前的一个限制点时,它将会停止创建新事务来避免可能的数据丢失并且发出这样的错误:

FATAL: database is not accepting commands to avoid wraparound data loss in database "database_name"

关于从这种错误恢复的过程请见从一次事务ID限制错误中恢复

服务器配置参数xid_warn_limit和 xid_stop_limit控制何时显示这些警告和错误。 xid_warn_limit参数指定在xid_stop_limit之前多少个事务ID时发出警告。 xid_stop_limit参数指定在回卷发生之前多少个事务ID时发出错误并且不允许创建新的事务。

事务隔离模式

SQL标准描述了数据库事务并发运行时可能发生的三种现象:
  • 脏读 – 一个事务可能读到来自另一个并发事务的未提交数据。
  • 不可重复读 – 在一个事务中两次读取同一行得到不同的结果,因为另一个并发事务在这个事务开始后提交了更改。
  • 幻读 – 在同一个事务中两次执行同一个查询可能返回不同的行集合,因为另一个并发事务增加了行。

SQL标准定义了数据库系统需要支持的四种事务隔离模式,以及在事务并发执行时可以出现的现象。

Table 1. SQL事务隔离模式
级别 脏读 不可重复读 幻读
未提交读 可能 可能 可能
已提交读 不可能 可能 可能
可重复读 不可能 不可能 可能
可串行化 不可能 不可能 不可能

Greenplum数据库中未提交读和已提交读行为与SQL标准的已提交读一致。 Greenplum数据库中可串行化与可重复读隔离级别除了避免了幻读外,行为与SQL标准的已提交读一样。

已提交读和可重复读的区别是:在已提交读模式下,每个语句可以看到该语句执行前已提交的行; 在可重复读模式下,语句只能看到该事务启动前提交的行。

在已提交读模式下,如果另一个并发执行的事务修改了行的值并提交,该行的值读两遍结果可能不同。 已提交读允许幻读,一个查询执行两次拿到的结果集可能不同。

可重复读模式避免了非可重复读和幻读,虽然后者并不是标准中所必须的。 一个尝试着修改其他并发事务修改过的数据的事务将被回滚。 在可重复读隔离级别下执行事务的应用必须做好处理因为可串行化错误失败的事务。 如果可重复读对于应用并不是必须的,建议使用已提交读模式。

可串行化模式,Greenplum数据库并不完全支持,可以保证一组事务在并行执行时得到的结果与串行执行的结果相同。 在Greenplum数据库里指定可串行化的方式会退化到可重复读模式。 MVCC 快照隔离(SI)模式在没有昂贵的锁开销的前提下避免了脏读、不可重复读和幻读, 但是仍然会存在在Greenplum数据库可串行化模式下的事务,无法做到真正的串行化。 这些异常通常归咎于Greenplum数据库没有执行谓词锁定,即一个事务里的写会影响另一个并行事务里之前读的结果。

Note: PostgreSQL 9.1 可串行化隔离级别引入了一种新的可串行化快照隔离(SSI)模式,可以与SQL标准定义的可串行化隔离级别完全兼容。 这个模式在Greenplum数据库中不可用。 SSI监视可能导致序列化异常的条件的并发事务。 当发现了潜在的串行化错误,一个事务被提交,其余的被回滚,并且必须重试。

Greenplum数据库并行运行的事务需要检查并识别可能并行更新相同数据的交互。 通过使用显式表锁或要求冲突事务更新以表示冲突的虚拟行,可以防止发现的问题。

SQL语句SET TRANSACTION ISOLATION LEVEL可以设置当前事务的隔离级别。 必须要在执行 SELECT, INSERT, DELETE, UPDATE, or COPY 语句前设置:
BEGIN;
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
...
COMMIT;
隔离模式也可以在BEGIN语句里指定:
BEGIN TRANSACTION ISOLATION LEVEL REPEATABLE READ;

可以通过设置default_transaction_isolation配置项来改变会话的默认隔离级别。

从表中移除过期的行

更新或者删除一行会在表中留下该行的一个过期版本。 当一个过期的行不在被任何活跃事务引用时,它可以被移除从而腾出其所占用的空间进行重用。 VACUUM命令会标记过期行所使用的空间可以被重用。

当表中的过期行累积后,为了容纳新的行就必须扩展磁盘文件。 这样执行查询所需的磁盘I/O就会增加,从而性能受到影响。 这种情况被称为膨胀,并且应该通过定期清理表来解决。

VACUUM命令(不带FULL)可以与其他查询并行运行。 它会标记之前被过期行所占用的空间为空闲可用。如果剩余的空闲空间数量可观,它会把该页面加到该表的空闲空间映射中。 当Greenplum数据库之后需要空间分配给新行时,它首先会参考该表的空闲空间映射以寻找有可用空间的页面。 如果没有找到这样的页面,它会为该文件追加新的页面。

VACUUM(不带FULL)不会合并页面或者减小表在磁盘上的尺寸。 它回收的空间只是放在空闲空间映射中表示可用。为了阻止磁盘文件大小增长,重要的是足够频繁地运行VACUUM。 运行VACUUM的频率取决于表中更新和删除(插入只会增加新行)的频率。 重度更新的表可能每天需要运行几次VACUUM来确保通过空闲空间映射能找到可用的空闲空间。 在运行了一个更新或者删除大量行的事务之后运行VACUUM也非常重要。

VACUUM FULL命令会把表重写为没有过期行,并且将表减小到其最小尺寸。 表中的每一页都会被检查,其中的可见行被移动到前面还没有完全填满的页面中。 空页面会被丢弃。该表会被一直锁住直到VACUUM FULL完成。 相对于常规的VACUUM命令来说,它是一种非常昂贵的操作,可以用定期的清理来避免或者推迟这种操作。 最好是在一个维护期来运行VACUUM FULL。VACUUM FULL的一种替代方案是用一个CREATE TABLE AS语句重新创建该表并且删除掉旧表。

用户可以运行VACUUM VERBOSE tablename来得到一份Segment上已移除的死亡行数量、受影响页面数以及有可用空闲空间页面数的报告。

查询pg_class系统表可以找出一个表在所有Segment上使用了多少页面。注意首先对该表执行ANALYZE确保得到的是准确的数据。
SELECT relname, relpages, reltuples FROM pg_class WHERE relname='tablename';

另一个有用的工具是gp_toolkit方案中的gp_bloat_diag视图,它通过比较一个表使用的实际页数和预期的页数来确定表膨胀。 更多有关gp_bloat_diag的内容请见Greenplum数据库参考指南中的“gp_toolkit管理方案”。