IBM Support

DB2提供一种更好的方法实现乐观锁定

Technical Blog Post


Abstract

DB2提供一种更好的方法实现乐观锁定

Body

——DB2 for i 中的 ROW CHANGE 列,让你的应用程序实现乐观锁定更加简单
 
在 IBM i 6.1 版本,DB2 为用户提供一项新功能(ROW CHANGE列)可以为需要使用乐观锁的应用程序改进其算法。乐观锁定是一种基于网络数据库应用程序中广泛应用的技术,这些应用程序一定会读取数据但是只是有可能更新读取过的数据,但其不会维持一个与数据库的持久连接。因为应用并不维持一个持久的连接,用户不能够依靠行级锁为那些后续更新的行保持数据完整性。实现乐观锁定的一个常见方法是,把这个应用程序可能稍后要更新的那一行对应的列的所有值记录在缓存中,再将这些值在接下来的更新语句作为条件。
 
SELECT * FROM SCHEMA.TEST
C1 C2 C3 ...
10 12 33 ...
...UPDATE SCHEMA.TEST SET C1 = 12 WHERE C1=10 AND C2=12 AND C3=33 ...
 
如果另一个用户已经对当前行做了更新操作,这条更新语句将会失败,因为选择条件无法匹配被更新过新行的值。因此,DB2 不会覆盖任何之前的改变。运用乐观锁定,许多应用程序的用户能够并行的从数据库访问数据,同时允许一个单独的更新语句发生,而不用担心数据的丢失或者其它并发用户覆盖已更新过的数据。
 
存储一行的每一个属性对用户来说十分不便,但应用程序需要一种方式保证当其它用户对数据做出改变的时候,当前用户不会改变行中任何一列的值。幸运的是,从 DB2 for i 6.1 版本开始,有一种更简单的方法来确定从应用程序读取记录到当前为止数据库记录是否发生了改变。
 
这就是 DB2 for i 的新特性乐观锁定真正突出的地方。用户可以使用新的 PREPARE 属性设置或者客户端应用编程接口设置来使用这项支持。运用这种新的技术,DB2 将在返回的结果集中自动增加两列,这两列用于唯一地标识当前行和上次更新的时间。
 
应用程序无需为这更新时间列赋值;这一行每次更新后,这一特定列的值也会自动更新。
 
列 ROW CHANGE 支持
 
从版本6.1起,应用程序能够请求在任一查询的结果集中返回 ROW CHANGE 列。ROW CHANGE 列由两个独立的二进制列组成,分别是行变化特征(RCT)和行 ID(RID)。应用程序可以决定是否通过这两列来保证唯一性,或者在大多数情况下足够保证唯一性(将会在下面分别进行解释)。这些列可以是被应用程序显式地加到选择语句的列表里,也可以更方便地通过传入一个准备语句的属性完成添加:

STMT = ‘SELECT * FROM SCHEMA.TEST’;
ATT = ‘WITH ROW CHANGE COLUMNS ALWAYS DISTINCT’;
EXEC SQL DECLARE C1 CURSOR FOR S1;
EXEC SQL PREPARE S1 ATTRIBUTES :ATT FROM :STMT
EXEC SQL OPEN C1;

某些应用编程接口,比如 JAVA Database Connectivity(JDBC)和 iAccess .NET provider也提供一些语句属性能够要求 DB2 在准备语句中添加这些列,从而查询语句本身不必改变。

 
行ID和行改变特征

RID 是一个 DB2 for i 为每一行自动添加的 BIGINT 类型的值。RID 标识行在表中的位置。

RCT 是一个派生出来的值。如果应用程序要求 RCT 和 RID 生成的值必须保证唯一性,那么表必须有一列是时间戳类型,而且拥有 FOR EACH ROW ON UPDATE AS ROW CHANGE TIMESTAMP 属性:
 
ALTER TABLE TEST ADD COLUMN
RTS TIMESTAMP NOT NULL
GENERATED ALWAYS
FOR EACH ROW ON UPDATE AS ROW CHANGE TIMESTAMP
IMPLICITLY HIDDEN
 
当一个表包含这一特殊的时间戳类型的列,每一行返回的 RCT 的值来源于这一特殊列中存储的值。应用程序无需为此列赋值;每次行更新时,RCT 这一列的值随之自动更新。像其它的列一样,在 6.1 及之后的版本中这一列能够被定义成隐藏列,这就表示当对这个表做 SELECT * 查询的时候,这一列不会被返回。
 
请注意,即使有这一附加列,RCT 自身存储的值并不能保证唯一性,这是因为在某些情况下可能有多行返回相同的时间戳。然而,RCT 和 RID 两列在一起可以保证唯一性。
 
有些应用程序则并不绝对要求返回的 ROW CHANGE 列值的唯一性,因此不必为表增加一个 ROW CHANGE 时间戳类型的列,RCT 将会在 DB2 内部生成。
 
决定一个表是否应该增加特殊时间戳类型的列,一个需要考虑的因素就是周期性更新操作的数量。对于一个拥有繁重更新活动的表,增加一个ROW CHANGE 时间戳类型的列可能是值得的,可以减少应用程序必须处理的漏报(这里指一个行的更新操作结束而并没有被更新的情况)。另一方面,如果更新操作很少发生,你能够节省磁盘空间,取代依靠由内部算法生成的 RCT。
 
当使用 DB2 准备属性,从游标中取出数据时,RID 和 RCT 分别以列 QRID 和 QTOKEN 的名字自动地添加到每一行的末尾:
 
STMT = ‘SELECT * FROM SCHEMA.TABLE’;
ATT = ‘WITH ROW CHANGE COLUMNS ALWAYS DISTINCT’;

EXEC SQL DECLARE C1 CURSOR FOR S1;
EXEC SQL PREPARE S1 ATTRIBUTES :ATT FROM :STMT
EXEC SQL OPEN C1;EXEC SQL FETCH C INTO ...

C1 C2 C3 ... Q TOKEN Q RID
10 12 33 ... #### $$$$

...EXEC SQL UPDATE TEST SET C1 = 12 WHERE ROW CHANGE
TOKEN(SHEMA.TABLE)=#### AND RID(SCHEMA.TABLE)=$$$$

 
注意:RCT 和 RID 是以 BIGINT 类型返回的。
 
程序示例:
 
下面这个程序示例会帮助你了解怎样使用这些特性。我们利用一个 DVD 网上商城,用户可以搜索 DVDs,加入购物车以及购买挑选好的 DVD。DVD 信息表的 RCT 和 RID 列用于乐观锁定显示在用户搜索界面下 DVD 的剩余数量。当用户决定购买 DVD,应用程序用这个数据更新可用的数量。
 
这个应用程序是一套在任一标准浏览器上可见的简单的网页。一个运行 IBM i 的 Power 系统作为网络服务器,使用服务器的脚本语言 PHP 处理与本地数据库的连接。DVD 网上商场使用RPG存储过程来访问数据和通过连接取回数据。
 
DVD 网上商城应用程序使用数据库模式 DVDSTORE,包括五个表(图1):
 
图像
 
 
USERS—应用程序用户的用户名和密码
DVDS —DVD 标题,价格,预存数量和类别列表
GENRE—DVDs 可能的类别列表
ORDERS—用户定单列表
DVDSONORDER—列单上的 DVD 购买列表
 
前端 PHP 代码
 
网页应用程序的用户需要一个界面来执行他们的操作。创建这样的界面是通过运行在 IBM i 上的 PHP 脚本,然后这些脚本生成浏览器上可见的HTML 网页。通过界面,用户能够登录,注销,搜索 DVDs,添加 DVDs 至购物车,查看购物车以及购买这些 DVDs。
 
index.php—应用程序的索引页面是用户访问整个网站的第一入口。用户通过索引页面上的链接能够浏览网站的其它页面。
 
login.php—登录页面包含一个填写用户名和密码的表单,表单将用户的登录信息返回给登录页面。一旦 PHP 脚本接收到登录数据,脚本会调动登录程序验证用户名和密码组合,为登录用户创建一个会话对象并保存这个用户 ID 便于以后使用。
 
logout.php—注销页面会清空当前的会话对象(从而用户将离开应用程序)然后回到索引页面。
 
search.php—作为初始入口,搜索页面为用户呈现一个允许用户按 DVDs 标题名或类别搜索的表单。一旦点击,提交按钮就会把用户搜索的条件返回给搜索页面。然后,当 PHP 脚本接收到这些数据,它会调用搜索程序将搜索结果以表格形式呈现给用户察看。应用程序也会把搜索程序返回的RCT 和 RID 一并记录下来。(图2)
 
图像
 
addToCart.php—一旦用户决定把 DVDs 加入到他们的购物车,DVDs 数据(包括 RCT 和 RID)会被返回给加入购物车页面。PHP 在用户的会话中保存这些 DVDs 信息以便于用户决定购买这些 DVDs(图3)。
 
图像
 
cart.php—用户可以通过网页上生成的表单查看和更改他们的购物车(保存在当前会话内)。用户也可以更改 DVDs 数量或者将 DVDs 从购物车中移除。
 
checkout.php—当用户准备购买购物车中的 DVDs 时,存储在购物车中的信息就于购买这些 DVDs。因为这个应用程序不会对 DVD 表 DVDSTORE 的任何行保持一个坚固锁,这种情况就必须使用乐观锁定(图4)。

图像
 

在这种情况下,应用程序(乐观地)假设最初的行从上一次程序读取后没有改变过,并且以存储在会话中的 RCT 和 RID 作为更新的条件,从而试图更新可用的数量(既然这个用户现在已经购买了这个 DVD)。如果从上一次读取过后这一行没有改变发生,更新操作将会成功,因为 RCT 和 RID 没有发生改变。如果另一个用户改变了这一行的任何一部分,数据库管理系统不会改变RCT的值,因此,更新操作会失败。在这个应用程序中,如果更新失败,最可能的原因是,当这个用户把 DVD 放入购物车的同时另一个用户刚好购买了同样的 DVD。为了恢复更新失败的操作,应用程序从行中重新读取数据来核实还有足够的 DVDs 剩余。假定有足够的 DVDs 剩下,应用程重新开始此过程,但是这次会为整个事务在这一行留下一个坚固锁。一旦数据库管理系统成功更新了这一行记录中的数量(DVDs 已经成功付款),数据库管理系统会去掉全部的坚固锁并且将结果发送给用户。

 
后端 RPG 代码
 
这个应用程序调用的程序是用 RPG 语言编写的,这样使用乐观锁定时应用程序能够利利用 RPG 语言的一些内建函数。
 
登录——应用程序传入用户提供的用户名和密码,登录成功时程序为用户的 ID 设置一个输出的参数,如果登录失败则返回0(图5)。

图像
 
搜索——应用程序传入用户想要找到的影片标题和类别(这些参数也可以为空,在这种情况下程序将忽略这个值)。搜索结果,包括任何找到的电影的 RCT 和 RID,都会以结果集的形式返回给应用程序(图6)。
 
图像
 
更新库存——在校检程序期间,使用 RID 和 RCT 对一行乐观锁定时,应用程序会尝试使用用户会话中的购物车数据来更新这个电影的该行记录。如果操作不能完成(记录中的数据被改变),程序会返回 SQLSTATE DVD99;如果更新成功将返回00000(图7)。
 
图像
 
重试更新库存——如果数据库中的更新操作失败,应用程序通过这行记录上规则的锁从这次失败中恢复,并且重试更新操作。应用程序会尝试从表中选择出新的值来决定是否有足够的库存留给用户购买。程序以 SQLSTATE 来通知调用程序恢复的结果。
 
生成订单——这个助手程序以用户 ID 作为输入参数,在订单表中生成一个条新的订单纪录并返回生成的订单 ID(图8)。
 
图像
 
增加订单——这个程序完成将 DVDs ID,数量,单价,订单 ID 及订单中的 DVD 信息加入到 DVDSONORDER 表中(图9)。

图像

 

原文作者Andrew Woodard,Jason Bruhnke,Kevin Adler

翻译者:李广奇

了解更多关于IBM i 的信息,请关注IBM i 新浪官方微博@IBMiChina

[{"Business Unit":{"code":"BU058","label":"IBM Infrastructure w\/TPS"},"Product":{"code":"SWG60","label":"IBM i"},"Component":"","Platform":[{"code":"PF025","label":"Platform Independent"}],"Version":"","Edition":"","Line of Business":{"code":"LOB57","label":"Power"}}]

UID

ibm11144900