计算机应用研究
APPLICATION RESERCH OF COMPUTERS
2000  Vol.17　No.1　P.78-80，90




PostgreSQL数据库XA协议的设计和实现
郭歌　贾焰
摘 要 XA协议是分布事务处理中子事务与全局事务间通讯的接口规范。PostgreSQL是一种集中式的数据库管理系统，尚未实现对XA协议的支持。在对数据库事务管理和存储管理分析的基础上，提出了PostgreSQL上XA协议的实现方法，并对事务预交后的恢复提出一种独特的解决方案。
关键词 事务 两阶段提交 预交 存储管理 恢复 日志
1 前言
　　在数据库中，分布事务的完成有赖于各个分布节点上的子事务。与集中式事务一样，分布事务也具有原子性、一致性、隔离性、持久性四种特性。
　　为了保证分布事务的原子性和一致性，分布事务的提交通常是两阶段提交(2PC)。在第一阶段，全局事务管理器要求所有的子事务进入预交阶段，进行投票查询。若子事务准备好了并愿意提交，则如果子事务是一个只读事务，就返回“只读”票并先行提交，否则投“提交”票。若子事务不能提交，就投“失败”票，然后先行失败。在第二阶段，全局事务管理器分析接收到的消息，若超时或收到一个“失败”票，则决定失败整个分布事务，并向所有投“提交”票的子事务发失败命令，否则向它们发提交命令。其中第一阶段就叫子事务的预交阶段。
　　XA协议是X/Open组织提出的一种资源管理器(通常指处理子事务的DBMS)在2PC过程中与全局分布事务管理器间通讯的接口协议规范。在863项目“分布式事务管理”中，要求全局事务下的各个DBMS能够支持XA协议以实现对全局事务的统一管理。当前的商业数据库Oracle，Sybase都支持XA协议。
　　PostgreSQL是一种面向对象并支持大对象管理的数据库，它强大的功能和代码开放的特点使得它在全球有着广泛的应用范围，但原有DBMS未能支持XA协议。本文在对数据库的有关管理机制的源码进行分析后，提出XA协议实现的解决方案。实现中的主要问题是事务预交后的崩溃恢复。在这之前，我们首先看看数据库的事务管理和存储管理。
2 PostgreSQL的事务管理和存储管理分析
　　数据库服务器处理多个事务过程中发生如电源掉电的故障时，正在执行的事务就会半途而废，这种故障称为“系统崩溃”，从系统崩溃故障中恢复称为“崩溃恢复”。崩溃恢复为了保证事务的原子性与一致性，需要进行Redo或Undo操作使得已提交的事务数据修改确实写入库中而未完成事务不能对库的数据有任何影响。如何恢复有赖于系统何时将数据写回数据库中的策略，也与数据库的事务管理和存储管理的具体实现相关。
　　PostgreSQL最初的一个设计目标是希望能够降低数据库崩溃恢复的代码数量。对于DBMS来讲，大量的崩溃恢复代码不仅不易于书写，而且对于不同的情况会出现许多分支，这也不易于测试和调试。同时PostgreSQL本身又支持允许用户自定义访问方法，所以PostgreSQL要求崩溃恢复的模型尽可能地简单和易于扩展。基于此PostgreSQL提出了一种解决方法：使用非重写的存储管理技术，并将日志作为一个系统关系文件来进行管理。
2.1 PostgreSQL的事务管理
　　PostgreSQL中每个事务都有唯一的事务标识(transaction identifier)XID。数据库日志中记录的是表明事务状态(“提交”，“失败”或“事务处理过程中”)的信息(事务缺省的状态值是“事务处理过程中”)，而不是事务的操作序列。每个事务在日志中的位置可通过事务XID计算得到。当事务提交时，就将日志中该事务状态的值修改为“提交”，并将日志所在的块写回静态磁盘中，同时该事务所作的任何数据页面修改也写回磁盘。而当事务失败时，则修改事务在日志中的状态为“失败”，将日志所在的块写回静态磁盘中(这个时候事务所作的数据页面修改不需写回磁盘，但由于这些页面可能包含别的已提交事务的数据，因此有可能已被别的事务写回磁盘。在这种情况下，系统需要能够识别某些数据的不合法性)。此外，在事务的处理过程中，事务的状态是由一个事务结构变量说明的。不同的事务操作(开始，结束，提交，失败)必须根据不同的事务状态作出相应的处理和并对事务状态进行转换。事务运行出现异常时，也会影响事务状态值。
2.2 PostgreSQL的存储管理
　　数据库系统的存储管理通常采用的是先写日志(WAL)的方法，即在真正将数据写入磁盘之前，先将该事务的操作或被修改的数据的前后像写入日志，系统崩溃后依据日志中的内容进行Redo或Undo操作。而PostgreSQL的存储管理采用非重写(no-overwrite)技术。采用这种技术，当数据发生修改(如update语句)时，旧的数据仍然会保留在数据库中。体现在PostgreSQL的数据库元组组织方式上表现为：
　　PostgreSQL中的关系和数据文件是一一对应的。每一个数据文件由许多块组成，块之间由前后指针连接成一条双向链，如图1，PostgreSQL的数据块由数据头(line table)，数据体和一个指针域组成。数据头是对该块中所有元组的索引。当插入一个元组时(insert语句)时，在数据块中插入一个锚点，以后在修改该元组时，并不修改原有的元组，只是将修改值插入到数据块中，并建立指向该修改的指针。

图1
　　使用非重写技术有两个好处。首先，失败事务是即时的，不必根据日志取消事务对数据的修改(Undo)，因为修改前的记录仍然留在数据库中。其次，使用非重写技术的另一个优点是可进行时间传播(time travel)。一个用户可以对历史数据(即已被新数据代替的或被删除的旧数据)进行查询。
　　同一元组的新旧数据通过链表连接起来，但这并不表示链表末端的数据一定是最新的，因为一个最近失败的事务插入值也会在这个链表内，而且可能就在末端。那么如何判断数据是否满足要求呢？
　　PostgreSQL中每个元组的数据不论新旧都有一个表明该数据相关事务信息的结构，结构中包括创建该数据的事务标识xmin，删除该数据的事务标识xmax和表明这两个事务状态的信息info以及其它的信息(我们暂不考虑历史数据的操作)。当发生如update的数据修改操作时，系统分成两步来完成，首先“删除”原有数据(相当于delete语句)，然后插入新的数据(相当于insert语句)。即修改原数据的xmax为当前事务XID，并创建新数据和初始化相应结构(置xmin为当前事务XID和xmax为空)。在以后发生例如select语句的检索数据操作时，对待合符查询条件的元组的所有数据，系统首先对数据的info进行判断，如果能够判断数据的xmin事务已提交并且xmax事务失败或不存在，就能表明数据是合法的。如果能判断xmax事务已提交了，说明这个数据是旧数据。如果不能确定数据是否合法，系统就会到日志中查找xmin与xmax的状态来进行判断，并把得到的相关信息记录到info中。对待历史数据的检索，还需使用到结构中与xmin和xmax有关的时间参数来判断，这里省略不提。当然系统也提供了清除一定范围历史数据的命令。
2.3 PostgreSQL的恢复问题
　　我们只考虑数据库的崩溃恢复。假设网络是完全可靠的，静态存储器也是完全可靠的。
　　数据库在事务提交时会把记录事务状态日志的内容和修改的数据页面写回静态磁盘，我们只考虑崩溃时未完成的事务，崩溃时如果它的数据修改未写入磁盘，不存在什么恢复。如果部分已写入了磁盘，也并不需像别的数据库作undo操作，因为例如select的检索语句发现这个值的xmax事务在日志中的状态是“处理过程中”，就认为它不合法的。这说明PostgreSQL基本上不存在崩溃恢复问题，这也达到的数据库初始的设计目的。
3 PostgreSQL中分布子事务的设计和实现
　　在XA协议中，DBMS作为资源管理器在事务处理的过程中向全局分布事务管理器至少提供三种函数接口：Prepare()，Commit()和Rollback()。全局分布事务管理器调用这三个函数来取得子事务的投票查询结果，提交和失败子事务。
　　我们已经知道PostgreSQL数据库本身基本上没有什么崩溃恢复，未完成的事务在对数据库的影响方面系统认为等价于失败的事务。但是在两阶段提交过程中，如果子事务投了“提交”票后，在等待事务管理器的提交或失败命令时，发生了系统崩溃。系统重启后必须能够恢复这个子事务并且能对事务管理器给这个子事务发出的“提交”或“失败”命令作出正确反应，而不是简单地认为是“失败”了。这是分布事务的特点决定的。这种情况下的恢复，我们称之为预交后的崩溃恢复。如何保证预交后的崩溃恢复是XA协议实现中的主要问题。
　　预交后的崩溃恢复有两种方法，一种是像WAL一样记录事务的操作序列，系统重启时重做已预交事务的这些操作。另外一方面由于PostgreSQL使用了非重写的存储管理技术，保留在数据库中的数据是否合法依赖于它的xmin事务和xmax事务的状态。一个未提交的事务完全可以把数据修改保留在数据库中而不会引起冲突。所以我们提出另外一种方法即在子事务投峤票前把数据修改写回数据库中，而真正提交时就不必再做这个操作了。这时如果系统崩溃，系统重启后这些数据仍然会在数据库中，我们只要在事务管理器的命令到达之前不让别的事务修改(以免改变数据的xmax)和检索(以免改变数据的info)这些数据即可(给这些数据上写锁)。然后不论是提交还是失败命令到达时，我们只需修改日志中该待恢复事务状态和释放这些锁就可真正完成这个待恢复事务。这就需要预交事务在投“提交”票前记录该事务保持的一些写锁信息。
　　比较两种方法，在投票前记录事务的锁比记录事务的数据或动作序列更容易。这是因为一方面，数据库也可以处理集中式事务，而我们要求记录的只是分布子事务的信息。如果在每个事务动作后做记录，此时我们并不能判断这个事务是否是分布子事务(在具体实现中，我们直到prepare语句时才能判断)。而事实上每个事务却始终维持着一个锁队列直到事务结束前才释放这些锁(数据库的锁机制是两阶段锁)，锁信息在执行prepare语句时容易得到。此外由于我们只记录关系级写锁(PostgreSQL的锁粒度是关系)，因而日志内容也不大。另一方面使用记录锁的方法，系统重启后我们只恢复了写锁，而如果重做事务操作还会对某些数据上一些读锁，因而影响数据库的并发度。另外，决定投“只读”票也可在记录写锁的基础上进行判断。所以我们采纳后一种方法即在预提交时就将事务的修改写入磁盘中并且记录事务的锁(主要是写锁)。而在当事务真正提交和失败时，只需改变事务在日志中的状态，并删除已记录的该事务的锁。这是一种以空间换取机制简单化的策略。
　　具体实现如下。
3.1 Prepare()，Commit()，Rollback()的实现
　　。Prepare()：
　　根据事务状态结构变量，判断事务的状态；
　　if 事务不可以提交 then 先行失败事务，返回“失败”票；
　　for 事务保持的每个锁 如果是写锁，则记录到锁日志中；
　　if 没有写锁 then 先行提交事务，返回“只读”票；
　　修改事务状态进入预提交；
　　将数据写回数据库中并返回“提交”票。
　　。Commit()：
　　If 事务已是预提交状态 删除锁日志中相关记录；
　　else 将数据写回数据库中；(这里是处理无需预交的集中式事务)
　　修改日志中事务状态为“提交”；
　　释放资源。
　　。Rollback()：
　　If 事务已是预提交状态 then 删除锁日志中相关记录；
　　修改日志中事务状态为“失败”；
　　释放资源。
3.2 2PC过程中失败恢复的实现
　　正如前面所讲，我们在原有的基础上的改进主要是记录了子事务所保持的写锁，目的在于崩溃恢复中使用。
　　PostgreSQL是一个集中式的数据库，它是由一个叫做postmaster的后台监控程序在接收到前台的连接请求后派生出一个子进程postgres，并由postgres与前台连接并处理前台请求。所有的postgres子进程共享postmaster所管理的锁信息。
　　由于事务的数据信息已写入数据库中，系统重启后，为避免在提交或失败命令到达之前数据信息结构被修改，要求这些数据要被锁住直至事务管理器发给该事务的命令得到执行。所以postmaster启动后，在接收任何前台连接请求之前它应该恢复那些失败前已预提交的事务的锁。这可由postmaster先派生出子进程用于这些锁的恢复，然后该恢复进程等待。当事务管理器的命令到达后，与输入该命令的前台对应的后台postgres就传递信息给恢复进程，并由恢复进程对恢复事务进行真正的结束即修改日志中事务的状态，并释放和清除日志中记录的该事务相关锁。
　　Crash_Recovery()和Complete_Transaction的实现如下。Crash_Recovery()是崩溃恢复后系统重启时的工作，Complete_Transaction是恢复进程接收到命令后的工作。
　　。Crash_Recovery()：
　　如果锁日志非空
　　派生出一个进程用于恢复记录于锁日志中各个预提交事务的锁，同时该进程处于等待状态随时接收结束这些事务的命令。处理完这些命令后如果进程发现已释放了本进程所有保持的锁，就结束这个进程。
　　。Complete_Transaction(TransactionId，Tran_ Status)：
　　(两个参数代表事务标识和要求事务是提交还是失败。)
　　将相关事务的状态记录到日志中。
　　释放相关事务的锁。
　　删除相关事务在锁日志中的记录。
3.3 锁日志的组织方式
　　锁日志作为一种频繁读写，进行插入删除操作的文件，其组织方式上有两种方法。其一，将其设计为一种系统关系文件(PostgreSQL的日志文件以及其它的系统文件都是按关系来组织的)，锁日志记录的写入与删除对应关系元组的插入与删除，这可以很好地与原系统结合。问题是关系的插入删除是较高级别的读写操作，频繁插入删除速度较慢，不利于性能的提高，而且也不容易调试。另一种方法是将其作为一种普通文件。文件可包含固定数目的记录，每个事务记录锁的位置根据事务XID的hash函数计算得到，这样使得有效记录平均分配到文件中。作为一个随机文件，数据的读写比关系的读写快，而且也易于实现。
　　由于性能以及实现上的原因，我们选择了后者。每个锁记录的结构包括以下的内容：事务标识、关系标识数组、数组有效数和一个特征标志位以及记录有效标志位。它们的作用分别是：事务标识用于区分待恢复的事务，也可定位记录在锁日志中的位置。关系标识数组是一个用于存储该事务被写锁的关系标识的定长数组，数组前面多少个元素是有效的可由数组有效数来说明。如果这个事务涉及的写锁关系过多，一个关系标识数组不够，可能需要另外的一个记录，这就需要一个特征标志位来判断(一般来讲，一个事务对数据的读可能涉及到很多关系，而对写就稍少。所以，一个事务一个记录一般就足够了)，表明该事务在日志中还有其它记录存在。记录有效标志位表明该记录在日志文件中是否有效，删除该记录时只需给该标志位置位。
4 小结
　　PostgreSQL的非重写技术抛弃了日志的前像和后像的传统模式，在技术上简化了事务管理，但给XA协议的实现带来了难度。在对XA协议的实现中，一方面我们必须参考普通日志的设计和使用，另一方面，也需充分使用PostgreSQL的存储管理达到很好地与原系统的合成。
本课题获863植际绞挛窆芾项目资助
郭歌（国防科技大学计算机学院 长沙 410073）　
贾焰（国防科技大学计算机学院 长沙 410073）
参考文献
1，Stonebraker M and Rowe L. A. THE DESIGN OF POSTGRES. Proc 1986ACM-SIGMOD Conference on Management of Data, Washington D. C, May 1986: (18～20)
收稿日期：1999年8月16日
