DB2 中的事务 ID

来源:互联网 发布:淘宝买家权重查询 编辑:程序博客网 时间:2024/05/19 09:42
DB2 中的事务 ID 作者:佚名 时间:2005-08-12 17:39 出处:互连网 责编:小渔               摘要:DB2 中的事务 ID从 DB2 日志记录中检索工作单元的惟一标识符

级别: 中级

Knut Stolze
DB2 WebSphere Information Integration Development, IBM Germany
2005 年 6 月 20 日

您需要识别运行在 IBM® DB2® Universal Database™ (DB2 UDB) 服务器上的当前工作单元吗?Knut Stolze 将向您展示如何完成这一任务,方法是使用存储过程和每个日志记录头中的惟一事务标识符。

简介
一般情况下,与数据库服务器交互的应用程序无需明白数据库引擎内部的事务处理。应用程序发出一系列 SQL 语句(如 INSERT、UPDATE、DELETE、SELECT 或 CREATE TABLE),在事务结束时,运行事务结束语句 COMMIT 或 ROLLBACK。COMMIT 语句结束事务并告知数据库服务器去持久存储所有更改。另一方面,ROLLBACK 使数据库服务器撤销在事务中所做的全部更改。

在这个世界上,不是只有“常规”应用程序。同时还存在着专用应用程序,这样的应用程序通常具有非常特别的需求。例如,涉及在数据库系统间相互复制数据的应用程序(如 DB2 Replication [2])需要了解事务信息。当复制的数据改变时,通常需要连同在单个事务中所做的全部更改一起复制下来。因此,需要知道在哪个事务中进行了哪些更改,并且每个事务必须被惟一标识。

DB2 UDB 没有提供特殊的注册表或任何其他直接的方法来检索事务的标识符,对于内部目的这是必需的。在以下几节中,我将向您介绍如何将 DB2 UDB 的几种特性组合起来,以生成事务标识符。总的思路是访问日志记录,日志记录是 DB2 为确保在万一系统崩溃、停电或硬盘故障时恢复宝贵数据而撰写的。DB2 将内部事务 ID 存储在每个日志记录中。执行一些附加设置步骤即可触发为检索正确的事务 ID 而编写特定的日志记录。

事务和工作单元 (UOW)
阅读 DB2 手册 [1] 时,您通常会看到术语工作单元,缩写为 UOW。在其他数据库文献中,您往往会看到术语事务。这两个术语指的是同一个概念;它们都表示在事务或工作单元中以原子、一致、隔离和持久的方式执行一组 SQL 语句。因此,在本文中,我将作为同义词使用这两个术语。

访问事务 ID
DB2 UDB 附带有很多种用于对数据库系统本身进行管理的 API。这些 API 的其中之一允许访问数据库引擎在操作过程中撰写的日志记录。这些日志记录是针对 INSERT、UPDATE、DELETE、REORGANIZE 和您发出的用于修改数据库表中数据或管理系统本身的很多其他操作而撰写的。

每个日志记录都在其头部包括一个触发记录写操作的事务的惟一标识符(以 6 个字节编码)[3]。此标识符是保证所有数据库事务的持久性、防止预料中的或意外的操作中断(如系统故障)所必需的。DB2 自动维护该标识符。

此方法利用存储在日志记录头中的事务标识符。我们使用日志头 API 访问日志记录,并从中提取事务 ID。执行此操作时,请记住:

日志头 API 的限制
使用日志头 API 意味着必须将数据库配置参数 LOGRETAIN 设置为 RECOVERY 和/或将参数 USEREXIT 设置为 ON。否则,不可能通过 API 检索日志记录。大多数生产系统已经使用这两种设置中的一种,而不再使用循环日志记录。

此外,到 db2ReadLog 的调用被当作其他 SQL 语句来处理。而 API 不是动态复合语句 中一个受支持的语句。但是,触发器(和表函数)无法执行任何任意 SQL 操作,并被限制为动态复合语句。因此,您无法将调用嵌入到触发器中已存在的过程 getTransactionId 中。

  • 日志记录必须被写入要为其确定请求标识符的当前事务中。
  • 由于数据库引擎将单个日志(可能拆分为若干个文件)用于所有并发事务,这些并发事务中的很多或全部可能都在更改数据,从而导致同时将内容写入日志记录,因此有必要找到真正属于当前事务的日志记录。

两个问题可一起解决。我们使用 DB2 的内置函数 GENERATE_UNIQUE 来生成惟一值。将该值插入表中,触发日志记录的写操作。下一步将读取所有新日志记录(自从上一次 INSERT 操作以来的记录),直到找到包含那个惟一值的日志记录为止。它就是我们要寻找的日志记录,并从中提取出事务 ID。最后,插入操作被撤销,以便该表不累积过期数据。整个处理过程如图 1 所示。

图 1. 从数据库日志提取事务标识符的逻辑
捕捉事务 ID 的过程

实现存储过程
上面介绍的整个逻辑被封装在单个存储过程中。这样,所有应用程序都有一种简单、标准化的方式来检索事务 ID。任何到 DB2 API 的直接调用都不是必需的。存储过程将事务 ID 作为字符串(即类型 CHAR(12) 的值)返回。

在编译存储过程之前,您需要创建一个名为 TA_ID_FORCE_LOGWRITE 的表。该表在过程内访问。该表本身结构非常简单,只有一个列,函数 GENERATE_UNIQUE 生成的惟一值存储在其中。使用清单 1 所示的 SQL 语句创建该表:

清单 1. 创建表 TA_ID_FORCE_LOGWRITE
            CREATE TABLE ta_id_force_logwrite (            unique_val VARCHAR(18) FOR BIT DATA NOT NULL,            CONSTRAINT taid_pk PRIMARY KEY (unique_val)            )@            

该过程以 C++ 实现,用于访问异步读取日志 API [4],它使用嵌入式 SQL 来执行必需的 SQL 操作。保存点用于回滚在过程中执行的 INSERT 操作,以触发日志记录的写操作。该过程中的 SQL 语句可以充分利用受 DB2 UDB Version 8.2 支持的 SQL 功能。

因此,我们生成惟一值,将其插入表中,并在单个语句中将它检索到存储过程代码调用。此操作在以下清单的斜体 部分中完成。如果需要在 DB2 的早先版本中使用这一过程,则必须将此逻辑拆分为多个独立的 SQL 语句。

DB2 日志是使用 API db2ReadLog 读取的。一开始,系统将调用该 API 来确定当前的日志序列号 (LSN)。完成此步骤可以避免查询调用此过程前撰写的日志记录。毕竟,我们感兴趣的只是单个日志记录:INSERT 操作所撰写的那个。

在 INSERT 操作之后,所有新的日志记录都会被检索。对于每个记录,我们都要检查它是否是针对 INSERT 操作而撰写的。如果是的,并且所插入的数据包含 INSERT 语句过程中使用的惟一值,那么我们就找到了所需的日志记录,可以返回正确的事务标识符了。

在离开该过程之前,我们需要将处理回滚到开始设置的保存点。这样,超出该过程的作用域的任何数据修改都不会保留。

该过程的完整代码如清单 2 所示。在清单 2 中,到日志 API 的调用被设置为粗体,SQL 语句则显示为斜体。其他部分仅涉及到参数的准备。

清单 2. 存储过程代码
            #include <string.h> // memset(), memcpy(), strncpy()            #include <stdio.h> // sprintf()            #include <sqludf.h>            #include <db2ApiDf.h>            #if defined(__cplusplus)            extern "C"            #endif            int SQL_API_FN getTransactionId(            SQLUDF_VARCHAR *taId,            SQLUDF_NULLIND *taId_ind,            SQLUDF_TRAIL_ARGS)            {            SQL_API_RC rc = SQL_RC_OK;            struct sqlca sqlca;            db2ReadLogInfoStruct logInfo;            db2ReadLogStruct logData;            SQLU_LSN startLsn;            SQLU_LSN endLsn;            char buffer[64 * 1024] = { '/0' }; // for log record data            EXEC SQL BEGIN DECLARE SECTION;            char uniqueVal[13] = { '/0' };            EXEC SQL END DECLARE SECTION;            // we assume NULL return            *taId_ind = -1;            /*            * Step 1: Set a savepoint to be able to undo the data modifications            */            EXEC SQL SAVEPOINT get_transaction_id ON ROLLBACK RETAIN CURSORS;            /*            * Step 2: Query the DB2 Log to get the start LSN            */            memset(&sqlca, 0x00, sizeof sqlca);            memset(&logInfo, 0x00, sizeof logInfo);            memset(&logData, 0x00, sizeof logData);            logData.iCallerAction = DB2READLOG_QUERY;            logData.piStartLSN = NULL;            logData.piEndLSN = NULL;            logData.poLogBuffer = NULL;            logData.iLogBufferSize = 0;            logData.iFilterOption = DB2READLOG_FILTER_OFF;            logData.poReadLogInfo = &logInfo;            rc = db2ReadLog(db2Version810, &logData, &sqlca);            if (rc < 0) {            memcpy(SQLUDF_STATE, "38TA0", SQLUDF_SQLSTATE_LEN);            strncpy(SQLUDF_MSGTX, "Could not query log for last LSN",            SQLUDF_MSGTEXT_LEN);            goto exit;            }            else if (sqlca.sqlcode) {            memcpy(SQLUDF_STATE, "38TA1", SQLUDF_SQLSTATE_LEN);            snprintf(SQLUDF_MSGTX, SQLUDF_MSGTEXT_LEN, "SQL error while "            "reading log records.  SQLCODE = %d, SQLSTATE=%s",            sqlca.sqlcode, sqlca.sqlstate);            goto exit;            }            memcpy(&startLsn, &logInfo.nextStartLSN, sizeof startLsn);            /*            * Step 3: Force a log record to be written            *            * Insert a unique value into our table, which triggers a log record to be            * written.  The same value is also returned right away so that we can use            * it to search through the new log records.            */            EXEC SQL            SELECT value            INTO   :uniqueVal            FROM   NEW TABLE ( INSERT            INTO   ta_id_force_logwrite            VALUES ( GENERATE_UNIQUE() ) ) AS t(value);            if (sqlca.sqlcode) {            memcpy(SQLUDF_STATE, "38TA2", SQLUDF_SQLSTATE_LEN);            snprintf(SQLUDF_MSGTX, SQLUDF_MSGTEXT_LEN, "SQL error while "            "triggering log record.  SQLCODE = %d, SQLSTATE=%s",            sqlca.sqlcode, sqlca.sqlstate);            goto exit;            }            /*            * Step 4: Search through the new log records to find our INSERT            */            while (true) {            char *ptr = NULL;            char *transactionId = NULL;            sqlint32 recordLength = 0;            memset(&sqlca, 0x00, sizeof sqlca);            memset(&logInfo, 0x00, sizeof logInfo);            memset(&logData, 0x00, sizeof logData);            memset(&endLsn, 0xFF, sizeof endLsn);            logData.iCallerAction = DB2READLOG_READ_SINGLE;            logData.piStartLSN = &startLsn;            logData.piEndLSN = &endLsn;            logData.poLogBuffer = buffer;            logData.iLogBufferSize = sizeof buffer;            logData.iFilterOption = DB2READLOG_FILTER_OFF;            logData.poReadLogInfo = &logInfo;            rc = db2ReadLog(db2Version810, &logData, &sqlca);            if (rc < 0) {            memcpy(SQLUDF_STATE, "38TA3", SQLUDF_SQLSTATE_LEN);            sprintf(SQLUDF_MSGTX, "Could not read log record.  rc = %d",            (int)rc);            goto exit;            }            else if (sqlca.sqlcode == SQLU_RLOG_READ_TO_CURRENT) {            memcpy(SQLUDF_STATE, "38TA4", SQLUDF_SQLSTATE_LEN);            strncpy(SQLUDF_MSGTX, "Last log record reached prematurely.",            SQLUDF_MSGTEXT_LEN);            goto exit;            }            else if (sqlca.sqlcode) {            memcpy(SQLUDF_STATE, "38TA5", SQLUDF_SQLSTATE_LEN);            snprintf(SQLUDF_MSGTX, SQLUDF_MSGTEXT_LEN, "SQL error while "            "reading log records.  SQLCODE = %d, SQLSTATE=%s",            sqlca.sqlcode, sqlca.sqlstate);            goto exit;            }            if (logInfo.logBytesWritten < 20) {            memcpy(SQLUDF_STATE, "38TA6", SQLUDF_SQLSTATE_LEN);            strncpy(SQLUDF_MSGTX, "Log Manager Header of record too small.",            SQLUDF_MSGTEXT_LEN);            goto exit;            }            memcpy(&startLsn, &logInfo.nextStartLSN, sizeof startLsn);            // the data in the buffer starts with the LSN, followed by the Log            // Manager Header; skip the LSN            ptr = buffer;            ptr += sizeof(SQLU_LSN);            // get the length of the log record (plus LSN)            recordLength = *(sqlint32 *)ptr + sizeof(SQLU_LSN);            ptr += 4;            // verify that this is a "Normal" log record            if (*(sqlint16 *)ptr != 0x004E) {            continue;            }            ptr += 2;            // skip behind the Log Manager Header (to the DMS Log Record Header);            // (we do not have "Compensation" records here and "Propagatable"            // doesn't occur either)            ptr += 2 + // flags            6; // LSN of previous record in same transaction            // remember the location of the transaction id            transactionId = ptr;            ptr += 6;            // now we are at the beginning of the DML Log Record Header            if (ptr - buffer + 18 + 4 > recordLength) {            continue;            }            // check that the "Function identifier" in the DMS header indicates an            // "INSERT" log record            ptr += 1;            if (*(unsigned char *)ptr != 118) {            continue;            }            // skip to the record data            ptr += 5 + // remainder of DMS Log Record Header            2 + // padding            4 + // RID            2 + // record Length            2 + // free space            2; // record offset            // the record contains data if the 1st byte of the record header (the            // record type) is 0x00 or 0x10, or if the bit 0x04 is set            if (*ptr != 0x00 && *ptr != 0x10 && (*ptr & 0x04) == 0) {            continue;            }            ptr += 4;            // we reached the record data and the unique value can be found after            // the record length            ptr += 1 + // record type            1 + // reserved            2 + // length of fixed length data            4; // RID            // that's where the unique value should be            // once we found the unique value, extract the transaction ID and            // convert it to a string            if (memcmp(ptr, uniqueVal, 13) == 0) {            int i = 0;            char *result = taId;            for (i = 0; i < 6; i++) {            sprintf(result, "%02hhx", ptr[i]);            result += 2;            }            *result = '/0';            *taId_ind = 0;            break; // found the correct log record            }            }            exit:            EXEC SQL ROLLBACK TO SAVEPOINT get_transaction_id;            return SQLZ_DISCONNECT_PROC;            }            

可以使用位于 sqllib/samples/c/ 目录中的 bldrtn 脚本来编译这一存储过程。该脚本将生成一个共享库,共享库会被复制到 sqllib/function 目录中。此操作完成后,您即可按照清单 3 所示将过程注册到数据库中。完成这最后一个步骤后,才能开始使用该过程。

清单 3. 将过程注册到数据库中
            CREATE PROCEDURE getTransactionId ( OUT transactionId CHAR(12) )            SPECIFIC getTaId            DYNAMIC RESULT SETS 0            MODIFIES SQL DATA            NOT DETERMINISTIC            NEW SAVEPOINT LEVEL            LANGUAGE C            EXTERNAL NAME 'transaction-id!getTransactionId'            FENCED THREADSAFE            NO EXTERNAL ACTION            PARAMETER STYLE SQL            PROGRAM TYPE SUB            NO DBINFO@            

测试过程
最后一步是检查过程的功能是否正确。清单 4 显示一些在 DB2 命令行上执行过而自动提交关闭的 SQL 语句。最前面的场景说明仍然激活循环日志记录时会出现的错误。在此之后,您将看到进行某些数据修改后,对存储过程的各种调用的结果。当然,只有在执行了 COMMIT 或 ROLLBACK 后并且当前事务触发了日志记录的写操作时,DB2 才分配新的事务 ID。

清单 4. 测试过程
            $ db2 -c- -td@            db2 => CALL getTransactionId(?)@            SQL0443N  Routine "*ACTIONID" (specific name "") has returned an error            SQLSTATE with diagnostic text "SQL error while reading log records.  SQLCODE =            -2651, SQLS".  SQLSTATE=38TA1            db2 => ? sql2651@            SQL2651N The log records associated with the database can not be            asynchronously read.            Explanation:            The asynchronous read log API was used against a connected            database which does not have LOG RETAIN or USER EXITS ON.  Only            databases which are forward recoverable may have their associated            logs read.            User Response:            Update the database configuration for the database, identified to            the asynchronous read log API, turning LOG RETAIN and/or USER            EXITS ON.            db2 => UPDATE DATABASE CONFIGURATION USING LOGRETAIN ON@            db2 => BACKUP DATABASE sample TO /dev/null@            Backup successful. The timestamp for this backup image is : 20050305214103            db2 => TERMINATE@            $ db2stop force && db2start && db2 -c- -td@            db2 => CONNECT TO sample@            db2 => CALL getTransactionId(?)@            Value of output parameters            --------------------------            Parameter Name  : TRANSACTIONID            Parameter Value : 200503052054            Return Status = 0            db2 => COMMIT@            DB20000I  The SQL command completed successfully.            db2 => CALL getTransactionId(?)@            Value of output parameters            --------------------------            Parameter Name  : TRANSACTIONID            Parameter Value : 200503052054            Return Status = 0            db2 => CREATE TABLE t ( a INT )@            DB20000I  The SQL command completed successfully.            db2 => CALL getTransactionId(?)@            Value of output parameters            --------------------------            Parameter Name  : TRANSACTIONID            Parameter Value : 200503052055            Return Status = 0            db2 => ROLLBACK@            DB20000I  The SQL command completed successfully.            db2 => CALL getTransactionId(?)@            Value of output parameters            --------------------------            Parameter Name  : TRANSACTIONID            Parameter Value : 200503052056            Return Status = 0            

结束语
为什么 DBA 想具有识别当前工作单元的能力?原因不一而足,对复制过程的管理就是一个例子。使用此处介绍的技巧,可以充分利用 DB2 UDB 的独特功能,同时进一步了解数据库的后台操作。