内容


使用 DB2 V9.5 乐观锁定特性改善并发性

使用全新的乐观锁定特性避免对长期锁的维护

Comments

简介

悲观锁定和乐观锁定

悲观锁定
悲观锁定策略的前提是,另一个用户很可能试图修改您正在修改的某个表行。如果选择了某个行,并检测到其他用户试图对该行执行更新或删除操作,那么该行将在这段时间内持有锁(例如,通过使用 repeatable read 隔离级别或以排他 exclusive 模式锁定表)。悲观锁定的优点就是能够保证实现一致且安全的更改。但是这种锁定策略的主要缺点就是可伸缩性较差。对于具有大量用户或运行长期事务的系统,或者事务涉及大量实体,需要等待锁释放的概率则会增加。

图 1 阐释了悲观锁定的功效。事务 1 读取某条特定记录并对该行应用一个锁。需要花些时间确定是否要对这个行执行更新操作。同时,事务 2 希望访问这个行,但是它必须等待事务 1 释放该行的锁。释放锁后,事务 2 才能收到 SELECT 操作的结果并继续执行它的业务逻辑。

图 1. 悲观锁定概念
悲观锁定概念
悲观锁定概念

乐观锁定
悲观锁定方法的主要问题是事务之间必须互相等待。避免发生这种情况的方法就是使用乐观锁定策略,即假设在修改某行时,另一个用户试图对这一行进行修改的可能性极低。如果确实对这一行进行了修改,那么更新或删除操作将会失败,应用程序逻辑将处理这些失败,例如,重新尝试选择。通过使用这种方法,在对行执行选择、更新或删除操作期间,不会持有行锁。但是,由此产生的问题是需要一种方式确保数据在被读取和修改期间没有发生变化。尽管应用程序需要更多的重试逻辑,乐观锁定策略的主要优点是最小化给定资源对于其他事务的不可用时间,因此,它具有比悲观锁定更好的伸缩性。

图 2 阐释了乐观锁定背后的思想。与 图 1 类似,事务 1 读取某个特定记录,但随后即释放锁。因此,事务 2 现在可以顺利地对同一行进行检索。在提交事务之前,事务 1 和事务 2 都必须检查该行在执行前面的 SELECT 之后是否发生改变。如果发生了一处修改,事务必须重新执行新的 SELECT 来检索当前数据。然而,若该行在执行 SELECT 之后未发生变化,则可以成功更新数据。

图 2. 乐观锁定概念
乐观锁定概念
乐观锁定概念

DB2 V9.5 中增强的乐观锁定

DB2 V9.5 的乐观锁定特性最小化了给定资源对于其他事务的不可用时间,进一步改善了可伸缩性。由于数据库管理器能够确定某一行何时会被修改,因此可以保证数据完整性,同时限制持有锁的时间。通过实现乐观并发控制,数据库管理器可以在完成读操作后立即释放行或页锁。

DB2 V9.5 for Linux, Unix, and Windows 支持更加简便、快捷的乐观锁定特性,并且不会产生误判(false positive)。这一支持通过如下所示的新 SQL 函数、表达式和特性实现:

  • 行标识符(RID_BITRID)内置函数:该内置函数可用于 SELECT 列表或谓词语句。例如,在谓词 WHERE RID_BIT(tab)=? 中,RID_BIT 等于谓词被实现为一种新的直接访问方法,从而可以更有效地定位行。在以前,这种称为值乐观锁定的技术确定值的方式为:将所有选择的列值添加到谓词,然后应用某些惟一的列组合来筛选出单个行,这种访问方法效率较低。
  • ROW CHANGE TOKEN 表达式:这种新的表达式返回一个标记作为 BIGINT。这个标记表示某一行的修改序列中的一个相对点。应用程序可以将某行的当前行修改标记值与上次取回行时保存的行修改标记值进行比较,以判断行是否发生修改。
  • 基于时间的更新检测:这个特性通过 ROW CHANGE TIMESTAMP 表达式添加到 SQL 中。要支持这一特性,表需要定义一个新生成的行修改时间戳列来保存时间戳值。这可以通过 ALTER TABLE 语句添加到现有表,或者在创建新表时定义行修改时间戳列。是否提供行修改时间戳列还将影响乐观锁定的行为,因为该列有助于将行修改标记的粒度从页级别提高到行级别,这对乐观锁定应用程序非常有利。
  • 隐式隐藏列:从兼容性方面来说,这个特性有助于将行修改时间戳列应用到现有表和应用程序。在使用隐式列列表时,隐式隐藏列不会被外部化。例如,对表执行 SELECT * 时不会在结果表中返回隐式隐藏的列,并且执行不包含列列表的 INSERT 语句时也不会要求提供隐式隐藏列的值,但是隐式隐藏列必须定义为允许 null 值或具有另一个默认值。

使用这种编程模型的应用程序将从增强的乐观锁定特性中获益。注意,未使用这种编程模型的应用程序被认为是非乐观锁定应用程序,它们将按照以前的方式工作。

图 3 阐释了 DB2 V9.5 乐观锁定特性的功效。事务 1 和事务 2 同时读取相同的行,包括 RID_BIT 和 ROW CHANGE TOKEN 值。随后,事务 1 在执行完 SELECT 并确保该行未发生修改后,通过将 RID_BITROW CHANGE TOKEN 谓词添加到 UPDATE 语句对行进行更新。现在,当事务 2 尝试使用与事务 1 相同的谓词对同一行进行更新时,它无法查找到该行,因为 ROW CHANGE TOKEN 的值已经根据事物 1 的 UPDATE 进行了更改。事务 2 必须进行重试更新,以取回最新的数据。

图 3. DB2 V9.5 中增强的乐观锁定特性
DB2 乐观锁定概念
DB2 乐观锁定概念

启用乐观锁定特性

由于不需要对表进行 DDL 修改即可使用针对乐观锁定的新 SQL 表达式和属性,因此可以轻松地在您的测试应用程序中尝试乐观锁定特性。

注意,在不进行 DDL 修改的情况下,乐观锁定应用程序可能会产生更多的漏判(false negative)。在生产环境中,如果应用程序发生了漏判,那么就不能够实现较好的伸缩,因为漏判很可能造成大量重试操作。因此,要避免发生漏判,执行乐观锁定的目标表应执行以下任意一种操作:

  • 创建时定义 ROW CHANGE TIMESTAMP 列
  • 进行修改以包含 ROW CHANGE TIMESTAMP 列

要在应用程序中启用乐观锁定支持,需要执行以下基本步骤:

  • 在初始查询中,对要进行处理的所有行的行标识符和行修改标记执行 SELECT(使用 RID_BIT()RID() 内置函数)。
  • 释放行锁,以便其他应用程序可以对表执行 SELECTINSERTUPDATEDELETE(例如,使用游标稳定性(cursor stability)隔离级别或未提交读(uncommitted read)隔离级别)。
  • 对目标行执行可搜索的 UPDATEDELETE,在搜索条件中使用行标识符和行修改标记,乐观地假定自从执行最初的 SELECT 语句后,未锁定的行没有发生过修改。
  • 如果行发生了修改,UPDATE 操作将失败,应用程序逻辑必须处理这一失败。例如,应用程序将重试 SELECTUPDATE 操作。

运行以上步骤之后:

  • 如果应用程序执行重试的次数超过预期值或与预期值相同,那么向表添加一个行修改时间戳列,以确保 RID_BIT 函数对行标识符作出的修改只会使行修改标记无效,而同一数据页上的其他活动不受影响。
  • 要查看给定时间范围内执行了插入或更新操作的行,需要创建或修改表以包含一个行修改时间戳列。该列由数据库管理器自动维护,并可以通过列名或 ROW CHANGE TIMESTAMP 表达式进行查询。
  • 对于行修改时间戳列(只针对这种列),如果该列使用 IMPLICITLY HIDDEN 属性定义,那么当对表列进行隐式引用时,不会对该列执行外部化。然而,在 SQL 语句中,可以始终显式引用一个隐式隐藏的列。当向表添加列可能造成使用隐式列列表的应用程序失败时,这一特性非常有用。

行修改标记的粒度和漏判

RID_BIT() 内置函数和行修改标记是实现乐观锁定的惟一需求。然而,表模式也会影响乐观锁定的行为。

例如,行修改时间戳列会促使 DB2 服务器保存最后一次修改(或第一次插入)行的时间。这提供了一种方式捕获最近一次修改行的时间戳。可以通过以下任意一条语句定义行修改时间戳列:

  • GENERATED ALWAYS FOR EACH ROW ON UPDATE AS ROW CHANGE TIMESTAMP
    该时间戳列始终由数据库管理器维护
  • GENERATED BY DEFAULT FOR EACH ROW ON UPDATE AS ROW CHANGE TIMESTAMP
    该时间戳列默认情况下由数据库管理器维护,但是也接受用户提供的输入值。

当应用程序对表使用新的 ROW CHANGE TOKEN 表达式时,需要考虑以下两种可能性:

  • 表没有定义行修改时间戳列:ROW CHANGE TOKEN 表达式返回一个派生的 BIGINT 值,由同一页面中的所有行共享。如果页面中的某行被更新,将针对该页面中的所有行修改行修改标记。这意味着,对其他行进行修改时更新会失败,这一特性被称为漏判。

    注意:只有在应用程序可以容忍漏判,并且不希望向每一行添加针对 ROW CHANGE TIMESTAMP 列的额外存储的情况下,才使用这种模式。
  • 表具有一个行修改时间戳列:ROW CHANGE TOKEN 表达式返回一个从列的时间戳值获得的 BIGINT 值。在这种情况下,发生漏判的机率大大减少:如果对表进行了重组和重新分布,那么当移动某行并且应用程序使用以前的 RID_BIT() 值时,将发生漏判现象。



可通过以下 SELECT 检查行修改时间戳列是否存在:

清单 1. SELECT 查询
      SELECT COLNAME, ROWCHANGETIMESTAMP, GENERATED FROM SYSCAT.COLUMNS
            WHERE TABNAME='tablename' AND ROWCHANGETIMESTAMP='Y'

      COLNAME       ROWCHANGETIMESTAMP    GENERATED
      ------------  ------------------    ---------
      ROWCHGTS      Y                     A

在这个示例中,存在一个行修改时间戳列 ROWCHGTS,并通过 GENERATED ALWAYS 子句定义(值 “A” 表示 GENERATED ALWAYS,而值 “D” 表示 GENERATED BY DEFAULT)。

基于时间的更新检测

某些应用程序需要了解特定时间范围内的数据库更新,以便进行数据复制、场景审计等等。这可以通过包含行修改时间戳列的表实现,通过定义行修改时间戳列来保存 ROW CHANGE TIMESTAMP 表达式生成的时间戳值。这种新的 ROW CHANGE TIMESTAMP 表达式返回的时间戳表示最后一次进行行修改的时间,使用类似于 CURRENT TIMESTAMP 的本地时间表示。对于已经更新过的行,将反映对行执行的最近更新。否则,该值将对应于最初的行插入时间。

ALTER TABLE 语句之后未进行更新的行将返回列的类型默认值,该值为 0001 年 1 月 1 日午夜。只有进行过更新的行具有惟一的时间戳。使用离线表重组对时间戳进行具体化的行将返回一个惟一的时间戳,该时间戳在表重组期间生成。REORG 仅仅使用 INPLACE 选项无法满足需求,因为它没有对模式修改进行具体化。

清单 2. 具有行修改时间戳列的表
      CREATE TABLE EMPLOYEE (EMPNO CHAR(6) NOT NULL,
              ......
              ROWCHGTS TIMESTAMP NOT NULL
              GENERATED ALWAYS
              FOR EACH ROW ON UPDATE AS
              ROW CHANGE TIMESTAMP)
清单 3. 未定义行修改时间戳列的表,但稍后通过一条 ALTER TABLE 语句添加
      ALTER TABLE EMPLOYEE ADD COLUMN
              ROWCHGTS TIMESTAMP NOT NULL
              GENERATED ALWAYS
              FOR EACH ROW ON UPDATE AS
              ROW CHANGE TIMESTAMP
清单 4. 选择在最近 30 天内发生修改的所有行
      SELECT * FROM EMPLOYEE WHERE
              ROW CHANGE TIMESTAMP FOR EMPLOYEE <= CURRENT TIMESTAMP AND
              ROW CHANGE TIMESTAMP FOR EMPLOYEE >= CURRENT TIMESTAMP - 30 days
表 1. 在创建具有行修改时间戳列的表后,使用 INSERT、IMPORT 或 LOAD 填充后的 ROW CHANGE TIMESTAMP 列的内容
EMPNOFIRSTNMELASTNAMEPHONENOROW CHANGE TIMESTAMP
000010CHRISTINEHAAS39782007-12-20 13:53:01.296000
000020MICHAELTHOMPSON34762007-12-20 13:53:01.312000
000030SALLYKWAN47382007-12-20 13:53:01.312001
表 2. 将行修改时间戳列添加到现有表后,ROW CHANGE TIMESTAMP 列的内容
EMPNOFIRSTNMELASTNAMEPHONENOROW CHANGE TIMESTAMP
000010CHRISTINEHAAS39780001-01-01 00:00:00.000000
000020MICHAELTHOMPSON34760001-01-01 00:00:00.000000
000030SALLYKWAN47380001-01-01 00:00:00.000000

隐式隐藏列

这一特性有利于将行修改时间戳列应用到现有表和应用程序中。CREATEALTER TABLE 语句中的 IMPLICITLY HIDDEN 属性表示,除非根据名称显式引用列,否则该列在 SQL 语句中不可见。例如,假设某个表包含一个使用 IMPLICITLY HIDDEN 子句定义的列,SELECT * 操作的结果将不会包含隐式隐藏的列。然而,如果 SELECT 显式引用隐式隐藏列的名称,那么它将在结果表中包含该列。必须只针对 ROW CHANGE TIMESTAMP 列指定 IMPLICITLY HIDDEN

清单 5. 行修改时间戳的隐式隐藏的列
      CREATE TABLE SALARY_INFO (
              LEVEL INT NOT NULL,
              SALARY INT NOT NULL, 
              UPDATE_TIME TIMESTAMP NOT NULL
              IMPLICITLY HIDDEN
              GENERATED ALWAYS FOR EACH ROW ON UPDATE AS ROW CHANGE TIMESTAMP)

                or

      ALTER TABLE SALARY_INFO
      ADD COLUMN UPDATE_TIME TIMESTAMP NOT NULL
      IMPLICITLY HIDDEN
      GENERATED ALWAYS FOR EACH ROW ON UPDATE AS ROW CHANGE TIMESTAMP
清单 6. 使用 DESCRIBE 命令显示表列
      DESCRIBE TABLE SALARY_INFO

                   Data type                     Column
      Column name  schema    Data type name      Length     Scale Nulls
      -----------  --------- ------------------- ---------- ----- ------
      LEVEL        SYSIBM    INTEGER                      4     0 No
      SALARY       SYSIBM    INTEGER                      4     0 No
      UPDATE_TIME  SYSIBM    TIMESTAMP                   10     0 No
清单 7. 针对隐式隐藏的列执行 INSERT 和 SELECT
      INSERT INTO SALARY_INFO VALUES (1, 50000)

      SELECT * FROM SALARY_INFO

      LEVEL       SALARY
      ----------- -----------
                1       50000
清单 8. 显式引用隐式隐藏列的 INSERT 和 SELECT
      INSERT INTO SALARY_INFO (LEVEL, SALARY, UPDATE_TIME)
              VALUES (2, 30000, DEFAULT)

      SELECT LEVEL, SALARY, UPDATE_TIME
              FROM SALARY_INFO
              WHERE LEVEL = 2

      LEVEL       SALARY      UPDATE_TIME
      ----------- ----------- --------------------------
                2       30000 2007-12-18-15.34.24.437000

乐观锁定特性的局限性和注意事项

  • 不支持 ROW CHANGE TIMESTAMP 列的元素包括:主键、外键、多维聚合(multidimensional clustered,MDC)列、范围分区(range partition)列、数据库散列分区(database hashed partitioning)键、DETERMINED BY 约束列和别名。
  • Database Partitioning Feature (DPF) 配置不支持 RID() 函数。
  • 在乐观锁定场景中,在取回到更新操作期间执行在线或离线表 REORG 可能会造成更新失败,但是普通的应用程序重试逻辑应该能够处理。
  • 在 V9.5 中,IMPLICITLY HIDDEN 属性只被限定到 ROW CHANGE TIMESTAMP 列以实现乐观锁定。
  • 对于后来才添加 ROW CHANGE TIMESTAMP 列的表,在保证所有行已被具体化之前,INPLACE REORG 的使用会受到限制(返回错误代码 SQL2219, reason code 13)。这可通过 LOAD REPLACE 命令或典型的表 REORG 实现。这将防止发生漏判,具有 ROW CHANGE TIMESTAMP 列的表没有限制。

使用场景

某员工履行新的工作职责并调到另一个部门工作。公司的两名经理(原部门的经理 Manager1 和新部门的经理 Manager2)正在使用人事管理应用程序更新 SAMPLE 数据库的 EMPLOYEE 表中的员工记录。此时存在一种可能,即两名经理可能同时对同一名员工的记录进行更新。当 Manager1 选择并更新这条员工记录时,Manager2 也对同一条记录执行更新。这时,乐观锁定特性将发挥作用,例如,它使 Manager2 在使用当前应用程序更新时了解到某个特定记录已经被更新过。这样,应用程序可以更容易地执行指令,因为它不必实现自己的更新检测逻辑。

场景 1

EMPLOYEE 表包含一个隐式隐藏的 ROW CHANGE TIMESTAMP 列(后来添加)并且只有 Manager1 访问了该表。Manager1 从 EMPLOYEE 表中选择数据并在稍后尝试将 Christine Haas 的电话号码 3978 更新为 1092。更新成功。

清单 9. SELECT 语句 (Manager1)
      SELECT RID_BIT(EMPLOYEE),
              ROW CHANGE TOKEN FOR EMPLOYEE,
              EMPNO, FIRSTNME, LASTNAME, PHONENO, ROWCHGTS
              FROM EMPLOYEE FETCH FIRST 3 ROWS ONLY
表 3. SELECT 的结果
乐观锁定表达式EMPLOYEE 表
RID_BITROW CHANGE TOKENEMPNOFIRSTNMELASTNAMEPHONENOROW CHANGE TIMESTAMP
x'0400400100000000 0000000000FA9023'74904229642240000010CHRISTINEHAAS39780001-01-01 00:00:00.000000
x'0500400100000000 0000000000FA9023'74904229642240000020MICHAELTHOMPSON34760001-01-01 00:00:00.000000
x'0600400100000000 0000000000FA9023'74904229642240000030SALLYKWAN47380001-01-01 00:00:00.000000
清单 10. UPDATE 语句
      UPDATE EMPLOYEE SET
            (FIRSTNME,LASTNAME,PHONENO) = ('CHRISTINE','HAAS','1092')
            WHERE RID_BIT(EMPLOYEE)=x'04004001000000000000000000FA9023' AND
            ROW CHANGE TOKEN FOR EMPLOYEE=74904229642240
表 4. UPDATE 的结果
RID_BITROW CHANGE TOKENEMPNOFIRSTNMELASTNAMEPHONENOROW CHANGE TIMESTAMP
x'0400400100000000 0000000000FA9023'141285645885181032000010CHRISTINEHAAS10922007-12-20
11:55:45.593000
x'0500400100000000 0000000000FA9023'74904229642240000020MICHAELTHOMPSON34760001-01-01 00:00:00.000000
x'0600400100000000 0000000000FA9023'74904229642240000030SALLYKWAN47380001-01-01 00:00:00.000000

场景 2

EMPLOYEE 表包含一个隐式隐藏的 ROW CHANGE TIMESTAMP 列,并且 Manager1 和 Manager2 同时访问该表。Manager1 从 EMPLOYEE 表中选择数据并稍后尝试更新这些数据。然而,在他选择这些数据到执行更新期间,Manager2 对相同的数据进行了更新。Manager2 执行的更新成功,而 Manager1 执行的更新失败。

表 5. SELECT 的结果 (Manager1 和 Manager2)
RID_BITROW CHANGE TOKENEMPNOFIRSTNMELASTNAMEPHONENOROW CHANGE TIMESTAMP
x'0400400100000000 0000000000FA9023'74904229642240000010CHRISTINEHAAS39780001-01-01 00:00:00.000000
x'0500400100000000 0000000000FA9023'74904229642240000020MICHAELTHOMPSON34760001-01-01 00:00:00.000000
x'0600400100000000 0000000000FA9023'74904229642240000030SALLYKWAN47380001-01-01 00:00:00.000000
清单 11. UPDATE 语句 (Manager2)
      UPDATE EMPLOYEE SET
            (FIRSTNME,LASTNAME,PHONENO) = ('CHRISTINE','HAAS','1092')
            WHERE RID_BIT(EMPLOYEE)=x'04004001000000000000000000FA9023' AND
            ROW CHANGE TOKEN FOR EMPLOYEE=74904229642240
表 6. UPDATE 的结果 (Manager2)
RID_BITROW CHANGE TOKENEMPNOFIRSTNMELASTNAMEPHONENOROW CHANGE TIMESTAMP
x'0400400100000000 0000000000FA9023'141285645885181032000010CHRISTINEHAAS10922007-12-20
11:55:45.593000
x'0500400100000000 0000000000FA9023'74904229642240000020MICHAELTHOMPSON34760001-01-01 00:00:00.000000
x'0600400100000000 0000000000FA9023'74904229642240000030SALLYKWAN47380001-01-01 00:00:00.000000
清单 12. UPDATE 语句 (Manager1)
      UPDATE EMPLOYEE SET
            (FIRSTNME,LASTNAME,PHONENO) = ('CHRISTINE','HAAS','1092')
            WHERE RID_BIT(EMPLOYEE)=x'04004001000000000000000000FA9023' AND
            ROW CHANGE TOKEN FOR EMPLOYEE=74904229642240

UPDATE 的结果 (Manager1)

Manager1 的更新失败。由于 Manager2 执行了 UPDATE,ROW CHANGE TOKEN 发生了改变,因此,当将执行 SELECT 时取回的标记与由 Manager2 的应用程序更新后的当前值进行比较时,Manager1 的 UPDATE 语句的 ROW CHANGE TOKEN 谓词失败。因此 UPDATE 无法找到指定的行,返回消息 “SQL0100W No row was found for FETCH, UPDATE or DELETE; or the result of a query is an empty table. SQLSTATE=02000”。

场景 3

EMPLOYEE 表包含一个隐式隐藏的 ROW CHANGE TIMESTAMP 列,并且 Manager1 和 Manager2 同时访问该表。Manager1 对行进行了更新,但还未提交修改。Manager2 使用 Uncommitted Read 隔离级别从 Employee 表中选择数据。Manager1 提交他做出的修改。Manager2 尝试对相同的数据进行更新。Manager2 执行更新成功,因为应用程序读取的是 Manager1 未提交的更新。然而,如果 Manager1 回滚更新而不是提交更新,Manager2 的更新将失败。

表 7. SELECT 的结果 (Manager1)
RID_BITROW CHANGE TOKENEMPNOFIRSTNMELASTNAMEPHONENOROW CHANGE TIMESTAMP
x'0400400100000000 0000000000FA9023'74904229642240000010CHRISTINEHAAS39780001-01-01 00:00:00.000000
x'0500400100000000 0000000000FA9023'74904229642240000020MICHAELTHOMPSON34760001-01-01 00:00:00.000000
x'0600400100000000 0000000000FA9023'74904229642240000030SALLYKWAN47380001-01-01 00:00:00.000000
清单 13. 未提交的 UPDATE 语句 (Manager1)
      UPDATE EMPLOYEE SET
            (FIRSTNME,LASTNAME,PHONENO) = ('CHRISTINE','HAAS','1092')
            WHERE RID_BIT(EMPLOYEE)=x'04004001000000000000000000FA9023' AND
            ROW CHANGE TOKEN FOR EMPLOYEE=74904229642240
表 8. SELECT 的结果 (Manager2)
RID_BITROW CHANGE TOKENEMPNOFIRSTNMELASTNAMEPHONENOROW CHANGE TIMESTAMP
x'0400400100000000 0000000000FA9023'141285665533242120000010CHRISTINEHAAS10922007-12-20
16:47:03.125000
x'0500400100000000 0000000000FA9023'74904229642240000020MICHAELTHOMPSON34760001-01-01 00:00:00.000000
x'0600400100000000 0000000000FA9023'74904229642240000030SALLYKWAN47380001-01-01 00:00:00.000000
表 9. 提交 UPDATE 的结果 (Manager1)
RID_BITROW CHANGE TOKENEMPNOFIRSTNMELASTNAMEPHONENOROW CHANGE TIMESTAMP
x'0400400100000000 0000000000FA9023'141285665533242120000010CHRISTINEHAAS10922007-12-20
16:47:03.125000
x'0500400100000000 0000000000FA9023'74904229642240000020MICHAELTHOMPSON34760001-01-01 00:00:00.000000
x'0600400100000000 0000000000FA9023'74904229642240000030SALLYKWAN47380001-01-01 00:00:00.000000
清单 14. UPDATE 语句 (Manager2)
      UPDATE EMPLOYEE SET
            (FIRSTNME,LASTNAME,PHONENO) = ('CHRISTINE','HAAS','1090')
            WHERE RID_BIT(EMPLOYEE)=x'04004001000000000000000000FA9023' AND
            ROW CHANGE TOKEN FOR EMPLOYEE=141285665533242120
表 10. UPDATE 的结果 (Manager2)
RID_BITROW CHANGE TOKENEMPNOFIRSTNMELASTNAMEPHONENOROW CHANGE TIMESTAMP
x'0400400100000000 0000000000FA9023'141285667099502664000010CHRISTINEHAAS10902007-12-20
16:51:53.125000
x'0500400100000000 0000000000FA9023'74904229642240000020MICHAELTHOMPSON34760001-01-01 00:00:00.000000
x'060040010000000000 00000000FA9023'74904229642240000030SALLYKWAN47380001-01-01 00:00:00.000000

Manager1 提交修改而 Manager2 尝试更新相同的数据的结果如下:

Manager2 最后执行的更新成功,因为应用程序从 Manager1 处读取的是未提交的更新;Manager2 的 UPDATE 语句中的 ROW CHANGE TOKEN 谓词成功,因为 Manager1 使用新标记提交了修改。

表 11. Manager1 发出 ROLLBACK 而不是 COMMIT 时的结果
RID_BITROW CHANGE TOKENEMPNOFIRSTNMELASTNAMEPHONENOROW CHANGE TIMESTAMP
x'0400400100000000 0000000000FA9023'74904229642240000010CHRISTINEHAAS39780001-01-01
00:00:00.000000
x'0500400100000000 0000000000FA9023'74904229642240000020MICHAELTHOMPSON34760001-01-01 00:00:00.000000
x'0600400100000000 0000000000FA9023'74904229642240000030SALLYKWAN47380001-01-01 00:00:00.000000

Manager1 对修改执行回滚后,Manager2 试图根据 Manager1 的未提交的 UPDATE 执行数据更新,则结果如下:

Manager2 最后执行的更新失败,因为当 Manager1 回滚到初始标记时,ROW CHANGE TOKEN 谓词失败,因此 UPDATE 无法找到特定行。

场景 4

EMPLOYEE 表包含 ROW CHANGE TIMESTAMP 列,并且 Manager1 和 Manager2 同时访问该表。Manager1 选择一行并尝试更新它。然而,在他进行选择到更新的期间,Manager2 更新了同一数据页中的其他数据(不一定与 Manager1 处理的数据相同,但是位于另一行中)。因此,当 Manager1 尝试更新数据时,更新将会失败。

清单 15. UPDATE 语句 (Manager2)
      UPDATE EMPLOYEE SET
            (FIRSTNME,LASTNAME,PHONENO) = ('CHRISTINE','HAAS','1092')
            WHERE RID_BIT(EMPLOYEE)=x'04004001000000000000000000FA9023' AND
            ROW CHANGE TOKEN FOR EMPLOYEE=74904229642240
表 12. UPDATE 的结果 (Manager2)
RID_BITROW CHANGE TOKENEMPNOFIRSTNMELASTNAMEPHONENO
x'0400400100000000 0000000000FA9023'141285645885181032000010CHRISTINEHAAS1092
x'0500400100000000 0000000000FA9023'141285645885181032000020MICHAELTHOMPSON3476
x'0600400100000000 0000000000FA9023'141285645885181032000030SALLYKWAN4738
清单 16. 对另一行执行 UPDATE 语句 (Manager1)
      UPDATE EMPLOYEE SET
            (FIRSTNME,LASTNAME,PHONENO) = ('MICHAEL','THOMPSON','9012')
            WHERE RID_BIT(EMPLOYEE)=x'04004001000000000000000000FA9023' AND
            ROW CHANGE TOKEN FOR EMPLOYEE=74904229642240

Manager1 试图对同一数据页中的另一行执行更新,结果如下:

Manager1 执行更新失败,因为在比较标记时,由于所有行的 ROW CHANGE TOKEN 值发生了变化(即使 Manager1 尝试进行更新的行实际上没有发生变化),ROW CHANGE TOKEN 谓词失败。如果向 EMPLOYEE 表添加行修改时间戳列,则漏判场景中的 UPDATE 不会发生失败。

场景 5

EMPLOYEE 表包含 ROW CHANGE TIMESTAMP 列。它进行了修改并且添加了一个 ROW CHANGE TIMESTAMP 列。Manager1 和 Manager2 访问该表。Manager1 从中选择一行并尝试更新。然而,在他进行选择到更新期间,Manager2 更新了同一数据页中的其他数据(不一定与 Manager1 处理的数据相同,而是位于另一行中)。由于已经添加了 ROW CHANGE TIMESTAMP 列,如果对不同行进行更新,即使位于相同页面,更新也会成功。

清单 17. UPDATE 语句 (Manager2)
      UPDATE EMPLOYEE SET
            (FIRSTNME,LASTNAME,PHONENO) = ('CHRISTINE','HAAS','1092')
            WHERE RID_BIT(EMPLOYEE)=x'04004001000000000000000000FA9023' AND
            ROW CHANGE TOKEN FOR EMPLOYEE=74904229642240
表 13. UPDATE 的结果(Manager2)
RID_BITROW CHANGE TOKENEMPNOFIRSTNMELASTNAMEPHONENOROW CHANGE TIMESTAMP
x'0400400100000000 0000000000FA9023'141285673714388072000010CHRISTINEHAAS10922007-12-20
18:22:25.593000
x'0500400100000000 0000000000FA9023'74904229642240000020MICHAELTHOMPSON34760001-01-01 00:00:00.000000
x'0600400100000000 0000000000FA9023'74904229642240000030SALLYKWAN47380001-01-01 00:00:00.000000
清单 18. 对另一行执行 UPDATE 语句 (Manager1)
UPDATE EMPLOYEE SET
(FIRSTNME,LASTNAME,PHONENO) = ('MICHAEL','THOMPSON','9012')
WHERE RID_BIT(EMPLOYEE)=x'04004001000000000000000000FA9023' AND
ROW CHANGE TOKEN FOR EMPLOYEE=74904229642240
表 14. UPDATE 的结果 (Manager1)
RID_BITROW CHANGE TOKENEMPNOFIRSTNMELASTNAMEPHONENOROW CHANGE TIMESTAMP
x'0400400100000000 0000000000FA9023'141285673714388072000010CHRISTINEHAAS10922007-12-20
18:22:25.593000
x'0500400100000000 0000000000FA9023'141285673726689984000020MICHAELTHOMPSON90122007-12-20
18:22:37.312000
x'0600400100000000 0000000000FA9023'74904229642240000030SALLYKWAN47380001-01-01 00:00:00.000000

场景 6

EMPLOYEE 表具有一个 ROW CHANGE TIMESTAMP 列,并且只有 Manager1 访问该表。Manager1 从中选择若干行并尝试更新它们。然而,在他进行选择到更新期间,表被离线重组。稍后,当 Manager1 尝试更新数据时,更新失败。行更新失败是因为执行 REORG 后 ROW CHANGE TIMESTAMP 列发生了变化。

清单 19. 重组 employee 表
REORG TABLE EMPLOYEE
表 15. 重组表 EMPLOYEE 后发生的变化
RID_BITROW CHANGE TOKENEMPNOFIRSTNMELASTNAMEPHONENOROW CHANGE TIMESTAMP
x'0400400100000000 0000000000FA9023'141285781563232400000010CHRISTINEHAAS39782007-12-21
11:29:30.250000
x'0500400100000000 0000000000FA9023'141285781563232401000020MICHAELTHOMPSON34762007-12-21
11:29:30.250001
x'0600400100000000 0000000000FA9023'141285781563232402000030SALLYKWAN47382007-12-21
11:29:30.250002
清单 20. 对表运行 REORG 后执行的 UPDATE 语句 (Manager1)
      UPDATE EMPLOYEE SET
            (FIRSTNME,LASTNAME,PHONENO) = ('CHRISTINE','HAAS','1092')
            WHERE RID_BIT(EMPLOYEE)=x'04004001000000000000000000FA9023' AND
            ROW CHANGE TOKEN FOR EMPLOYEE=74904229642240

Manager1 在运行 REORG 后尝试更新数据,其结果如下:

Manager1 执行更新失败,因为在 Manager1 执行 SELECTUPDATE 期间,另一个任务对表执行了离线重组,因此在将执行 SELECT 时检索到的标记和当前标记进行比较时,ROW CHANGE TOKEN 谓词失败。因此,UPDATE 语句无法找到具有 ROW CHANGE TOKEN 的行,这些行是在发生 REORG 之前检索得到的。

结束语

为了避免在使用悲观锁定技术时可能引发的锁等待问题,乐观锁定技术最小化了给定资源对于其他事务的不可用时间。由于数据库管理器可以确定行发生修改的时间,它可以确保数据完整性,同时限制持有锁的时间。通过使用乐观并发控制,数据库管理器在完成读操作之后可以立即释放行或页锁。

DB2 V9.5 支持进行更简便和快捷的乐观锁定,而且避免了误判的发生。这些支持通过行标识符(RID_BITRID)内置函数、ROW CHANGE TOKEN 表达式、基于时间的更新检测和隐式隐藏的列实现。使用这种编程模型的应用程序可以从增强的乐观锁定特性受益,并且能够进一步增强并发性。


相关主题


评论

添加或订阅评论,请先登录注册

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=Information Management
ArticleID=292783
ArticleTitle=使用 DB2 V9.5 乐观锁定特性改善并发性
publish-date=03032008