在数据库管理系统(DBMS)的领域中,术语“并发性”用于表示不止一个应用程序基本上(从用户的角度来看)同时访问同一数据的能力。因为 DBMS 的主要优点之一就是可以在多个用户和多个应用程序中共享数据,所以数据库系统应该提供一种管理并发访问数据的方法。DBMS 必须确保维护数据的一致状态和数据的完整性。
取得该效果的一种方法就是实施只串行(serial-only)模式来处理数据库请求。即每个事务都要等待另一事务(具有更高的优先权或者比它早启动)完成其工作。然而,对于现在的在线系统和客户异常来说,这种处理方式所产生的性能水平简直令人无法接受。
而另一种方法就是,DBMS 可以通过 锁 的方式管理多个应用程序对数据的访问。锁是一种软件机制,用于在维护数据完整性和一致性的同时,允许尽可能大的吞吐量(通过最大限度地并发访问数据)。
并发性控制的重要性
如果没有控制并发性的有效方法,就可能损害数据的完整性和一致性。DBMS 必须保护数据库,防止发生下列状况:
- 丢失更新 —— 假设应用程序 A 和应用程序 B 同时读取数据库中的同一行,并且都为其中某一列计算新值。如果应用程序 A 先用其新值更新该行,随后应用程序 B 又更新同一行,那么第一次的更新(由应用程序 A 执行的)就会丢失。
- 不可重复读 —— 某些应用程序进程可能要求完成以下事件序列:程序 A 从表中读取特定的一行,然后继续进行其他的 SQL 请求。稍后,程序 A 再次读取开始的那一行,并且必须在所有的列中找到与第一次读取相同的值。如果缺乏合适的并发性控制,另一应用程序就可能在这两次读取操作之间修改该行数据。
- 访问未提交的数据 —— 应用程序 A 更新一行中的某些列的值,而在提交该修改之前,应用程序 B 读入该行的新(更新)值。如果应用程序 A 接着又“撤销”更新值(通过程序逻辑中的 SQL ROLLBACK 语句,或者因为发生错误由 DB2 UDB 自动进行回滚),那么,应用程序 B 对该行的处理就是基于未提交的(因而可能是不正确的)数据进行的。
在维护数据完整性的同时,提供多个应用程序同时访问数据的能力称作 并发性控制 。
锁
锁是一种由 DB2 UDB 用于完成并发性控制的软件机制。锁实质上就是一个控制块,将 DB2 UDB 对象或资源与应用程序关联起来,并控制其他应用程序如何访问同一对象或资源。与 DB2 UDB 资源有关联的应用程序被称为“持有”或“拥有”该锁。
通过使用锁,DB2 UDB(管理该数据库)可以防止发生上述几类问题。DB2 UDB 与另一 MVS 地址空间 IRLM 配合管理这些锁。IRLM 将跟踪这些锁及其所有者,以确定应用程序请求的 DB2 UDB 资源是否可用于该类工作。资源可以是 锁定的 或 共享的 ,这取决于当前资源上的锁的“持有者”所进行的处理类型,以及请求应用程序所预期的处理类型。
锁模式
最常用的两种锁模式是 共享的 和 排他的 。共享锁与只读操作有关联,这意味着持有该锁的应用程序可以读取数据,而其他应用程序也可以读取该数据。排他锁与写操作有关联,这意味着持有该锁的应用程序有资格更新数据,但在锁所有者完成更新(将修改提交给数据库)并释放该锁之前,其他应用程序无法使用该数据。
DB2 UDB 和 IRLM 使用其他类型和子类型的锁模式来实现锁定和并发性控制。您可以在 DB2 UDB Administration 手册中找到关于这点的更多详细描述。
锁定粒度
除了使用各种锁模式,DB2 UDB 还提供了不同的锁定级别,用以控制被锁定数据的范围。各种级别表示了 DB2 UDB 使用的锁定粒度,其范围可以从单个行到整个表空间。DB2 UDB 根据锁定粒度来使用不同的锁模式。
具有多种锁定级别的理由十分简单。某些应用程序可能要求有权读取或更新大范围的数据,而其他应用程序则可能只要求访问窄得多的范围。如果只能使用一种锁定级别,则会降低整个系统性能。例如,一下子锁定太多数据会强制其他应用程序进行不必要的等待。否则,DB2 UDB 可能使用过多的系统资源,尝试服务对附加数据资源进行锁定的附加请求。能够实现多种级别的锁粒度可以极大地提高并发性水平。
暂挂
若一个应用程序进程请求一个锁,而该锁已被另一应用程序进程所拥有,且不能共享,此时,就称该进程为 暂挂的 。请求应用程序会被挂起,即它将暂时停止运行。锁请求的优先次序如下:将新来的锁请求按照接收次序进行排队。已经持有锁的应用程序的请求以及进行锁提升的请求要比新应用程序的请求先得到服务。而在那些分组中,请求次序则为“先进先出(first in,first out,FIFO)”。
超时
当应用程序处于暂挂状态(见上面)超过了预设的一段时间间隔,那么就要终止该应用程序。该应用程序被称为已经超时。在终止该应用程序之前,会在 SQLCA 中收到一条合适的错误消息。SQLCA(SQL 通信域)是 SQL 应用程序预留的一块大小固定的存储区域,用于从 DB2 UDB 向程序传递条件代码和其他信息。
某些操作,如 COMMIT 和 ROLLBACK,就不能超时。在下面的子标题 RESOURCE TIMEOUT 中,将对决定应用程序进程可以等待资源多长时间的预设时间间隔进行讨论。
死锁
当两个或更多应用程序每个都持有另一应用程序所需资源上的锁,没有这些资源,那些应用程序都无法继续完成其工作时,这时就会出现死锁的状况。
以下是一个简单的死锁场景:
- 应用程序 A 访问表 T,并请求页面 X 上的排他(非可共享的)锁。
- 应用程序 B 访问表 T,并请求页面 Y 上的排他锁。
- 然后,应用程序 A 请求页面 Y 上的锁,同时仍然持有页面 X 上的排他锁。应用程序 A 将被挂起,因为应用程序 B 具有页面 Y 上的排他锁。
- 然后,应用程序 B 请求页面 X 上的锁,同时仍然持有页面 Y 上的排他锁。应用程序 B 将被挂起,因为应用程序 A 具有页面 X 上的排他锁。
- 这是一种僵持情况。应用程序 A 和 B 都无法继续工作。
在一段预设的时间间隔之后(请参阅标题 DEADLOCK TIME 下面的讨论),DB2 UDB 将终止当前工作单元,因为某个应用程序陷入死锁状态(通常为所做工作最少的应用程序)。这将释放终止程序所持有的锁,并允许剩余的应用程序继续下去。 DB2 UDB 将向被终止的应用程序的 SQLCA 发送描述性的错误消息和信息。
实用程序和命令的并发机制
当 SQL 应用程序使用事务锁来控制对 DB2 UDB 对象的并发访问时,DB2 UDB 实用程序和命令可以通过其他方法访问 DB2 UDB 对象,即 声明(claim)、放弃(drain) 和 兼容性规则。
声明是指通知 DB2 UDB 当前正在访问某个特定对象。提交之后,该声明通常不会继续存在。为了在下一工作单元里访问 DB2 UDB 对象,应用程序需要进行新的声明。声明通知 DB2 UDB 当前正关注某个 DB2 UDB 对象,或是该对象上存在活动。只要 DB2 UDB 对象上存在声明,在释放那些声明之前,就不能采取任何放弃(drain)。
放弃(drain)是指通过下列方式来访问 DB2 UDB 对象的动作:
- 阻止对对象进行任何新的声明。
- 等待释放对象上现有的所有声明。
DB2 UDB 对象上的放弃导致 DB2 UDB 停止(quiesce)所有当前正声明该资源的应用程序,其方式为允许它们到达提交点,但阻止它们(或任何其他的应用程序进程)进行新的声明。Drain 锁还阻止冲突进程同时放弃(drain)同一对象。
DB2 UDB 一般通过一组兼容性规则来控制实用程序的并发操作。如果两个实用程序不需要同时以不可兼容的模式访问相同的 DB2 UDB 对象,就将它们视为是“兼容的”。当一个实用程序作业开始时,DB2 UDB 就检查系统,查看当前是否有其他任何实用程序正处理同一 DB2 UDB 对象。如果该对象当前未被另一实用程序访问,或者如果另一执行实用程序是可兼容的,该实用程序才可以继续。
数据库设计考虑
取得高度并发性应该是指导 DB2 UDB 数据库初始设计的目标之一。在创建各个表的同时,可能要考虑许多影响并发性的功能。您可以在实现创建之后再添加某些功能(例如通过更改 DB2 UDB 对象),但另一些功能则无法添加,或者至少需要大量重复操作和/或打乱当前实现。因此,最好是一开始就考虑这些问题。
分区
我极力建议您将较大的表创建为分区表。一个分区表空间只包含单个分区表。要基于分区索引的键范围将该表分成多个分区。每个分区都是作为单独的数据集(dataset)来创建的,并且可以作为单独的实体由 DB2 UDB 加以处理。
对于大量的批处理操作(INSERT、UPDATE、DELETE),您可以将大型作业划分成较小的作业,利用该分区表结构。许多这些较小的作业可以并发运行(对不同的分区进行),以便减少整个批处理操作的占用时间,从而使另一应用程序进程可以更早地访问该表。
分区表以类似的方式使实用程序作业只作用于所选择的分区,从而允许其他作业或应用程序进程并发访问表中的其他分区。在许多情况下,DB2 UDB 实用程序本身就具有利用分区表的能力,而在其他情况下,它只为用户提供了设计其工作负载的选项,以便支持 DB2 UDB 数据的更高并发性级别。
锁升级
DB2 UDB 使用升级技术来平衡锁定性能开销的并发性需求。当一个应用程序进程持有单个表或表空间上的大量页面锁、行锁或 LOB 锁时,DB2 UDB 就获取该资源上的表或表空间锁,然后释放该资源上以前的页面锁、行锁或 LOB 锁。该过程称作 锁升级 。
如果一个使用分区锁定(带有 LOCKPART YES 的 CREATE 或 ALTER)的表上发生锁升级,那么,就只升级当前被锁定的分区,未锁定的分区仍旧未锁定。一旦表空间中发生了锁升级,那么就要用表空间锁来锁定随后访问的未锁定分区。
在执行应用程序时,DB2 UDB 首先使用页面锁或行锁,并且只要该进程访问相对较少的页面或行,还会继续这样做。当该应用程序访问许多页面或行时,DB2 UDB 将变为使用表锁、表空间锁或分区锁。调用锁升级的准确时间是由 LOCKSIZE 和 LOCKMAX.的值决定的。
LOCKSIZE
LOCKSIZE 是 CREATE/ALTER TABLESPACE 语句的选项,在应用程序进程访问表空间中的表时,控制 DB2 UDB 获取的何种类型的锁(即,它决定该锁的“大小”,这有时也称作锁粒度)。该选项的可以是 LOB、TABLESPACE、TABLE、PAGE、ROW 和 ANY。
CREATE TABLESPACE 语句的 LOCKSIZE 参数默认为 ANY。LOCKSIZE ANY 允许 DB2 UDB 选择锁大小。DB2 UDB 通常将 LOCKSIZE PAGE 用于非 LOB 的表,而将 LOCKSIZE TABLESPACE 用于 LOB 表。
我建议在创建表空间时使用该默认值,除非您有理由进行其他选择。如果您选择修改 LOCKSIZE,那么就要根据使用该表空间的应用程序的性能监控结果和并发性特点来做决定。
使用何种大小的锁
在 DB2 V4 中才开始可以使用行级锁。之前,数据页是最小的锁定单元。I/T 行业中的许多人都假定行锁是并发性问题的灵丹妙药,但实际上,它并不能解决所有的并发性问题。在经历许多锁等待、死锁和超时的环境中,它也许提供了较大的改进。但在其他情形下,DB2 UDB 可能在获取更多锁上消耗资源,同时无法成比例地提高并发性。
因为 IRLM 获取、维护和释放行锁所需的处理与页面锁需要的大致相同,所以关于指定锁大小的决定其实就是在较高的锁定开销与并发性的潜在提高之间进行权衡。
因此,至于是使用页面锁还是使用行锁,这取决于您的数据和应用程序的特点。如果您觉察到使用页面锁定级别的表空间的数据页上存在大量竞争,那么就请考虑使用行锁。通过在行级别而非页面级别上进行锁定,可以极大地减少与其他应用程序进程的竞争,特别在访问是在随机的情况下。
但是,如果多个应用程序正以不同的顺序更新某一页上的相同行,那么行锁导致的竞争甚至可能比页面的还要多。这是因为,通过页面锁,第二个以及随后的应用程序在访问该页面之前,都必须等待第一个应用程序完成,而它们就可能超时。通过行锁,多个应用程序可以同时访问同一页上的行,但如果它们试图访问相同的行集,就可能死锁。
使用 LOCKSIZE TABLESPACE 或 LOCKSIZE TABLE 之前,用户必须相当确定没有其他重要的应用程序进程需要并发访问该对象。在任何表上指定 LOCKSIZE ROW 之前,极为明智的做法就是先对增加锁开销来提高并发性是否值得进行估计。
小型表
如果一个 DB2 UDB 表兼具尺寸小和高使用率的特点,那么就考虑使用 LOCKSIZE ROW,特别是每个页面中有许多行的时候。此外,因为表十分小,锁粒度将不会给锁开销带来较大的性能影响。
DB2 UDB 对象和授权的分组
通常总是将与同一应用程序逻辑相关的表分组到一个数据库中。理想情况是,每一个应用程序进程都引用尽可能少的 DB2 UDB 数据库。而且,要设法给用户不同的授权 ID 来使用不同的 DB2 UDB 数据库。实际上,这将增加可能应用程序进程的数目,同时可能减少每个应用程序进程可以访问的数据库数目。因此一般说来,“组合相似的事物”和“分开不同的事物”。
应用程序设计考虑
正如初始数据库设计一样,早在开发新的 DB2 UDB 应用程序之时就开始考虑并发性问题也很重要。更好的做法是,在实现之前,就将并发性原则包含到生产中去。然而,有时候并发性的提高可能取决于较新的 DB2 UDB 版本中的新功能以及与之接合的其他系统。在这些情况下,可能需要修改现有的应用程序,以便提高并发性水平。而在其他情况下,可能要等并发性问题发展和增长到一定的时候,才有必要对应用程序进行大量修改。以下是一些用以评估在某一时刻是开发新的 DB2 UDB 应用程序,还是修改现有的 DB2 UDB 应用程序的选项。
尽可能晚地访问关键资源
例如,设计和编写应用程序,以便 SQL 语句获取和更新 DB2 UDB 数据的时间尽可能地接近提交点。注意该问题将减少锁定数据的时间总量,从而减少其他应用程序进程无法使用该数据的时间。
尽可能快地提交工作
通常,在达到数据一致性临界点之后,尽快发出 COMMIT 语句是一种较好的 SQL 编程技术。该实践将有助于避免不必要的锁竞争,甚至在只读应用程序中也应该采用该实践。类似地,我们建议应用程序中一检测出 SQL 故障,就发出 ROLLBACK 语句。这将阻止未成功的 SQL 语句不必要地、过长时间地锁定 DB2 UDB 资源。
尽可能快地关闭游标
尽可能快地关闭游标是另一个可以帮助提高并发性的编程实践。通过尽快发出 CLOSE CURSOR 语句,应用程序将释放那些锁以及它们所持有的 DB2 UDB 资源。
应用程序重试逻辑
我建议应用程序中包含重试逻辑,即当程序接收死锁或超时状况的指示时,该应用程序代码应重试该操作,甚至允许重试多次。这就使得应用程序有可能从该状况中恢复,而无需操作人员从外部干涉或提供帮助。SQLCA 中的字段 SQLERRD(3) 返回一个理由码,指示是否发生了死锁或超时。
以一致次序访问数据
在多数环境中,时常会发生不同的应用程序需要访问同一数据的情况。要在您的环境中规定应用程序以相同的次序访问数据。例如,要求所有应用程序按升序访问表 XYZ 中各行。如果那样,第一个访问表 XYZ 的应用程序就可能会延迟其他应用程序,但不会导致死锁的状况。以类似的方式,所有访问表 A、B 和 C 的应用程序都应按字母顺序来进行访问。
增加联机并发性
减少死锁和超时的一种方法就是按照上面所描述的,在整个系统中按照一致次序来访问 DB2 UDB 数据。而第二种方法则是在 UPDATE 和 DELETE 上使用 WHERE CURRENT OF 游标选项,而不是使用独立的 UPDATE 和 DELETE 语句。在争取获得联机 DB2 UDB 数据的最大并发性时,考虑的第三种选择就是在非活动或非高峰 OLTP 时期,尝试为联机表计划批处理活动。
参照约束
在 DB2 UDB 数据库中使用参照约束可以带来性能影响。父表中某行上的 DELETE 语句需要获取子表(或者,至少是其索引)上的锁。那么,那些子表的并发使用的可用性会由于那些锁而减小。该影响对于级联删除可能相当大,因为访问单行的单个删除操作可能需要扫描多个表中的多行数据。
您经常可以通过在外键上创建索引来减小性能影响。虽然并不强制外键上有索引,但是若不存在该索引,那么当从父表中删除一行时,DB2 UDB 就必须扫描整个子表。
可以潜在地改进该状况的另一个动作必须与组合相关表有关。如果频繁删除父表中的行,则可以考虑将所有的子表(以及它们的子表)置于同一表空间中。
BIND 选项
将“源” SQL(实际上就是准备好的 DBRM)转换成可执行代码的过程称作绑定,在某些方面类似于 COBOL 等过程语言的编译/链接-编辑过程。在绑定 SQL 代码时,可以指定许多选项,而这些选项可以极大地影响应用程序将经历的并发性程度。下面讨论了一些影响并发性的重要选项。
- ACQUIRE
参数 ACQUIRE 决定 DB2 UDB 用以获得应用程序所请求数据上的锁的时间。ACQUIRE(ALLOCATE) 指示 DB2 UDB 在分配应用程序计划时获取所有所需的表空间锁,但 ACQUIRE(USE) 将使 DB2 UDB 等到应用程序首次向该表发出 SQL 语句时,才获取那些锁。
您通常应该用 ACQUIRE(USE) 选项(这是包绑定的默认选项)来绑定计划。那通常也是并发性的最佳选择。ACQUIRE(ALLOCATE) 选项可以更好地保护优先级极高的作业免于死锁。为了偶尔适当地使用该选项,您可能需要考虑将所有 DBRM 直接绑定到该计划。
- RELEASE
DB2 UDB 释放它所持有的锁的时间是由 RELEASE 选项决定的。通过 RELEASE(COMMIT),DB2 UDB 指示 IRLM 在到达 COMMIT 点时释放锁,而通过 RELEASE(DEALLOCATE),DB2 UDB 将保留所有锁,直到终止程序并释放计划。
虽然不一定适用于所有状况和环境,但通常的建议是将 RELEASE(COMMIT) 用于 OLTP 应用程序,以及其他请求相对少量锁和应用程序进程的经过时间相当短的应用程序。对于批处理应用程序进程以及其他获取大量锁且/或执行时间长的应用程序进程来说,RELEASE(DEALLOCATE) 通常为更好的性能选项。
ACQUIRE/RELEASE 组合
对于 BIND 的 ACQUIRE/RELEASE 选项的通用建议是基于应用程序的特点选择组合值。有一种组合,即 ACQUIRE(ALLOCATE)/RELEASE(COMMIT) 是不允许的。DB2 BIND 进程会将该种说明标记为错误。其他三种可能的组合都有其合适的用法,下面将简要地进行描述。
- ACQUIRE(ALLOCATE)/RELEASE(DEALLOCATE)
ACQUIRE(ALLOCATE)/RELEASE(DEALLOCATE) 可能对于运行几小时并访问不同表的批应用程序十分有用,因为该组合将有效地阻止死锁状况损耗一切处理。一开始运行程序,应用程序就会锁定所有所需资源。
该组合的不足就是它可能严重地减小并发性。 潜在地具有较高需求的资源的锁定时间可能比实际所需要长。同时,请注意 ACQUIRE(ALLOCATE) 禁用选择性的分区锁定,以至于如果您访问用 LOCKPART YES 定义的表空间,将会锁定所有分区。
- ACQUIRE(USE)/RELEASE(DEALLOCATE)
ACQUIRE(USE)/RELEASE(DEALLOCATE) 组合通常导致最为高效地使用处理时间。DB2 UDB 资源仅在应用程序需要它们时才被锁定。这样就减小了锁开销,因为在那之后,会将所获取的锁保留到终止应用程序之时。
该组合的不足就是相较于 ACQUIRE(ALLOCATE) 时,它可能导致更频繁地发生死锁状况。只有在实际的应用程序运行时,才按可预测的次序这样来作用所获取的锁。因此,可能发生更多死锁。
- ACQUIRE(USE)/RELEASE(COMMIT)
ACQUIRE(USE)/RELEASE(COMMIT) 碰巧就是 BIND 的默认组合,提供了最佳的并发性。DB2 UDB 资源仅在应用程序需要它时才被锁定,如果该应用程序包含许多不常使用的 SQL 语句,这就可能极其重要。
然而,其权衡就是,如果应用程序相当频繁地提交,就可能增加处理时间,那么该应用程序就必须再次重新获取锁。与上面的 ACQUIRE(USE)/RELEASE(DEALLOCATE) 组合一样,也可能增加死锁,因为只有在执行实际的程序时,才按可预测的次序获取所有的锁。