对多用户环境编程
本部分描述当您在多用户环境中工作时需要注意的几个编程问题。
如果您的数据库包含在单个用户工作站中,且不访问来自另一计算机的数据。则您的程序可任意修改数据。在所有其他情况下,您必须考虑一种可能性,即,在您的程序正在修改数据时,另一程序正在读取或修改同一数据。将这种情况描述为并发:同一时刻对相同数据的两处或多处独立的使用。本部分讨论并发、锁定和隔离级别。
本部分还描述语句高速缓存特性,它可减少每一会话的内存分配,并加速查询处理。语句高速缓存存储那些稍后在使用相同的 SQL 语句的不同的用户会话之中共享的数据。
并发和性能
在多编程系统中,要有好的性能,并发至关重要。为了保证某一时刻仅一个程序可使用数据,当访问序列化的数据时,处理会显著减慢。
锁定和完整性
除非对数据的使用作出控制,否则,并发可导致许多负面效果。程序可读取过时的数据,或可丢失所做的修改,即使表面上已经完成了它们。
要防止此类错误,数据库服务器强加一个锁定系统。锁定是程序可在数据块上放置的声明或保留。只要锁定数据,数据库服务器保证没有其他程序可修改它。当另一程序请求该数据时,数据库服务器或者让该程序等待,或者让其返回并报错。
锁定和性能
由于锁定序列化对一块数据的访问,因此,它减少并发;任何想要访问该数据的其他程序都必须等待。数据库服务器可在单个行、磁盘页、整个表或整个数据库上放置锁。(磁盘页可能保存多行,且一行可能需要多个磁盘页。)它越是放置锁,它锁定的对象越大,并发降得越低。锁越少,锁定的对象越小,并发和性能越高。
下列部分讨论您可如何使您的程序实现下列目标:
- 放置所有必要的锁以确保数据完整性。
- 锁定与前面的目标可能相一致的最少、最小的数据块。
并发问题
要理解并发的危险性,您必须从多个程序的方面考虑,每一程序以它自己的速度执行。 假设您的程序通过下列游标正在访存行:
EXEC SQL DECLARE sto_curse CURSOR FOR
SELECT * FROM stock
WHERE manu_code = 'ANZ';
将每一行从数据库服务器传送到程序都花费时间。在传送期间和传送之间,其他程序可执行其他数据块操作。大约在您的程序访存由那个查询产生的行的同一时刻,另一用户的程序可能执行下列更新:
EXEC SQL UPDATE stock
SET unit_price = 1.15 * unit_price
WHERE manu_code = 'ANZ';
换句话说,两个程序都在通读同一个表,一个正在访存某些行,而另一个正在更改相同的行。可能出现下列情况:
-
在您的程序访存它的第一行之前,其他程序完成了它的更新。
您的程序仅向您显示更新了的行。
-
在其他程序有机会更新它之前,您的程序访存了每行。
您的程序仅向您显示原始的行。
-
在您的程序访存某些原始的行之后,其他程序赶上并继续更新您的程序还要读取的一些行;然后,它执行 COMMIT WORK 语句。
您的程序可能返回的既有原始的行,也有更新了的行。
-
与第 3 种情况一样,除了在更新表之后,另一程序发出 ROLLBACK WORK 语句之外。
您的程序可向您显示的既有原始的行,也有在数据库中不再存在的更新了的行。
前两种可能性是无害的。在第 1 种可能的情况中,在您的查询开始之前完成了更新。更新是在一毫秒之前结束的,还是一周前结束的,并无差异。
在第 2 种可能的情况中,实际上,您的查询在更新开始之前完成。其他程序可能紧跟在您的程序之后只处理了一行,或它可能直到明晚才开始;这没有关系。
然而,后两种可能的情况对于某些应用程序的设计可至关重要。在第 3 种可能的情况下,查询返回的既有更新了的数据,也有原始数据。在某些应用程序中,那种结果可能是有害的。在其他程序中,诸如计算所有价格的平均值,根本无关紧要。
由于取消了它们的事务,如果程序返回一些在表中不可再找到的数据行,则第 4 种可能的情况是灾难性的。
当您的程序使用游标来更新或删除最后访存的数据时,需要引起另外的关注。下列的事件序列产生错误的结果:
- 您的程序访存该行。
- 另一程序更新或删除该行。
- 您的程序更新或删除 WHERE CURRENT OF cursor_name。
要控制诸如此类的事件,请使用数据库服务器的锁定和隔离级别特性。
锁定如何工作
GBase 8s 数据库服务器支持复杂的、灵活的锁定特性的集合,本部分中描述这些主题。
锁的种类
下表展示 GBase 8s 数据库服务器对不同情况支持的锁的类型。
锁类型 | 用途 |
---|---|
共享的 | 共享锁为了只读保留它的对象。它防止在该锁保留期间更改该对象。多个程序可在同一对象上放置共享锁。在以共享的模式锁定记录时,多个对象可读取该记录。 |
排他的 | 排他锁为了单个程序的使用保留它的对象。当程序打算更改该对象时,使用此锁。 您不可在存在任何其他种类锁的地方放置排他锁。在您放置排他锁之后,您不可在同一对象上放置另一锁。 |
可提升的(或更新) | 可提升的(或更新)锁有更新的意图。您仅可在不存在其他可提升锁或排他锁的地方放置它。您可在已有共享锁的记录上放置可提升锁。当程序要更改该锁定了的对象时,您可将该可提升锁提升为排他锁,但仅当该锁可能从可提升的更改为排他的时刻,该记录上没有包括共享锁在内的其他锁时。如果当设置了可提升锁时,共享锁在该记录上,则在将可提升锁提升为排他锁之前,您必须删除共享锁。 |
锁作用域
您可将锁应用于整个数据库、整个表、磁盘页、单个行或索引键值。正被锁定的对象的大小称之为锁的作用域(也称为锁颗粒度)。通常,锁的作用域越大,并发降得越低,但编程越简单。
数据库锁
您可锁定整个数据库。打开数据库的操作在数据库的名称上放置一共享锁。使用 CONNECT、DATABASE 或 CREATE DATABASE 语句打开数据库。只要程序有一数据库是打开的,则该名称上的共享锁防止任何其他程序删除该数据库或在其上放置排他锁。
下列语句展示您可能如何排他地锁定整个数据库:
DATABASE database_one EXCLUSIVE
如果没有其他程序已打开了那个数据库,则此语句成功。放置该锁之后,其他程序不可打开该数据库,即使是读取也不可,因为它在该数据库名称上放置共享锁的尝试会失败。
仅当关闭该数据库时,才释放数据库锁。可使用 DISCONNECT 或 CLOSE DATABASE 语句来显式地执行那个操作,或通过执行另一 DATABASE 语句来隐式地执行。
由于锁定数据库导致那个数据库中的并发降低为零,因此,它使得编程非常简单;不可发生并发效果。然而,仅当其他程序不需要访问时,才应锁定数据库。在非高峰期间,对数据进行大量更改之前,通常使用数据库锁定。
表锁
您可锁定整个表。在某些情况下,数据库服务器自动地执行此操作。您还可使用 LOCK TABLE 语句来显式地锁定整个表。
LOCK TABLE 语句或数据库服务器可放置下列类型的表锁:
共享锁
任何用户都不可写表。在共享模式下,数据库服务器在表上放置一个共享锁,其通知其他用户不可执行更新。此外,数据库服务器为每个更新了的、删除了的或插入了的行添加锁。
排他锁
任何其他用户不可从该表读取或写该表。在排他模式下,数据库服务器仅在该表上放置一个排他锁,不论它更新多少行。排他的表锁防止该表的任何并发使用,因此,如果其他程序正在争夺对该表的使用,则可严重影响性能。然而,当您需要更新表中的大部分行时,请在表上放置排他锁。
使用 LOCK TABLE 语句来锁定表
事务告诉数据库服务器通过 LOCK TABLE 语句来使用表级别锁定。下列示例展示如何在表上放置排他锁:
LOCK TABLE tab1 IN EXCLUSIVE MODE
下列示例展示如何在表上放置共享锁:
LOCK TABLE tab2 IN SHARE MODE
在提供更大的并发时,您可为数据库服务器设置隔离级别来获得与共享的表锁相同的保护程度。
数据库服务器何时自动地锁定表
在数据库服务器对任何下列语句执行操作时,它总是锁定整个表:
ALTER FRAGMENT
ALTER INDEX
ALTER TABLE
CREATE INDEX
DROP INDEX
RENAME COLUMN
RENAME TABLE
完成该语句(或事务结束)会释放该锁。在某些查询期间也可自动地锁定整个表。
使用 ONLINE 关键字来避免表锁定
对于未以 IN TABLE 关键字选项定义的索引,当您使用 ONLINE 关键字来 CREATE 或 DROP 索引时,您可最小化索引了的表上的排他锁的持续时间。
在联机创建或删除索引时,不支持表上的 DDL 操作,但当发出了 CREATE INDEX 或 DROP INDEX 语句时,可完成并发了的操作。直到没有其他进程正在并发地访问该表时,才创建或删除指定的索引。然后,短暂地保持锁来写与该索引相关联的系统目录数据。这提高了系统的可用性,因为通过正在进行的会话或新会话仍可读该表。下列语句展示如何使用 ONLINE 关键字来避免在使用 CREATE INDEX 语句时的自动表锁定:
CREATE INDEX idx_1 ON customer (lname) ONLINE;
然而,对于使用 IN TABLE 关键字选项定义的“表中”索引,在包括 ONLINE 关键字的 CREATE INDEX 或 DROP INDEX 操作期间,该索引了的表保持锁定。其他会话尝试访问该锁定了的表时,会失败并发出这些错误之一:
107: ISAM error: record is locked.
211: Cannot read system catalog (systables).
710: Table (table.tix) has been dropped, altered or renamed.
行和键锁
您可锁定表的一行。在其他程序继续处理同一表的其他行时,程序可锁定一行或选择的多行。
行和键锁定不是缺省的行为。当您创建表时,您必须指定行级别锁定。下列示例创建带有行级别锁定的表:
CREATE TABLE tab1
(
col1...
) LOCK MODE ROW;
当您创建表时,如果指定 LOCK MODE 子句,则您可稍后使用 ALTER TABLE 语句来更改锁定模式。下列语句将保留表上的锁定模式更改为页级别锁定:
ALTER TABLE tab1 LOCK MODE PAGE
在某些情况下,数据库服务器必须锁定不存在的行。要这样做,数据库服务器在索引键值上放置一个锁。使用键锁与使用行锁一样。当表使用行锁定时,作为假想行上的锁来实现键锁。当表使用页锁定时,在包含该键或如果存在则包含该键的索引页上放置一键锁。
当您插入、更新或删除键(当您插入、更新或删除行时,自动地执行)时,数据库服务器在该索引的键上创建锁。
当您更新较少行数时,由于它们提高并发性,因此行和键锁通常提供最佳性能。然而,数据库服务器在获取锁时会造成一些开销。
当通过排他锁来锁定表中的一行或多行时,对其他用户的影响部分地依赖于它们的事务隔离级别。其隔离级别不是 Dirty Read 的其他用户可能遇到事务失败,这是由于在指定的时限内,未释放了排他锁。
对于 Committed Read 或 Dirty Read 隔离级别操作,这些操作尝试访问那些并发的会话已在其上设置了排他的行级别锁的表,为了降低锁定冲突的风险,可启用事务来读取锁定了的行中数据的最近提交的版本,而不是等待提交或回滚设置该锁的那个事务。启用对排他地锁定了的行的最后提交的版本的访问,可以采用几种方式来实现:
-
对于个别的会话,发出此 SQL 语句
SET ISOLATION TO COMMITTED READ LAST COMMITTED;
-
对于使用 Committed Read 或 Read Committed 隔离级别的所有会话,DBA 可将 USELASTCOMMITTED 配置参数设置为 'ALL' 或设置为 'COMMITTED READ'。
-
对于使用 Committed Read 或 Read Committed 隔离级别的个别的会话,任何用户都可发出带有 'ALL' 或 'COMMITTED READ' 的 SET ENVIRONMENT USELASTCOMMITTED 语句作为此会话环境选项的值。
-
对于使用 Dirty Read 或 Read Uncommitted 隔离级别的所有会话,DBA 可将 USELASTCOMMITTED 配置参数设置为 'ALL' 或设置为'DIRTY READ'。
-
对于使用 Dirty Read 或 Read Uncommitted 隔离级别的个别的会话,任何用户都可发出带有 'ALL' 或 'DIRTY READ' 的 SET ENVIRONMENT USELASTCOMMITTED 语句作为此会话环境选项的值。
仅当行级别锁定生效时,而不是当另一会话持有对整个表的排他锁时,此 LAST COMMITTED 才有用。对于任何 LOCK TABLE 语句在其上应用表级别锁的任何表,禁用此特性。请参阅《GBase 8s SQL 指南:语法》 中的 SET ENVIRONMENT 语句的描述,要获取关于对并发访问通过排他锁锁定其中一些行的表的此特性的更多信息,以及对于可支持此特性的表的种类限制的更多信息,请参阅《GBase 8s 管理员参考手册》中的 USELASTCOMMITTED 配置参数的描述。
页锁
数据库服务器以称为磁盘页的单位存储数据。一个磁盘页包含一行或多行。在一些情况下,最好锁定一个磁盘页,而不是锁定它上面的个别行。例如,对于要求更改大量行的操作,您可能选择页级别锁定,因为行级别锁定(每行一锁)的性价比可能不高。
当您创建表时,如果您未指定 LOCK MODE 子句,则对于该数据库服务器的缺省行为为页级别锁定。使用页锁定,数据库服务器锁定包含该行的整个页。如果您更新存储在同一页上的若干行时,数据库服务器对该页仅使用一个锁。
为所有 CREATE TABLE 语句设置行锁或页锁模式
对于所有新创建的表,GBase 8s 允许您为单个用户(每会话)或为多个用户(每服务器)将锁模式设置为页级别锁定或行级别锁定。您不再需要在每次使用 CREATE TABLE 语句创建新表时指定锁模式。
如果您想要以特定的锁模式创建您的会话内的每个新表,则您必须设置 IFX_DEF_TABLE_LOCKMODE 环境变量。例如,对于要以锁模式行创建的您的会话内的每个新表,请将 IFX_DEF_TABLE_LOCKMODE 设置为 ROW。要覆盖此行为,请使用 CREATE TABLE 或 ALTER TABLE 语句并重新定义 LOCK MODE 子句。
单用户锁模式
如果在您的会话中创建的所有新表都要求相同的锁模式,则设置单用户锁模式。请使用 IFX_DEF_TABLE_LOCKMODE 环境变量来设置单用户锁模式。例如,对于在您的会话内创建的每一个新表都以行级别锁定来创建,请将 IFX_DEF_TABLE_LOCKMODE 设置为 ROW。要覆盖此行为,请使用 CREATE TABLE 或 ALTER TABLE 语句并重新定义 LOCK MODE 子句。要获取关于设置环境变量的更多信息,请参阅《GBase 8s SQL 参考指南》。
多用户锁模式
通过为同一服务器上的所有用户指定锁模式,数据库管理员可使用多用户锁模式来创建更大的并发。然后,任何用户在那台服务器上创建的所有表都会有相同的锁模式。要启用多用户锁模式,请在启动数据库服务器之前设置 IFX_DEF_TABLE_LOCKMODE 环境变量,或设置 DEF_TABLES_LOCKMODE 配置参数。
优先顺序的规则
CREATE TABLE 或 ALTER TABLE 的锁定模式有下列优先顺序的规则,按从最高优先顺序到最低的顺序罗列:
- 使用 LOCK MODE 子句的 CREATE TABLE 或 ALTER TABLE SQL 语句
- 单用户环境变量设置
- 在该服务器环境中的多用户环境变量设置
- 配置文件中的配置参数
- 缺省行为(页级别锁定)
稀疏索引锁
当您将索引的锁模式从常规的更改为稀疏的锁模式时,在该索引上要求索引级别的锁,而不是项级别锁或页级别锁,这些是常规的锁。此模式减少索引上的锁调用的数目。
当您知道不会更改索引时,请使用稀疏索引模式;即,当在该索引上执行只读操作时。
请使用常规的锁模式来使数据库服务器在必要时在索引上放置项级别锁或页级别锁。当频繁地更新索引时,请使用此模式。
当数据库服务器执行命令来将锁模式更改为稀疏的时,在命令期间它需要在表上的排他锁。在数据库服务器切换到稀疏锁模式之前,当前正在使用更细颗粒度的锁的任何事务都必须完成。
智能大对象锁
CLOB 或 BLOB 列上的锁与行上的锁是分开的。仅当访问智能大对象时,才锁定它们。当您锁定包含 CLOB 或 BLOB 列的表时,不锁定智能大对象。如果访问是为了写,则以更新模式锁定智能大对象,且当实际的写发生时,将该锁提升为排他的。如果访问是为了读,则以共享的模式锁定智能大对象。数据库服务器识别事务隔离模式,因此,如果设置 Repeatable Read 隔离级别,则在事务结束之前,数据库服务器不释放智能大对象读锁。
当数据库服务器检索行并更新该行指向的智能大对象时,在更新它期间,仅排他地锁定智能大对象。
字节范围锁
您可锁定智能大对象的字节范围。字节范围锁允许事务有选择地仅锁定访问的那些字节,因此,写用户和读用户可同时访问同一智能大对象中不同的字节范围。
要获取关于如何使用字节范围锁的信息,请参阅您的《GBase 8s 性能指南》。
字节范围锁支持死锁检测。要获取关于死锁检测的信息,请参阅 处理死锁。
锁的持续时间
程序控制数据库锁的持续时间。当数据库关闭时,释放数据库锁。
依赖于数据库是否使用事务,表锁的持续时间有所不同。如果数据库不使用事务(即,如果不存在事务日志且您不使用 COMMIT WORK 语句),则保留表锁,直到通过执行 UNLOCK TABLE 语句移除它为止。
表锁、行锁和索引锁的持续时间依赖于您使用的 SQL 语句,并依赖于是否使用事务。
当您使用事务时,在事务结束时释放所有表锁、行锁、页锁和索引锁。当事务结束时,释放所有锁。
在修改时锁定
当数据库服务器通过更新游标访存行时,它在访存的行上放置可提升锁。如果此操作成功,则数据库服务器知道没有其他程序可改变那一行。由于可提升锁不是排他的,其他程序可继续读取该行。由于访存该行的程序可在它发出 UPDATE 或 DELETE 语句之前花费一些时间,或它仅可访存下一行,因此,可提升锁可提升性能。当到了修改行时,数据库服务器获取该行上的排他锁。如果已有可提升锁,则它将那个锁更改为排他的状态。
排他的行锁的持续时间依赖于是否在使用事务。如果未使用事务,则将修改了的行一写到磁盘就释放该锁。当在使用事务时,保留所有这些锁,直到事务结束为止。此操作防止其他程序使用那些可能回滚到它们的原始状态的行。
当在使用事务时,每当删除行时,就使用键锁。使用键锁防止发生下列错误:
- 程序 A 删除一行。
- 程序 B 插入有相同的键的一行。
- 程序 A 回滚它的事务,强制数据库服务器恢复它的删除了的行。
如何处理由程序 B 插入的行?
通过锁定索引,数据库服务器防止第二个程序插入行,直到第一个程序提交它的事务为止。
当前的隔离级别控制在数据库服务器读取不同的行时放置的锁,如下一部分中讨论的那样。
使用 SELECT 语句来锁定
数据库服务器放置的锁的类型和持续时间依赖于应用程序中的隔离设置,以及该 SELECT 语句是否在更新游标之内。
本部分描述不同的隔离级别和更新游标。
设置隔离级别
隔离级别是您的程序与其他程序的并发操作的隔离程度。数据库服务器提供隔离级别的选择,反映当程序读数据时,程序如何使用锁的不同的规则集。
要设置隔离级别,请使用 SET ISOLATION 或 SET TRANSACTION 语句。SET TRANSACTION 语句还允许您设置访问模式。要获取关于访问模式的更多信息,请参阅 使用访问模式来控制数据修改。
对比 SET TRANSACTION 与 SET ISOLATION
SET TRANSACTION 语句符合 ANSI SQL-92。此语句类似于 GBase 8s SET ISOLATION 语句;然而,SET ISOLATION 语句不符合 ANSI,且不提供访问模式。
下表展示您使用 SET TRANSACTION 与 SET ISOLATION 语句设置的隔离级别之间的关系。
SET TRANSACTION 相关联的 | SET ISOLATION |
---|---|
Read Uncommitted | Dirty Read |
Read Committed | Committed Read |
不支持 | Cursor Stability |
(ANSI) Repeatable Read Serializable | (GBase 8s) Repeatable Read (GBase 8s) Repeatable Read |
SET TRANSACTION 与 SET ISOLATION 语句之间的主要差异是,在事务内隔离级别的行为。对于一个事务,仅可发出 SET TRANSACTION 语句一次。请保证在那个事务期间打开的任何游标都有那个隔离级别(或访问模式,如果您正在定义访问模式的话)。在启动事务之后,您可使用 SET ISOLATION 语句在该事务内多次更改隔离级别。下列示例说明使用 SET ISOLATION 与使用 SET TRANSACTION 之间的差异。
SET ISOLATION
EXEC SQL BEGIN WORK;
EXEC SQL SET ISOLATION TO DIRTY READ;
EXEC SQL SELECT ... ;
EXEC SQL SET ISOLATION TO REPEATABLE READ;
EXEC SQL INSERT ... ;
EXEC SQL COMMIT WORK;
-- Executes without error
SET TRANSACTION
EXEC SQL BEGIN WORK;
EXEC SQL SET TRANSACTION ISOLATION LEVEL TO SERIALIZABLE;
EXEC SQL SELECT ... ;
EXEC SQL SET TRANSACTION ISOLATION LEVEL TO READ COMMITTED;
Error -876: Cannot issue SET TRANSACTION once a transaction has started.
ANSI Read Uncommitted 与 GBase 8s Dirty Read 隔离
最简单的隔离级别 ANSI Read Uncommitted 和 GBase 8s Dirty Read 实际上相当于没有隔离。当程序访存行时,它不放置锁,且不考虑任何东西;它只是从数据库复制行,而不考虑其他程序正在做什么。
程序总是收到完整的数据行。即使在 ANSI Read Uncommitted 或 GBase 8s Dirty Read 隔离之下,程序也从不看到行中是否更新了某些列而有些没更新。然而,使用 ANSI Read Uncommitted 或 GBase 8s Dirty Read 隔离的程序有时会在更新程序结束它的事务之前读取更新了的行。如果更新程序后来回滚它的事务,则读取程序处理从未真正存在的数据(在并发问题列表中的可能性编号 4。)
ANSI Read Uncommitted 或 GBase 8s Dirty Read 是最有效率的隔离级别。 读取程序从不等待,且从不使另一程序等待。 它是任何下列情况下首选的级别:
- 所有表都是静态的;即,并发程序仅读取数据,且从不修改数据。
- 在排他锁中保持表。
- 仅一个程序正在使用该表。
ANSI Read Committed 与 GBase 8s Committed Read 隔离
当程序请求 ANSI Read Committed 或 GBase 8s Committed Read 隔离级别时,数据库服务器保证它从不返回未提交到数据库的行。此操作防止读取未提交的或后来回滚的数据。
ANSI Read Committed 或 GBase 8s Committed Read 实现简单。在它访存行之前,数据库服务器测试来确定更新进程是否在该行上放置了锁;如果没有,则它返回该行。由于已被更新了的(但还未提交的)行在它们上面有锁,因此,此测试确保程序不读取未提交的数据。
ANSI Read Committed 或 GBase 8s Committed Read 实际上不在访存了的行上放置锁,因此,此隔离级别几乎与 ANSI Read Uncommitted 或 GBase 8s Dirty Read 一样有效率。当将每一行数据作为独立的单元处理,未引用同一表或其他表中的其他行时,适于使用此隔离级别。
然而,如果由于并发会话持有行上的共享锁,导致放置该测试锁的尝试不成功,则在 ANSI Read Committed 或 GBase 8s Committed Read 会话中可发生锁定冲突。要避免等待并发进程(通过提交或回滚来)释放共享锁, GBase 8s 支持 Committed Read 隔离级别的 Last Committed 选项。当此 Last Committed 选项生效时,由另一会话的共享锁导致该查询返回该行的最近提交了的版本。
还可通过将 USELASTCOMMITTED 配置参数设置为 'COMMITTED READ' 或设置为 'ALL',或通过当用户连接到数据库时设置 sysdbopen( ) 过程中的 SET ENVIRONMENT 语句中的 USELASTCOMMITTED 会话环境选项,来激活 Last Committed 特性。要获取关于 ANSI Read Committed 的 Last Committed 选项,或 GBase 8s Committed Read 隔离级别的更多信息,请参阅《GBase 8s SQL 指南:语法》中的 SET ISOLATION 语句的描述。要获取关于 USELASTCOMMITTED 配置参数的信息,请参阅《GBase 8s 管理员参考手册》。
GBase 8s Cursor Stability 隔离
仅可随同 GBase 8s SQL 语句 SET ISOLATION 使用下一级别 Cursor Stability。
当 Cursor Stability 生效时,GBase 8s 在访存的最新的行上放置锁。它为普通的游标放置共享锁,或为更新游标放置可提升锁。一次仅锁定一行;即,每一次访存一行,释放前一行上的锁(除非更新那一行,在此情况下,保持该锁直到事务结束为止。)由于 Cursor Stability 一次仅锁定一行,因此,它对并发的限制低于表锁或数据库锁。
Cursor Stability 确保在程序检测行时,不更改它。当程序更新基于从该行读取的数据的某个其他的表时,这样的行稳定性非常重要。由于 Cursor Stability,程序保证更新是基于当前的信息的。它防止使用陈旧数据。
下列示例说明 Cursor Stability 隔离的有效使用。根据演示数据库,程序 A 想要为制造商 Hero (HRO) 插入新的库存商品。与此同时,程序 B 想要删除制造商 HRO 以及所有与它相关联的库存。可发生下列事件的序列:
- 在 Cursor Stability 之下操作的程序 A 从 manufact 表访存 HRO 行来获取制造商代码。此操作在该行上放置共享锁。
- 对于那一行,程序 B 发出 DELETE 语句。由于该锁,数据库服务器让该程序等待。
- 程序 A 使用它从 manufact 表获得了的制造商代码在 stock 表中插入新行。
- 程序 A 在 manufact 表上关闭它的游标,或读取不同的行,释放它的锁。
- 程序 B 从等待中被释放,完成该行的删除,并继续删除那些使用制造商代码 HRO 的 stock 的行,包括程序 A 刚刚插入的行。
如果程序 A 使用较低级别的隔离,则会发生下列序列:
- 程序 A 读取 manufact 表的 HRO 行来获取制造商代码。未放置锁。
- 对于那一行,程序 B 发出 DELETE 语句。操作成功。
- 程序 B 删除使用制造商代码 HRO 的所有 stock 行。
- 程序 B 结束。
- 程序 A 不知道该 HRO 行的它的副本现在是无效的,使用制造商代码 HRO 插入新的 stock 行。
- 程序 A 结束。
最后,在 stock 中出现一行,在 manufact 中没有与之相匹配的制造商代码。而且,程序 B 显然有问题;它未删除本应删除的行。使用 Cursor Stability 隔离级别可防止这些后果。
即使使用 Cursor Stability,重新安排前面的场景也可能失败。只需要让程序 B 以与程序 A 相反的序列对表操作。如果在程序 B 移除 manufact 的行之前,从 stock 删除它,则任何程度的隔离都不可防止出错。每当可能发生这种错误时,所有涉及到的程序都必须使用相同的访问顺序。
ANSI Serializable、ANSI Repeatable Read 和 GBase 8s Repeatable Read 隔离
如果需要 ANSI Serializable 或 ANSI Repeatable Read,则提供单个称为 GBase 8s Repeatable Read 的隔离级别。这在逻辑上等同于 ANSI Serializable。由于 ANSI Serializable 比 ANSI Repeatable Read 更有约束性,因此,当需要 ANSI Repeatable Read 时,可使用 GBase 8s Repeatable Read(虽然 GBase 8s Repeatable Read 在此上下文中比所需的具有更强的约束性)。
Repeatable Read 隔离级别要求数据库服务器在程序检测和访存的每行上都放置锁。对于普通游标放置共享的,对于更新游标放置可提升的。在检测每一行时单独地放置锁。直到该游标关闭或事务结束,才释放它们。
Repeatable Read 允许使用滚动游标的程序多次读取选择了的行,并确保在读取之间不修改或删除它们。(SQL 编程 描述滚动游标。)没有更低的隔离级别保证行依然存在且在第二次读取它们时保持不变。
Repeatable Read 隔离放置最多的锁且保持它们的时间最长。因此,它是降低并发最多的级别。如果您的程序使用此隔离的级别,则请仔细考虑它放置多少锁,保持它们多长时间,以及对其他程序可产生哪些影响。
除了对并发的影响,大量的锁还可是个问题。数据库服务器在锁定表中,按每一程序记录锁的数目。如果超出锁的最大数目,则锁表填满,且数据库服务器不可放置锁。返回一错误代码。管理 GBase 8s 数据库服务器系统的人员可检测该锁定表,并当过度使用它时通知您。
在缺省情况下,在符合 ANSI 的数据库中,将隔离界别设置为 Serializable。需要 Serializable 隔离级别来确保根据 SQL 的 ANSI 标准执行操作。
更新游标
更新游标是一种特殊的游标,当可能潜在地更新行时,应用程序可使用它。要使用更新游标,请在您的应用程序中执行 SELECT FOR UPDATE。更新游标使用可提升锁;即,当应用程序访存行时,数据库服务器放置更新锁(意味着其他用户仍可查看该行),但当应用程序使用更新游标和 UPDATE...WHERE CURRENT OF 更新该行时,将锁更改为排他锁。
使用更新游标的优势在于,您可查看该行,并确信在您查看它时以及您更新它之前,其他用户不可使用更新游标来更改它或查看它。
提示: 在符合 ANSI 的数据库中,由于任何选择游标的行为都与更新游标一样,因此更新游标不是必需的。
下图中的伪代码展现数据库服务器何时使用游标来放置和释放锁。
图: 为更新放置的锁
保留更新锁
如果用户有低于 Repeatable Read 的隔离级别,则一旦从游标访存下一行,数据库服务器就释放放置在行上的更新锁。当您设置任何下列隔离级别时,使用此特性,您可使用 RETAIN UPDATE LOCKS 子句来保留更新锁,直到事务的结束为止:
- Dirty Read
- Committed Read
- Cursor Stability
此特性允许您避免 Repeatable Read 隔离级别的开销或暂时避开诸如行上的假更新。当打开 RETAIN UPDATE LOCKS 特性,且在 SELECT...FOR UPDATE 语句的访存期间在行上隐式地放置更新锁时,直到事务的结束,才释放更新锁。使用 RETAIN UPDATE LOCKS 特性,仅保持更新锁,直到事务的结束为止,而 Repeatable Read 隔离级别同时保持更新锁和共享锁,直到会话的结束为止。
下列示例展示当您将隔离级别设置为 Committed Read 时,如何使用 RETAIN UPDATE LOCKS 子句。
SET ISOLATION TO COMMITTED READ RETAIN UPDATE LOCKS
要关闭 RETAIN UPDATE LOCKS 特性,请不要使用 RETAIN UPDATE LOCKS 子句来设置隔离级别。当您关闭该特性时,未直接地释放更新锁。然而,从此时起,后续的访存释放紧前的访存的更新锁,而不是更早的访存操作的更新锁。关闭的游标释放当前行上的更新锁。
要获取关于当您指定隔离级别时,如何使用 RETAIN UPDATE LOCKS 特性的更多信息,请参阅《GBase 8s SQL 指南:语法》。
使用某些 SQL 语句发生的排他锁
当您执行 INSERT、UPDATE 或 DELETE 语句时,数据库服务器使用排他锁。排他锁意味着任何其他用户都不可更新或删除该项,直到数据库服务器移除该锁为止。此外,任何其他用户都不可查看该行,除非他们正在使用 Dirty Read 隔离级别。
数据库服务器移除排他锁的时刻,依赖于该数据库是否支持事务日志记录。
要获取关于这些排他锁的更多信息,请参阅使用 INSERT、UPDATE 和 DELETE 语句加上的锁定。
锁类型的行为
GBase 8s 数据库服务器在内部的锁表中存储锁。当数据库服务器读行时,它检查该行或它的相关联的页、表或数据库是否罗列在该锁表中。如果它在锁表中,则数据库服务器还必须检查锁类型。锁表可包含下列类型的锁。
锁名称 | 描述 | 通常放置该锁的语句 |
---|---|---|
S | 共享锁 | SELECT |
X | 排他锁 | INSERT、UPDATE、DELETE |
U | 更新锁 | 在更新游标中的 SELECT |
B | 字节锁 | 更新 VARCHAR 列的任何语句 |
此外,锁表可能存储意向锁。意向锁可为意向共享的(IS)、意向排他的(IX)或意向共享排他的(SIX)。意向锁是当需要锁定较低颗粒度对象时,数据库服务器(锁管理器)放置在较高颗粒度对象上的锁。例如,当用户以“共享的”锁定模式锁定行或页时,数据库服务器在该表上放置 IS(意向共享的)锁来提供立即的检查,检查没有其他用户持有该表上的 X 锁。在此情况下,仅在该表上放置意向锁,而不是放置在行或页上。仅可以行、页或表级别放置意向锁。
用户不对意向锁进行直接的控制;锁管理器在内部管理所有的意向锁。
下表展示如果另一用户(或数据库服务器)持有某类型的锁,则用户(或数据库服务器)可放置哪些锁。例如,如果一个用户持有对某项的排他锁,另一用户请求任何种类的锁(排他的、更新或共享的)都会收到错误。此外,如果用户持有对一个项的排他锁,则数据库服务器不能在该项上放置任何意向锁。
持有 X 锁 | 持有 U 锁 | 持有 S 锁 | 持有 IS 锁 | 持有 SIX 锁 | 持有 IX 锁 | |
---|---|---|---|---|---|---|
请求 X 锁 | 否 | 否 | 否 | 否 | 否 | 否 |
请求 U 锁 | 否 | 否 | 是 | 是 | 否 | 否 |
请求 S 锁 | 否 | 是 | 是 | 是 | 否 | 否 |
请求 IS 锁 | 否 | 是 | 是 | 是 | 是 | 是 |
请求 SIX 锁 | 否 | 否 | 否 | 是 | 是 | 否 |
请求 IX 锁 | 否 | 否 | 否 | 是 | 否 | 是 |
要获取关于锁定如何影响性能的信息,请参阅您的《GBase 8s 性能指南》。
使用访问模式来控制数据修改
GBase 8s 数据库服务器支持访问模式。对于事务内的行,访问模式影响读和写的并发,并使用 SET TRANSACTION 语句设置访问模式。您可使用访问模式来控制共享的文件之中的数据修改。
在缺省情况下,事务是读写的。如果您指定事务为只读,则那个事务不可执行下列任务:
- 插入、删除或更新表行
- 创建、改变或删除任何数据库对象,诸如模式、表、临时表、索引或存储例程。
- 授予或撤销权限
- 更新统计信息
- 重命名列或表
只读访问模式禁止更新。
您可在只读事务中执行存储例程,只要该例程不试图执行任何受限制的操作。
要获取关于如何使用 SET TRANSACTION 语句来指定访问模式的信息,请参阅《GBase 8s SQL 指南:语法》。
设置锁模式
当您的程序遇到锁定了的数据时,锁模式决定会发生什么情况。当程序尝试访存或修改锁定了的行时,会发生下列情况之一:
- 数据库服务器立即将 SQLCODE 或 SQLSTATE 中的错误代码返回到程序。
- 数据库服务器暂挂程序,直到放置了该锁的程序移除锁为止。
- 数据库服务器暂挂程序一段时间,然后,如果移除该锁,则数据库服务器将错误返回代码发送至该程序。
使用 SET LOCK MODE 语句来在这些结果中选择。
等待锁
当用户遇到锁时,数据库服务器的缺省行为是将错误返回到应用程序。如果您愿意无限期地等待锁(对许多应用程序来说,这是最好的选择),则可执行下列 SQL 语句:
SET LOCK MODE TO WAIT
当设置此锁模式时,您的程序通常忽略其他并发程序的存在。当您的程序需要访问另一程序已锁定了的行时,它等待,直到移除该锁位置,然后再处理。在大多数情况下,觉察不到该延迟。
您还可等待指定的秒数,如下例所示:
SET LOCK MODE TO WAIT 20
不等待锁
等待锁的缺点是等待时间可能变长(虽然正确设计的应用程序应短暂地保持它们的锁)。当不可接受长时间延迟的可能性时,程序可执行下列语句:
SET LOCK MODE TO NOT WAIT
当程序请求锁定了的行时,它立即收到错误代码(例如,错误 -107 Record is locked),并终止当前的 SQL 语句。该程序必须回滚它的当前事务并重试。
当程序启动时,初始的设置为不等待。如果您正在交互地使用 SQL 并看到与锁定相关的错误,则请将锁模式设置为等待。如果您正在编写程序,请考虑使其成为程序首先执行的嵌入式 SQL 语句之一。
等待有限的时间
您可使用下列语句要求数据库服务器设置等待的上限:
SET LOCK MODE TO WAIT 17
此语句对任何等待的长度放置 17 秒的上限。如果在那个时间内未移除锁,则返回错误代码。
处理死锁
死锁是一对程序阻塞彼此的进度的情况。每一程序对其他程序想要访问的一些对象有锁。仅当所有相关的程序将它们的锁模式都设置为等待锁时,才发生死锁。
当仅涉及单个网络服务器上的数据时,GBase 8s 数据库服务器立即检测到死锁。通过将错误(错误 -143 ISAM error: deadlock detected)返回给要请求锁的第二个程序,它防止发生死锁。如果程序将它的锁模式设置为不等待锁,则程序收到该错误代码。如果即使程序将锁模式设置为等待之后,它还收到与锁相关的错误代码,则您知道是由于即将发生的死锁。
处理外部的死锁
在不同数据库服务器上的程序之间也可发生死锁。在此情况下,数据库服务器不可立即检测到死锁。(完善的死锁检测需要在网络中的所有数据库服务器之间大量的通信。)而是每一数据库服务器对于程序可等待的时间量设置一个上限,来在不同的数据库服务器上获得对数据的锁。如果到时间了,则数据库服务器假定发生死锁并返回一个与锁相关的错误代码。
也就是说,当涉及外部的数据库时,每个程序都运行最大锁等待时间。DBA 可为数据库服务器设置或修改该最大值。
简单的并发
如果您不确定如何在锁定与并发之间做出选择,则可使用下列指导方针:如果您的应用程序访问非静态表,且不存在死锁的风险,则当您成程序启动时(就在第一个 CONNECT 或 DATABASE 语句之后),让它执行下列语句:
SET LOCK MODE TO WAIT
SET ISOLATION TO REPEATABLE READ
请忽略来自两个语句的返回代码。请执行,就如同没有其他程序存在一样。如果未出现性能问题,则您不需再阅读这部分。
保持游标
当使用事务日志记录时,GBase 8s 保证在事务结束时,可回滚在事务内所作的一切。要可靠地处理事务,数据库服务器通常应用下列规则:
- 当事务结束时,关闭所有游标。
- 当事务结束时,释放所有锁。
对于支持事务的大多数数据库系统,用于可靠地处理事务的规则都是正常的。然而,存在一些情况,随同游标使用标准事务是不可能的。例如,在没有事务的情况下,下列代码正常工作。然而,当添加事务时,关闭游标与同时使用两个游标发生冲突。
EXEC SQL DECLARE master CURSOR FOR
EXEC SQL DECLARE detail CURSOR FOR FOR UPDATE
EXEC SQL OPEN master;
while(SQLCODE == 0)
{
EXEC SQL FETCH master INTO
if(SQLCODE == 0)
{
EXEC SQL BEGIN WORK;
EXEC SQL OPEN detail USING
EXEC SQL FETCH detail
EXEC SQL UPDATE WHERE CURRENT OF detail
EXEC SQL COMMIT WORK;
}
}
EXEC SQL CLOSE master;
在此设计中,使用一个游标来扫描表。选择了的记录用作更新不同的表的基础。问题在于,当将每一更新当做分开的事务处理时(如前一示例中伪代码所示),跟在 UPDATE 之后的 COMMIT WORK 语句关闭所有游标,包括主游标。
最简单的替代方案是将 COMMIT WORK 语句和 BEGIN WORK 语句分别移到最后一个语句和第一个语句, 这样,对整个主表的扫描就是一个大事务。将主表的扫描作为一个大事务来处理,有时是可能的,但如果需要更新许多行,它可变得不现实。锁的数目可能太大,并且在程序期间持有它们。
GBase 8s 数据库服务器支持的解决方案是将关键字 WITH HOLD 添加到主游标的声明。引用这样的游标作为持有游标,在不在事务结束时关闭。数据库服务器仍然关闭所有其他游标,且它仍然释放所有锁,但持有游标保持打开,直到显式地关闭它为止。
在您尝试使用持有游标之前,您必须确保了解此处描述的锁定机制,且您还必须了解正在并发地运行的程序。每当执行 COMMIT WORK 时,释放所有锁,包括放置在任何通过该持有游标访存的行上的任何锁。
对于对表的单向扫描,如果如您所愿地使用游标,则锁的移除无关紧要。然而,您可为任何游标指定 WITH HOLD,包括更新游标和滚动游标。在您这么做之前,您必须了解该事实的含义,即,在事务结束时,释放所有锁(包括对整个表的锁)。
SQL 语句高速缓存
SQL 语句高速缓存是一个特性,允许您将反复地执行的同一 SQL 语句存储在缓冲区中,以便在不同的用户中可重用这些语句,而不需要每个会话都分配内存。对于包含大量准备好的语句的应用程序,语句高速缓存可显著地提高性能。然而,当使用语句高速缓存来高速缓存那些一次准备多次执行的语句时,性能的提升就不太显著。
当为数据库服务器启用语句高速缓存时,请使用 SQL 为个别的数据库服务器打开或关闭语句高速缓存。下列语句展示如何使用 SQL 为当前的数据库会话打开高速缓存:
SET STATEMENT CACHE ON
下列语句展示如何使用 SQL 为当前的数据库会话关闭高速缓存:
SET STATEMENT CACHE OFF
当禁用高速缓存时,如果您尝试关闭或打开语句高速缓存,则数据库服务器返回错误。
要了解关于 SET STATEMENT CACHE 语句的语法的信息,请参阅《GBase 8s SQL 指南:语法》。要了解关于 STMT_CACHE 和 STMT_CACHE_SIZE 配置参数的信息,请参阅《GBase 8s 管理员参考手册》和您的《GBase 8s 性能指南》。要获取关于 STMT_CACHE 环境变量的信息,请参阅《GBase 8s SQL 参考指南》。
总结
每当多个程序并发地访问一个数据库(且当其中至少有一个可修改数据时,)所有程序必须允许在它们读数据时,另一程序可更改该数据的可能性。数据库服务器提供锁和隔离级别的机制,其通常允许程序运行,如同它们独占数据一样。
SET STATEMENT CACHE 语句允许您将反复地使用的相同的 SQL 语句存储在缓冲区中。当打开语句高速缓存时,数据库服务器存储相同的语句,因此可在不同的用户会话之中重用它们,而无需为每个会话都分配内存。