并发控制例子
数据库应用中并发控制若干实现途径 一.引言 并发控制是指在多用户的环境下,对数据库进行并发操作进行规范的机制。其目的是为了避免对数据的丢失修改、读脏数据 与不可重复读等,从而保证数据的正确性与一致性。并发控制在多用户的模式下是十分重要的,但这一点经常被一些数据库 开发人员忽视,而且因为并发控制的层次和类型非常丰富,有时使人在选择时比较迷惑,不清楚衡量并发控制层次选择的原 则和途径。本文将从一个例子入手,结合数据库理论的相关知识,对数据库应用中并发控制的途径、方法做出一个较全面的 总结,希望能帮助读者找到合理的并发控制方法。 二.一个并发控制失败的例子 为了更好的理解并发控制的概念,我们先重复一个经常列举的例子。先给出银行数据库中一个经过简化的帐户表(account) 信息的数据字典定义,以后我们都将会以此表作为示例。 列名称 列代码 列类型 帐户号 Id(键值列) Char(10) 户主 Uname Char(10) 存入金额 Mdeposit Currency 支出金额 Mpayout Currency 存款余额 Mbalance Currency 这个例子是在客户程序与服务器端数据库的会话过程中产生的:某户主代表在银行前台取款 2,000 元,银行出纳查询用户的 存款信息显示银行存款余额 20,000 元;正在这时,另一银行帐户转帐支票支付该帐户 5,000 元,机器查询也得到当前用户存款 20,000 元,这时银行的出纳员看到用户存款超过了取款额,就支付了客户 2,000 元并将用户存款改为 18,000 元,然后银行的另 一名操作员根据支票,将汇入的 5,000 元加上,把用户的余额改为 25,000 元。很明显银行将会损失 2000 元,因为另一个出纳 员所做的修改被覆盖了。 这是由于对并发操作控制的失败造成的,由于没有对两Байду номын сангаас并发操作进行合理的隔离,对数据进行合理的锁定,导致经出纳员
免。 (二)借助于 DBMS 的功能。 大型关系系统都有比较好的并发控制功能。例如可以采用更新游标、显式加锁、更改事务隔离级别等等。当然在其使用方面 有很多注意的技巧,如:(1)事务定义最好不要包含客户交互部分。(2)只有在数据一致性要求特别严格,但并发度要求 不高的时候采用可重复读与可串行读的隔离级别。(3)在同一个事务当中,要适当根据需要来变更数据的锁定级别,但一般 情况下不要用 TABLOCK 这样粗粒度的封锁。(4)不同事务之间可以根据并发度的需要来显式设定隔离级别。(5)在包含客 户交互的操作中使用游标,并尽可能缩短交互时间。 我们看一个 informix 数据库中采用更新游标的例子。 定义更新游标语法: DECLARE CURSOR-name CURSOR FOR SELECT-statement FOR UPDATE[OF column-list]。更新游标在完成数据的浏览和修改时,要对当前的记录隐式加锁,注意更新游标只对可更新 视图有效。 为了提高并发度, 经常要结合滚动游标来使用, 滚动游标定义方法: DECLARE CURSORname SCROLL CURSOR[with hold] FOR SELECTstatement),不过滚动游标不对当前的记录加锁。 下段代码完成客户对帐户内容的浏览和修改,代码采用 informix 的 esql/c(以 c 语言作为宿主语言)来编写,展示了更新 游标的使用方法: $DECLARE mycurs CURSOR FOR SELECT Mdeposit,Mpayout,Mbalance FROM acount FOR UPDATE; //定义更新游标 $OPEN mycurs; //打开游标 for(;;) {$FETCH mycurs INTO $ Mdeposit,$Mpayout,$Mbalance;// 从游标中读记录 if sqlcode=SQLNOTFOUND then exit;//如果记录取完,则退出循环 ….//显示记录内容给用户 ….//如果用户决定要修改记录,则继续执行 $UPDATE acount SET (Mdeposit,Mpayout,Mbalance)= ($Mdeposit,$Mpayout,$Mbalance) WHERE CURRENT of mycurs;//更新数值 }
带有 FOR UPDATE 的游标语句有加锁功能。看上面的代码,在进行 FETCH 操作以后,游标所指向的当前记录被加共享锁,当 用户决定要修改时,将该记录上的锁提升为排它锁。那么此时其它用户不可以更新此记录。这种方法有个缺点,就是即使用 户不对当前记录进行修改,也要对当前的记录加锁,影响了并发度,那么此时可以采用的一个方法是:(1)定义一个滚动游 标来完成查询;(2)获取游标的一个记录,显示给用户;(2)用户浏览记录,直到要修改或者删除的记录;(3)当用户选 择修改一个记录时,为用户想要修改的记录定义一个更新游标(4)使用更新游标获取记录,并重新显示锁定的记录;(5) 更新这个记录。那么上面的程序就可以更改为: $DECLARE mycurs SCROLL CURSOR for SELECT Id, Mdeposit,Mpayout,Mbalance FROM acount;//定义滚动游标 $OPEN mycurs//打开滚动游标 for(;;) {$FETCH mycurs INTO $Id,$Mdeposit,$Mpayout,$Mbalance; //从游标中读取记录值 if sqlcode=SQLNOTFOUND then exit; //如果读完,退出循环 ….//显示记录内容给用户看 ….//如果用户决定要修改记录,则继续执行 $DECLARE mycurs_update CURSOR FOR SELECT Mdeposit,Mpayout,Mbalance FROM Acount WHERE Id=$Id; //定义更新游标 $FETCH mycurs_UPDATE INTO $Mdeposit,$Mpayout,$Mbalance; //读取数值 $UPDATE acount SET (Mdeposit,Mpayout,Mbalance)= ( $Mdeposit,$Mpayout,$Mbalance) WHERE CURRENT of mycurs_update; //更新数值 (三)利用开发工具的支持。 许多数据库开发工具都有一些方便的选项或部件来支持并发控制,而不论 DBMS 是否支持并发控制。我们看一下 Delphi 与 Powerbuilder 的并发控制方法。 Delphi 是一个优秀的 c/s 开发工具,它用来查询数据的数据库控件是 TQuery,它可以和 TUpdatesql 控件有机的结合起来完 成数据库表数据的浏览和更新。 其中在 TQuery 控件中有一个属性是 Updatemod (修改模式) 他有三种选择: , (1) upWhereAll:
查询所得到的客户端数据集与数据库的数据出现不一致,结果便产生了丢失修改。 三.数据库并发控制理论基础 在此对并发控制中经常用到的概念略做解释, 具体内容请读者查资料。 事务是数据库中一个重要概念, 它是一系列要么都做, 要么都不做的程序集合,是数据库并发控制的单位。事务并发控制不当的话,可以产生丢失修改、读脏数据、不可重复读等 数据不一致。但在应用中为了并发度的提高,可以容忍一些这样的不一致,例如大多数业务逻辑经适当的调整以后是可以容 忍不可重复读的。 当今流行的关系数据库系统 (如 oracle, server 等) sql 是通过事务隔离级别 (TRANSACTION ISOLATION LEVEL) 与封锁机制来定义并发控制所要达到的目标的,根据其提供的协议,我们可以得到几乎任何类型的合理的并发控制方式。例 如,Microsoft sql server 系统中有四种锁:共享锁,排它锁,意向锁(又分为共享意向锁,排它意向锁,共享意向排它锁), 修改锁。各种锁之间有确定的相容关系。有四种事务隔离级别:未提交读、提交读、可重复读、串行化读,不同的隔离级别 所规定的封锁协议不同。这一部分内容非常之丰富,足足可以写一本书,篇幅所限,不拟细述,有关内容请查阅数据库理论 方面的教材。 封锁类型与隔离级别如此之丰富,那么选择时到底本着一个什么原则呢?那就是数据一致性要求与并发度两个方面。例如四 种隔离级别数据一致性依次升高,但并发度依次降低,一般系统默认的隔离级别是提交读。一般来说这可以满足应用的要求 了,但这种隔离级别不能避免不可重复读的现象,就是说在你浏览数据库记录的期间,不同时间读的同一条记录可以有不同 的内容。有时需要动态的通过 sql 语句来改变其封锁状态或者隔离级别。 四.并发控制技术的实现途径 并发控制的实现途径有多种,如果 DBMS 支持的话,当然最好是运用其自身的并发控制能力。如果系统不能提供这样的功能, 可以借助开发工具的支持,还可以考虑调整数据库应用程序,而且有的时候可以通过调整工作模式来避开这种会影响效率的 并发操作。笔者对各种策略做了一个总结,主要有一下几点: (一)调整工作模式,修改应用程序,避免不必要的并发。 这在某些情况下是可行的,例如规定录入人员只能修改自己所创造的记录,那么就不会出现并发操作中的各种错误,因为这 时各个不同的用户所能更新的记录不会发生重合。这种情况下,需要在数据库表中增加用户列。在用户浏览记录时,将用户 列作为一个过滤条件,对应用程序的 sql 语句做相应的调整。但这种策略的作用有限,因为在大量情况下,并发控制不可避
在浏览和修改期间只要有人修改了此记录某个列,那么不管你是否修改过这个列,你的修改在提交时都不能成功。(2) upWhereChanged:只根据键值列和你已经修改的列来决定你的修改是否成功,如果别人所修改的本记录的列与你修改的列不 相交,那么你的修改仍然是成功的。(3)UpWhereOnly:只根据键值是否修改来判断你的更新是否成功。 与 TQuery 控件配套使用的 TUpdatesql 控件根据所指定的修改属性,自动生成所需的更新语句,非常方便。第 2 种模式是最 常用的修改模式,只要别人对记录所做的修改不与自己的重合,那么就会提交成功,这即保证不会发生数据的丢失、覆盖, 并且具有较高的并发度。还是上边的例子,比如说客户浏览记录后修改的是记录中 Mpayout、Mbalance 两列,那么在修改选 项 upWhereChanged 下,Tupdatesql 控件所生成的 SQL 语句是: UPDATE acount SET (Mpayout,Mbalance)=($Mpayout,$Mbalance) WHERE Key=Key_old and Mpayout=Mpayout_old and Mbalance=Mbalance_old; 其中 Key_old,Mpayout_old 和 Mbalance_old 是 delphi 替用户所生成的中间变量,暂存原先数据记录的旧值,用于比较旧值 与现在的值是否相等,如果不相等,说明已经有别的用户更改了该记录,那么为了避免丢失修改,该用户的更新操作不能完 成,反之则可以完成。那么当出纳员修改帐户时,如果别人已经修改了这个帐户,那么他的这次修改是不成功的,必须重新 刷新记录才可能成功修改。对上面的例子进行这种改造,就可以避免银行的损失。 与 Delphi 媲美的一个另一个工具是著名的 Powerbuilder,在其 DataWindows 的设计中,我们选择菜单 Rows|Update…,会出 现 Specify Update Characteristics 的设置窗口,在这个窗口中我们设置 Update 语句中 Where 子句的生成,以此来进行并发 控制。在这里有三个选项: (1)Key Columns:生成的 Where 子句中只比较表中的主键列的值与最初查询时是否相同来确定要 修改的记录。与 Delphi 中的 UpWhereOnly 选项对应。 (2)Key and Updateable Columns:生成的 Where 子句比较表中主键列 和可修改列的值与最初查询时否是相同。与 Delphi 的 upWhereall 相对应。 (3)Key and Modified Columns:与 Delphi 的 upWhereChanged 选项对应。Where 子句比较主键和要修改的列。 (四)调整应用。 有的数据库没有提供并发控制的功能,例如 Foxpro 等,象 Mysql 的某些版本也不支持事务。而且有的开发工具(例如一些网 页脚本编辑器等)也没有提供实现并发控制的部件,那么要实现并发控制,就只能借助于调整我们的应用程序和数据库结构 的办法了。