关于Greenplum数据库中的并发控制
Greenplum数据库使用PostgreSQL多版本并发控制(MVCC)模型来管理堆表的并发事务。
数据库管理系统中的并发控制,在确保数据库完整性的同时,允许并发查询并得到正确的结果。传统数据库使用两阶段锁定协议,该协议可阻止事务修改另一个并发事务已读取的数据,也可阻止事务读取或写入另一个事务已更新的数据。协调事务所需的锁增加了对数据库的争用,从而降低了总体的事务吞吐量。
Greenplum数据库使用PostgreSQL多版本并发控制(MVCC)模型来管理堆表的并发。通过MVCC,每一个查询都在它开始时的一个数据库快照上运行。执行查询时,查询无法看到其他并发事务所做的更改。这样可以确保查询所看到数据库的视图前后一致。读取行的查询绝不会阻止等待写入行的事务。相反,写行的查询也不会被读行的事务阻止。与使用锁来协调读写事务之间的访问的传统数据库系统相比,这允许更高的并发度。
注意:AO表使用别的并发控制模型进行管理,而非节主题所讨论的MVCC模型。它们是为了“一次写,多次读”的应用而设计,这些应用从不或者很少会进行行级更新。
快照
MVCC模型取决于系统管理数据行的多个版本的能力。查询是在其开始时的数据库快照上进行操作。快照是在语句或事务开始时可见行的集合。快照可确保查询在执行期间具有一致且有效的数据库视图。
每个事务都会被分配一个唯一的事务ID(XID),它是一个递增的32位值。新事务开始时,会被分配下一个XID。未包裹在事务中的SQL语句被视为单语句事务——即给它隐式地加上BEGIN和COMMIT。这类似于某些数据库系统中的自动提交。
注意: Greenplum数据库仅将XID值分配给涉及DDL或DML操作的事务,这些事务通常是唯一需要XID的事务。
当一个事务插入一行时,其XID被保存在该行的xmin系统列中。当一个事务删除一行时,XID将保存在xmax系统列中。更新一行被视为一次删除和一次插入,因此XID被保存到当前行的xmax中以及新插入行的xmin中。xmin和 xmax列,再加上事务的完成状态,就指定了事务的可见行版本的一个范围。一个事务可以看到所有小于xmin的事务的影响行(这些事务保证提交),但它无法看到任何大于等于xmax的事务的影响行。
多语句事务还必须记录事务中哪个命令插入了一行(cmin)或删除一行(cmax),以便该事务可以查看其中在前面的命令所做的更改。命令的顺序仅在事务期间有意义,因此该顺序在事务开始时重置为0。
XID是一个数据库的属性。每个Segment数据库都有其自己的XID序列,不能拿它与其他Segment数据库的XID进行比较。 Master使用一个集群范围的会话ID号,名为gp_session_id,来与Segment协调分布式事务。Segment维护一个分布式事务ID到其本地XID的映射。
SELECT xmin, xmax, cmin, cmax, * FROM tablename;
因为您是在Master上运行该SELECT命令,所以XID都是分布式事务ID。如果您是在一个Segment数据库上执行该命令,则xmin和xmax值将是该Segment的本地XID。
注意:Greenplum数据库将复制表的所有行分布到每个segment,因此每个segment上的每一行都是重复的。每个segment实例维护自己的系统列xmin、xmax、cmin和cmax,以及gp_segment_id和ctid。Greenplum数据库不允许用户查询访问复制表的这些列,因为它们在查询中没有单一、明确的值。
事务ID的回转
MVCC模型使用事务ID(XID)来确定哪些行在查询或事务开始时可见。XID是一个32位值,因此在该值发生溢出并回转归零之前,一个数据库理论上可以执行超过40亿次事务。不过,Greenplum数据库对XID使用模的计算方式,这使事务ID可以回转归零,就像时钟在12点回转归零一样。对于任何给定的XID,有大约二十亿个过去的XID和二十亿个未来的XID。这套机制行之有效,直到某一行的版本持续存在了大约20亿笔事务后,它突然看似为一个新行。为了防止这种情况,Greenplum有一个特殊的XID,称为FrozenXID,它始终被认为比任何常规的XID都要老。某一行的xmin必须要在20亿次事务内替换为FrozenXID,这也是VACUUM命令执行的功能之一。
每隔20亿个事务对数据库进行至少一次清理,就可以防止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标准定义了数据库系统可以支持的四个事务隔离级别,以及每个级别下并发执行事务时所允许的现象。
表1. SQL事务隔离模式
级别 | 脏读 | 不可重复读 | 幻读 |
未提交读 | 可能 | 可能 | 可能 |
已提交读 | 不可能 | 可能 | 可能 |
可重复读 | 不可能 | 不可能 | 可能 |
可串行化 | 不可能 | 不可能 | 不可能 |
Greenplum数据库中未提交读和已提交读的行为与SQL标准的已提交读的行为一致。 Greenplum数据库中可串行化与可重复读,除了也避免了幻读外,其行为与SQL标准的已提交读一样。
已提交读和可重复读的区别是,在已提交读模式下,每个语句可以看到该语句执行前提交的行; 而可重复读模式下,语句只能看到该事务启动前提交的行。
在已提交读模式下,如果另一个并发执行的事务修改了行的值并提交,则该行的值读两遍结果可能不同。 已提交读允许幻读,一个查询执行两次拿到的结果集可能不同。
可重复读模式避免了非可重复读和幻读,虽然后者并不是标准中所必须的。 一个尝试着修改其他并发事务修改过的数据的事务将被回滚。 在可重复读隔离级别下,执行事务的应用程序必须要做好准备,来处理因为可串行化错误失败的事务。 如果可重复读对于应用并不是必须的,建议使用已提交读模式。
可串行化模式,可以保证一组事务在并行执行时得到的结果与串行执行的结果相同,而Greenplum数据库并不完全支持此模式。 在Greenplum数据库里指定可串行化的方式会退化到可重复读模式。 MVCC 快照隔离(SI)模式在没有昂贵的锁开销的前提下避免了脏读、不可重复读和幻读, 但Greenplum数据库中某些可序列化事务之间可能会发生其他交互,从而使它们无法真正序列化。 这些异常通常归因于Greenplum数据库没有执行谓词锁定,即一个事务里的写会影响另一个并行事务里之前读的结果。
注意: 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 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 schema下的gp_bloat_diag视图,它通过比较一个表使用的实际页数和预期的页数来鉴别表膨胀。 更多有关gp_bloat_diag的信息,请参见Greenplum数据库参考指南中的“The gp_toolkit Administrative Schema”。