事务
理解数据一致性
什么是数据一致性?回答这个问题的最佳方法是通过研究示例。假定您的公司拥有多家连锁饭店,公司用一个数据库来跟踪每家饭店中的货物存储量。为了使货物采购过程更方便,数据库包含每个连锁店的库存表。每当一家饭店收到或用掉一部分货物时,与该饭店相应的库存表就会被修改以反映库存变化。
现在,假定从一家店调配若干瓶番茄酱到另一家店。为了准确地表示这一次库存调配,调出方饭店表中存储的番茄酱瓶数必须减少,而接收方饭店表中存储的番茄酱瓶数必须增加。如果用户减少了调出方饭店库存表中的番茄酱瓶数,但没有增加接收方库存表中的番茄酱瓶数,则数据就会变得不一致。此时所有连锁店的番茄酱的总瓶数不再准确了。
如果用户忘记了进行所有必要的更改(正如在前面的示例中一样),或者如果在用户进行更改的过程中系统崩溃了,又或者如果数据库应用程序由于某种原因过早地停止了,数据库中的数据都会变得不一致。当几个用户同时访问相同的数据库表时,也可能发生不一致。为了防止数据的不一致(尤其是在多用户环境中),DB2 的设计中集成了下列数据一致性支持机制:
事务
隔离级别
锁
事务和事务边界
事务(也称为工作单元)是一种将一个或多个 SQL 操作组合成一个单元的可恢复操作序列,通常位于应用程序进程中。事务的启动和终止定义了数据库一致性点;要么将一个事务中执行的所有 SQL 操作的结果都应用于数据库(提交),要么完全取消并丢弃已执行的所有 SQL 操作的结果(回滚)。
使用从 Command Center、Script Center 或 Command Line Processor 运行的嵌入式 SQL 应用程序和脚本,在可执行 SQL 语句第一次执行时(在建立与数据库的连接之后或在现有事务终止之后),事务就会自动启动。在启动事务之后,必须由启动事务的用户或应用程序显式地终止它,除非使用了称为自动提交(automatic commit) 的过程(在这种情况下,发出的每个单独的 SQL 语句被看作单个事务,它一执行就隐式地提交了)。
在大多数情况下,通过执行 COMMIT 或 ROLLBACK 语句来终止事务。当执行 COMMIT 语句时,自从事务启动以来对数据库所做的一切更改就成为永久性的了 —— 即,它们被写到磁盘。当执行 ROLLBACK 语句时,自从事务启动以来对数据库所做的一切更改都被撤消,而数据库返回到事务开始之前所处的状态。不管是哪种情况,数据库在事务完成时都保证能回到一致状态。
一定要注意一点:虽然事务通过确保对数据的更改仅在事务被成功提交之后才成为永久性的,从而提供了一般的数据库一致性,但还是需要用户或应用程序来确保每个事务中执行的 SQL 操作序列始终会导致一致的数据库。
COMMIT 和 ROLLBACK 操作的效果
正如在前面提到的,通常通过执行 COMMIT 或 ROLLBACK SQL 语句来终止事务。为了理解这些语句如何工作,研究一个示例是有帮助的。
如果按所示的顺序执行下列 SQL 语句:
清单 1. 由三个事务组成的简单工作负载
CONNECT TO MY_DB
CREATE TABLE DEPARTMENT (DEPT_ID INTEGER NOT NULL, DEPT_NAME VARCHAR(20))
INSERT INTO DEPARTMENT VALUES(100, 'PAYROLL')
INSERT INTO DEPARTMENT VALUES(200, 'ACCOUNTING')
COMMIT
INSERT INTO DEPARTMENT VALUES(300, 'SALES')
ROLLBACK
INSERT INTO DEPARTMENT VALUES(500, 'MARKETING')
COMMIT
这将创建一个名为 DEPARTMENT 的表,它的结构如下所示:
DEPT_ID | DEPT_NAME |
100 | PAYROLL |
200 | ACCOUNTING |
500 | MARKETING |
这是因为当执行第一个 COMMIT 语句时,创建名为 DEPARTMENT 的表并向 DEPARTMENT 表中插入两条记录,这两个操作都会变成永久性的。另一方面,当执行 ROLLBACK 语句时,删除插入 DEPARTMENT 表中的第三条记录,该表返回到执行插入操作之前所处的状态。最后,当执行第二个 COMMIT 语句时,插入到 DEPARTMENT 中的第四条记录成为永久性的,而数据库再次返回到一致状态。
正如可以从这个示例中看到的,提交或回滚操作只影响在这个操作所结束的事务内做出的更改。只要数据更改仍然未被提交,其他用户和应用程序通常就无法看见它们(也有例外情况,稍后我们将进行讨论),并可以通过执行回滚操作取消它们。但是,一旦数据更改被提交了,其他用户和应用程序就可以访问它们,并且再也不能通过回滚操作取消它们了。
不成功事务的效果
我们刚才已看到当通过 COMMIT 或 ROLLBACK 语句终止事务时会发生什么。但是,如果在事务完成前出现系统故障,那会发生什么情况呢?在这种情况下,DB2 数据库管理程序会取消所有未提交的更改,从而恢复数据库一致性(假定在事务启动时存在这样的一致性)。图 1 对比了成功的事务和在成功终止之前失败的事务的效果。
图 1. 对比成功的和不成功的事务
并发性和隔离级别
当多个用户访问同一数据库时会发生的现象
在单用户环境中,每个事务都是顺序执行的,而不会遇到与其他事务的冲突。但是,在多用户环境下,多个事务可以(而且常常)同时执行。因此每个事务都有可能与其他正在运行的事务发生冲突。有可能与其他事务发生冲突的事务称为交错的 或并行的 事务,而相互隔离的事务称为串行化 事务,这意味着同时运行它们的结果与一个接一个连续地运行它们的结果没有区别。在多用户环境下,在使用并行事务时,会发生四种现象:
丢失更新:这种情况发生在两个事务读取并尝试更新同一数据时,其中一个更新会丢失。例如:事务 1 和事务 2 读取同一行数据,并都根据所读取的数据计算出该行的新值。如果事务 1 用它的新值更新该行以后,事务 2 又更新了同一行,则事务 1 所执行的更新操作就丢失了。由于设计 DB2 的方法,DB2 不允许发生此类现象。
脏读:当事务读取尚未提交的数据时,就会发生这种情况。例如:事务 1 更改了一行数据,而事务 2 在事务 1 提交更改之前读取了已更改的行。如果事务 1 回滚该更改,则事务 2 就会读取被认为是不曾存在的数据。
不可重复的读:当一个事务两次读取同一行数据,但每次获得不同的数据值时,就会发生这种情况。例如:事务 1 读取了一行数据,而事务 2 在更改或删除该行后提交了更改。当事务 1 尝试再次读取该行时,它会检索到不同的数据值(如果该行已经被更新的话),或发现该行不复存在了(如果该行被删除的话)。
幻像:当最初没有看到某个与搜索条件匹配的数据行,而在稍后的读操作中又看到该行时,就会发生这种情况。例如:事务 1 读取满足某个搜索条件的一组数据行,而事务 2 插入了与事务 1 的搜索条件匹配的新行。如果事务 1 再次执行产生原先行集的查询,就会检索到不同的行集。
维护数据库的一致性和数据完整性,同时又允许多个应用程序同时访问同一数据,这样的特性称为并发性。DB2 数据库用来尝试强制实施并发性的方法之一是通过使用隔离级别,它决定在第一个事务访问数据时,如何对其他事务锁定或隔离该事务所使用的数据。DB2 使用下列隔离级别来强制实施并发性:
可重复的读(Repeatable Read)
读稳定性(Read Stability)
游标稳定性(Cursor Stability)
未提交的读(Uncommitted Read)
可重复的读隔离级别可以防止所有现象,但是会大大降低并发性的程度(可以同时访问同一资源的事务数量)。未提交的读隔离级别提供了最大的并发性,但是后三种现象都可能出现。
可重复读隔离级别
可重复读隔离级别是最严格的隔离级别。在使用它时,一个事务的影响完全与其他并发事务隔离:脏读、不可重复的读、幻像都不会发生。当使用可重复的读隔离级别时,在事务执行期间锁定该事务以任何方式 引用的所有行。因此,如果在同一个事务中发出同一个 SELECT 语句两次或更多次,那么产生的结果数据集总是相同的。因此,使用可重复的读隔离级别的事务可以多次检索同一行集,并可以对它们执行任意操作,直到由提交或回滚操作终止事务。但是,在事务存在期间,不允许其他事务执行会影响这个事务正在访问的任何行的插入、更新或删除操作。为了确保这种行为,锁定该事务所引用的每一行 —— 而不是仅锁定被实际检索或修改的那些行。因此,如果一个事务扫描了 1000 行,但只检索 10 行,则所扫描的 1000 行(而不仅是被检索的 10 行)都会被锁定。
那么在现实环境中可重复读隔离级别是如何工作的呢?假定您使用 DB2 数据库跟踪旅馆记录,包括房间预订和房价信息,还有一个基于 Web 的应用程序,它允许顾客按 “先到先服务” 的原则预订房间。如果旅馆预订应用程序是在可重复读隔离级别下运行的,当顾客扫描某个日期段内的可用房间列表时,您(旅馆经理)将无法更改那些房间在指定日期范围内的房价。同样,其他顾客也无法进行或取消将会更改该列表的预订(直到第一个顾客的事务终止为止)。但是,对于生成第一个顾客的列表时没有读取的任何房间记录,您可以修改房价。同样,其他顾客也可以进行或取消这些房间的预订。图 2 说明了这种行为。
图 2. 可重复读隔离级别的示例