快速深入地掌握和管理数据库系统-第六章

 
2009-01-13 来源:chinaunix.net
 

第六章 事务和并发控制

事务是数据库系统的逻辑处理单元,用户以事务的形式操作数据库中的数据。为实现事务之间的并发处理和可串行化调度,数据库系统使用了并发控制机制。了解系统的事务处理和并发控制机制,可以加强系统管理、有效地利用系统资源,保证系统更好、更稳定地运行。

本章讲述了事务的基本概念和特征,介绍了并发控制需要解决的问题,然后就两种常用的并发控制机制:锁机制和多版本机制进行了讲解,并就它们的管理和维护提出一些建议。本章在最后对常用数据库系统并发控制的管理和配置进行了介绍。

6.1 事务概述

6.1.1 事务的概念

从数据库用户的观点看,数据库上一些操作的集合通常被认为是一个独立单元。例如:从帐户A到帐户B的资金转帐是一次独立操作。在数据库系统中,这个操作需要由以下两个步骤来完成:

(1)从帐户A中减去金额

(2)向帐户B中增加对应金额

显然,这两个操作步骤要么全部发生,要么由于出错而全不发生。保证这一点非常重要,我们无法接受资金从帐户A转出而没有存入帐户B的情况。

事务(transaction),也被称为交易、工作单元(unit of work),是构成单一逻辑工作单元的操作集合。不论有无故障,数据库系统必须保证事务的正确执行——或者执行整个事务,或者属于该事务的操作一个也不执行。上面资金转帐的两个操作步骤就必须放在一个事务中来完成。此外,数据库系统还必须以一种能避免数据不一致的方式来管理事务的并发执行。

数据库系统中对数据的所有处理都以事务的形式提交,事务的操作集合中可以包含一个,也可以包含多个操作。用户在编写应用程序时,通常使用形如begin transaction和end transaction的标识语句,来界定事务的起始和终止。如果没有使用事务标识,缺省情况下数据库系统把单个操作看作一个事务。

和事务相关的其它概念,包括:事务提交、事务回滚、数据落实等。

事务提交,就是事务中所有操作都正确完成后,向系统和发起事务请求用户表明事务已经成功完成的过程。事务可以成功地提交,表明由该事务引起的所有更新已经作用于数据库。事务也可能由于系统故障等原因而提交失败,这就需要事务的回滚。

事务回滚,就是将事务中已经执行的操作反方向地执行,使数据库恢复到事务执行前的状态,就好象事务没有被运行过一样。在事务中操作处理失败或者事务没能成功提交,就需要回滚事务,以保证数据库中数据的一致性。

事务的更新处理都是在内存中完成。在事务被提交之前,内存中这些被更新的数据都没有得到最后确认,这些更新可能会发生,也可能由于事务失败而回退,我们称这些数据为未落实数据。一旦事务成功提交,这些更新被写入磁盘、得到永久性的确认,我们称之为数据落实。

6.1.2 事务的特征

为了保证数据完整性,数据库系统要求事务具有以下四个特征:原子性(atomicity)、一致性(consistency)、隔离性(isolation)、持久性(durability),简称为ACID特征。

1. 原子性

事务的原子性,是指整个事务在逻辑上是一个整体,是数据库系统中最小的操作单元,就好象组成生物体的最小单位——原子一样,不可以分割。

事务的原子性,要求由事务中所有操作引起的更新,在数据库中要么全部正确反映出来,要么全部不反映。

仍旧以我们前面提到过的这样一个事务——由帐户A向帐户B转帐——为例,该事务的执行只允许产生以下两种结果:

(1)事务执行成功。帐户A中金额减少,帐户B中金额增加,帐户A和B的总金额保持不变。

(2)事务执行不成功。帐户A和B的金额没有变化。

除此之外的任何结果,都不能被数据库系统所接受。然而,有许多因素可能导致该事务的执行结果,不是以上的两种情况,这主要是因为系统顺序执行事务中的所有操作。对该事务来说,数据库系统按照以下两个操作顺序执行:

(1)帐户A中金额减少

(2)帐户B中金额增加

在第1步操作完成后,数据库中数据处于以下状态:

(1)帐户A中金额减少

(2)帐户B中金额不变

(3)帐户A和B的总金额减少

在这一时刻,数据库系统中的数据并没有反映出现实世界的真实状态,我们把这种状态称为不一致状态。事务在运行过程中出现这种情况是不可避免的。在事务继续执行并最终完成后,数据的不一致状态就解决了。然而,如果在这一时刻系统出现故障,事务的处理被终止,事务中的更新就部分地作用于数据库,事务的原子性被破坏。

现有数据库系统使用事务日志来维护事务的原子性。在事务执行过程中,对事务的所有操作都要记录日志信息。如果由于各种原因事务没能正常完成,数据库系统就使用这些日志信息,进行事务回滚,使数据库恢复到事务执行前的状态,就好象事务没有被运行过一样,从而解决了数据的不一致性。对上面的例子来说,系统需要在事务回滚时向帐户A中增加金额,从而使帐户A和B的总金额保持不变。

2. 一致性

事务的一致性,是指事务对数据的更新,要真实地、正确地反映现实世界中的业务处理。这包含两个方面的含义:

(1)事务的完整性。事务中的操作要么全部完成,要么都不执行,这是通过事务的原子性来实现的。

(2)不能丢失事务。事务对数据所发生的改变,要切切实实地反映在数据库中,不能够丢失。

数据库中数据的正确、一致,是数据库被广泛使用和依赖的前提,其重要性不言而喻。一旦一致性被破坏,其数据就无法使用,数据库就失去了价值。

事务在执行过程中,不可避免会在某一时刻使数据库中的数据不一致,这是由于事务中的操作串行执行、有先有后引起的。保证数据的不一致状态不被用户看到,是数据库系统必须要解决的问题。

事务丢失会由于多个事务的并发执行而引起。在上面的例子中,如果在事务还没有执行对帐户B的更改之前,数据库系统又接收到另外一个事务,也需要更改帐户B中的金额(如:向帐户B中存入现金)。这两个事务的处理都是基于帐户B原有的金额进行,不论两个事务执行的先后顺序,也不论那一个事务首先完成,总有一个事务的更新丢失。帐户B中的金额只是反映最后完成事务的更改,首先完成事务的更改被覆盖了。

数据库系统的并发控制机制,用来实现多个事务之间的调度管理,避免了事务的丢失,我们将在并发控制这一节中进行详细的阐述。

3. 隔离性

事务的隔离性,是指多个事务并发执行,一个事务的执行不会对其它事务的执行产生影响,就好象它们是分开、按顺序执行的一样。

数据库中的数据为很多用户所共享,数据库系统要能够同时处理多个用户发送的请求。事务本身的处理特征,使得数据库系统能够做到这一点:

(1)事务由多个操作组成,一些操作涉及磁盘I/O处理,一些操作涉及CPU处理。

计算机系统中CPU与磁盘可以并行运作,因此I/O活动可以与CPU处理并行进行。利用CPU与I/O系统的并行性,多个事务可以并发执行。当一个事务在一个磁盘上进行读写时,另一个事务可在CPU上运行,而第三个事务又可以在另一个磁盘上进行读写。这样系统的吞吐量增加——即给定时间内执行的事务数增加。相应地,处理器与磁盘的利用率也提高。

(2)事务的执行时间有长有短,所包含的操作也各式各样。

如果事务串行执行,短事务可能得等待它前面的长事务完成,这可能导致难以预测的时延。由于系统中的事务一般是针对数据库中的不同部分进行操作,事务的并发执行,不仅使多个事务可以共享CPU周期与磁盘存取,而且还可以减少不可预测的事务执行时延。

当多个事务并发执行时,即使每个事务都正确执行,数据库的一致性也可能被破坏,上面讲到的事务丢失就是其中的一种情况,还有许多种情况都会造成数据的不一致。

数据库系统可以使用很多技术,来保证事务的并发执行不会破坏数据的正确性,这些技术将在并发控制一节中进行阐述。简单地来说,数据库系统对多个并发事务的调度要做到在逻辑上可串行化,即这些并发事务好象是按照某一个顺序串行执行的。

4. 持久性

事务的持久性,是指事务正确完成后,事务所作的更新需要长久地反映在数据库中,即使数据库系统出现各种软件、硬件故障。

事务处理时,所有的操作在内存中完成。如果系统失败,存放在内存中的数据就要丢失。因此要保证被更新的数据长久化,就必须将数据存入磁盘,这就是数据的落实。数据库系统可以采取以下措施,来保证事务的持久性:

(1)事务的所有更新,在事务结束前就写入磁盘。

(2)将事务操作的日志信息,在事务结束前存入磁盘。数据库系统在出现故障而重新启动后,系统依据日志信息,重新执行事务处理。

对第一种方法,将所有更新写入磁盘,事务才结束。系统需要更多的磁盘I/O操作,事务要等待I/O操作完成才能提交,要花费更多的时间。由于数据为很多用户所共享,最近被处理的数据有很大的可能会再次被使用,这种处理方式会不断地进行数据的磁盘读写,造成数据库系统的低效率。因此,现有数据库系统都没有采用这种方法。

现有数据库系统都是通过记录事务日志,来实现事务的持久性。在事务完成前,系统要求事务的所有日志信息都写入磁盘。如果系统失败而事务所做的更新还没有写入磁盘,系统在重新启动时就使用日志信息重新执行该事务。

由于磁盘I/O的操作速度很慢,要求日志信息写入磁盘之后,才能结束一个事务,同样会影响事务的处理速度。另外,系统以块为单位对磁盘进行读写,在交互式系统中,一个事务的日志信息可能很小,不能添满一个块,从而浪费磁盘空间。为了解决这些问题,一些数据库系统也提供折中的日志组写技术。一个事务在处理完成后,就立即提交,不要求该事务的日志信息写入磁盘。在系统已经执行了多个事务,或者存放日志的内存空间被写满后,数据库系统才将日志信息写入磁盘。

这种日志写方式提高了事务处理速度和用户的响应时间,但对事务的持久性构成了威胁。一旦系统失败,已经提交、但日志信息仍旧存放在内存中的事务,其更新将丢失。有关这种日志写方式的更详细信息,可参见第2.2.5一节。

6.1.3 事务的并发执行和调度

多个事务在数据库系统中并发执行,数据的一致性可能遭受到破坏,因此数据库系统必须控制各并发事务之间的相互作用。由于事务本身是保持数据一致性的一个逻辑单元,多个事务之间的串行执行必然能够保证整个数据库的数据一致性,因而我们要求数据库系统对事务集的并发处理,所使用调度的执行效果要等价于这些事务按某种串行顺序执行的效果。

事务并发执行所要求的串行化调度,可以通过并发控制机制来加以保证。任何数据库系统都有自己的并发控制机制,来实现事务集的串行化调度。

6.2 并发控制概述

数据库系统为多个用户所共享,每个用户都可以发出自己的请求。为了充分利用资源,提高运行效率,数据库系统采用多个事务并发执行的机制。为了避免事务之间的互相影响,保证数据的一致性,数据库系统要按照一定的方式调度和控制各个事务,这就是并发控制。

6.2.1 并发处理可能出现的问题

在数据库系统中,多个事务的并发运行、共享数据的处理方式,会给系统带来以下问题:

(1)丢失更新(lost update)

两个事务A和B,先后从数据库读取相同的记录进行更新,在事务A完成后事务B的更新覆盖了事务A的更新,从而事务A的更新丢失。其具体处理过程可见图6-1。

(2)存取未落实的数据(read uncommitted)

事务A更新数据库中的数据,在更新被落实之前,事务B读取了该数据,随后事务A由于失败而回退,该更新并没有成功,而事务B读取的却是更新后的内容。其具体处理过程可见图6-2。

(3)不可重复读(unrepeatable read)

事务A在整个事务的执行过程中需要两次读取同一个数据,在第一次的读取之后,事务B更新了该数据;事务A第二次读取该数据,得到的是事务B更新后的内容,从而事务A在执行过程中,对同一数据的两次读取得到不同的内容。其具体处理过程可见图6-3。

(4)幻像读现象(phantom read)

事务A在整个事务的执行过程中需要两次读取同一个区间范围内的数据(在表中,落在此区间范围内的记录,可能有一条、多条,也可能一条也没有),在第一次的读取之后,事务B向表中插入一条记录或者更改现有的一条记录,新增加记录或者被更改记录满足事务A读取的区间范围;事务A第二次读取该区间范围内的记录,结果将包含事务B刚刚插入或者更改的记录,从而事务A在执行过程中,对同一区间数据的两次读取得到不同的内容。其具体处理过程可见图6-4。

依据这些问题对系统的影响程度,从高到低依次为:丢失更新、存取未落实的数据、不可重复读、幻像读现象。而为解决这些问题要付出的系统代价,正好和前面的这个顺序相反,具体可见图6-5。

对用户来说,丢失更新是不可接受的,任何数据库系统都必须要解决好这个问题。存取未落实的数据,将数据库不一致的数据状态呈现在用户面前,一般情况下也是不允许的。但如果用户不要求数据的准确性,而只对数据库系统的性能感兴趣;或者数据库中的数据只是用作查询处理,就可以不解决此问题,也即允许读取未落实的数据。

对不可重复读、幻像读现象,只会出现在这样的事务中:一个事务需要对同一数据读取两次。这种事务出现的概率很低,而且完全可以通过其它的途径来避免。

6.2.2 可用的并发控制机制

数据库系统的并发控制机制,要能够解决由于事务的并发执行而带来的所有问题,实现事务集的串行化调度。数据库系统可以采用的并发控制机制,主要有以下这些:

(1)锁机制

(2)多版本机制

(3)时间戳机制

(4)有效性检查机制

任何一种并发控制机制,都首先要解决更新丢失问题。用户在使用具体的数据库系统时,可以根据自身应用系统的需要,设置数据库系统的并发控制能力,允许系统中出现不可重复读、幻像读现象,从而提高事务并发能力以及应用程序的效率。

我们常用的数据库系统:DB2、ORACLE、INFORMIX、SYBASE,其并发控制用到锁机制和多版本机制。因此下面我们就只对这两种机制进行讨论。至于其它的并发控制机制,感兴趣的读者可以参看相关数据库书籍。

6.3 锁机制

锁机制是一种并发控制机制,就是使用锁来实现事务的并发处理。锁机制定义了一系列规则,数据库系统利用这些规则,在事务的调度和执行过程中,决定什么时候、使用那种锁给数据加锁,又在什么时候释放数据上的锁。通过锁,系统限制了多个事务之间对数据的竞争。事务也只有获得锁,才能开始相关的数据处理。

一般而言,对数据的查询处理,事务对数据加共享锁,允许其它事务同时访问这些数据;对数据的更新处理,事务要对数据加排它锁,禁止其它事务同时访问或者更新这些数据。由于数据查询是数据库系统中最经常要处理的工作,因此锁机制最大限度地提高了系统的并发能力。对于锁机制,我们将从锁的类型、粒度、授予、转换、升级、等待和超时等几个方面进行阐述。

6.3.1 锁的类型

数据库系统使用锁来限制事务对数据的访问。任何事务在访问数据前,都要给数据加锁。事务能否被授予所申请的锁,与数据上是否存在锁以及锁的类型有关。

在数据库系统中,有以下两类比较常用的锁:

(1)共享锁(shared lock)

如果事务只是查询数据的内容,就可以申请共享锁。获得数据共享锁的事务,可以读取数据的内容,但不能更改。由于数据只是用作读取,因此一个数据上可以拥有多个共享锁,多个事务可以同时查询该数据的内容。

(2)排它锁(exclusive lock)

如果事务要更改数据的内容,就必须申请排它锁。获得数据排它锁的事务,可以更改数据的内容。任一时刻,一个数据上只能有一个排它锁,只允许一个事务更改数据的内容。

排它锁具有严格的使用限制,它和共享锁、其它的排它锁之间是互斥的。事务要成功地给数据加上排它锁,该数据上就不能存在共享锁、其它的排它锁;事务在获得数据上的排它锁之后,其它事务就不能再在数据上加共享锁、排它锁。

数据库系统不仅仅包含以上两种锁,在具体的实现时会根据需要创建多种锁。在下面一节中,我们会再讲述两种常用的锁:意向锁(intent lock)和更新锁(update lock)。

6.3.2 锁的粒度

数据库系统在运行时可能需要维护很多的锁,不同锁的作用范围、影响到的数据量大小也不一样。例如:一个记录上的共享锁,只作用于该记录;而一个表上的共享锁,相当于为表中的所有记录都加上共享锁。我们使用锁的粒度(lock granularity)来说明锁的作用范围。依据数据库的结构层次,可以将锁的粒度从高到低依次划分为:数据库、表、页、记录。

在数据库级别上加锁,数据库中的所有表、索引、记录等都被锁定。这种锁定方式虽说提高了单个事务的处理能力,但严重地影响系统的并发处理。

在记录级别上加锁,就是为事务要处理的每一条记录都分别加上锁。使用此级别的锁定,数据库系统有更大的并发处理能力,但要花费更多的系统资源去管理锁。

相应地,在表级别上加锁,就是锁定表中的每一条记录;在页级别上加锁,就是锁定数据页中的每一条记录。系统对这两个级别的锁管理以及它们对系统并发能力的影响,处于数据库和记录锁级别之间。

事务到底使用那种级别上的锁定,应当根据事务要处理的数据量来决定。例如:一个事务只是更改表中的一条记录,使用记录级别的锁定将是比较好的选择;而如果一个事务需要更改表中大部分、甚至整个表中的数据,那么使用表级别上的锁定会比较好。

在此,我们引入另外两种比较常用的锁:意向锁、更新锁。

1. 意向锁

由于锁定可以在多个级别上实现,数据库系统需要考虑的一个问题是:在一个级别上申请给数据加锁,该如何判断是否可以授予呢?

例如:申请一个表的排它锁,该锁能够被授予,必须满足以下条件:

(1)在该表上,不存在表级别上的共享锁、排它锁。

(2)在该表的所有数据页上,不存在页级别上的共享锁、排它锁。

(3)在该表的所有记录上,不存在记录级别上的共享锁、排它锁。

只有这三个条件都满足,表级别上排它锁的申请才会成功。由此我们可以看出,要成功地在较高级别上给数据加锁,所有较低级别上一定不能存在相冲突的锁。为了能够快速地进行判断,数据库系统引入了一种新的锁类型:意向锁。

意向锁表明了一种意图,是在锁级别的高层次上,说明低层次所拥有的锁。事务在任何级别上申请加锁时,对这一级别之上的所有级别,都要加意向锁。由于记录是锁粒度的最低层次,因此在记录级别上不存在意向锁。有两种类型的意向锁:与共享锁相关联的意向锁,称为共享型意向锁;与排它锁相关联的意向锁,称为排它型意向锁。

例如:事务A查询一条记录,除了在该记录上加共享锁之外,还需要分别在数据库、包含该记录的表、包含该记录的数据页上加共享型意向锁;事务B更改整个表中的记录,需要在数据库、表上,分别加排它型意向锁、排它锁。

如果我们看到一个表级别上的排它型意向锁,就可以知道记录级别或者数据页级别上存在排它锁。如果需要进一步确认,就需要在数据页、记录级别上进行检查。

在使用意向锁之后,数据库系统就可以方便地判断一个锁的申请是否可以被授予。例如:要给一个表加排它锁,系统只需检查该表上是否存在共享锁、排它锁、共享型意向锁、排它型意向锁就可以了。只要不存在这些锁,锁就可以被授予;否则就不被授予。

在数据库系统中,还有另外一种意向锁:共享排它型意向(SIX)锁,它相当于以下两种锁的结合:(1)共享锁,(2)排它型意向锁。

对意向锁,用户在操作或者编写应用程序时,不能够直接使用。意向锁是系统决定如何加锁、加锁是否成功的工具,由系统自动进行管理,用户无法控制。

2. 更新锁

更新锁也表明了一种意图,它对数据的限制介于共享锁和排它锁之间。给数据加上更新锁,就表明事务随后可能需要更新该数据,需要在该数据上加排它锁。由于更新锁只是随后操作的一个事先说明,因此即使数据上存在更新锁,系统也允许其他事务查询该数据,给该数据加共享锁。但是,在该数据上加其它更新锁、排它锁是不允许的。

对于这样的事务:首先查询数据,再根据条件决定是否进行更新处理,就可以在数据查询时申请更新锁,在需要更新时再转换为排它锁。数据库系统引入更新锁,主要是基于事务并发处理能力的考虑,使数据被排它访问的时间尽可能的短。

更新锁一般只在记录级别上使用。使用游标(cursor)更改数据时,最好能使用更新锁。这样要更改的数据不会被其他的事务更改,同时又允许其它的事务读取该数据,提高了数据的利用率。

更新锁和共享锁互相兼容,和排它锁、其它更新锁互斥。不像共享锁和排它锁,更新锁只能在记录级别上实现。

6.3.3 锁的授予

当一个事务申请对某一数据加某一类型的锁时,如果该数据上不存在相冲突的锁(由其它事务所加)时,则锁被授予该事务。

在锁的授予时,要防止有些事务总是不能获得所需要的锁,以至于事务永远无法执行。例如:事务A要对数据加排它锁,如果数据上已经存在共享锁,则事务A等待。这时,事务B也需要在该数据上放置共享锁,由于共享锁互相兼容,事务B就获得该数据。如果随后连续有很多事务都在该数据上加共享锁,每一事务都只是运行一小段时间。这样,由于数据上总是存在共享锁,事务A就一直无法执行。

数据库系统必须要采取相关措施,避免这种情况的发生。一种可行的解决办法是,事务在申请对数据加锁时,数据库系统会进行条件检查。只有以下两个条件都满足之后,锁才会被授予:

(1)数据上不存在与所申请锁相冲突的锁。

(2)不存在给数据加锁而等待的事务。

6.3.4 锁的转换

更改事务已经拥有锁的类型,称为锁的转换(lock conversion)。在任一时间,一个事务对一个数据只能拥有一个锁定。当事务已经拥有数据的锁定,而进一步的处理需要比已拥有锁定更严格的锁定时,系统将进行锁的转换,用新的锁定类型取代现有的锁定。例如:事务拥有数据的共享或者更新锁,在要修改数据时,就转换为排它锁。

在锁的转换时,共享锁和排它型意向锁之间的转换是一种特殊的情况。这两种锁中的任何一个都不比另一个有更多的限制,因此如果一个事务在某数据上拥有其中的一个锁,而又申请另一个锁,系统就转换为共享排它型意向锁。

当然,锁的转换能否成功,还要看数据上是否存在其它不兼容的锁。如上面的例子,如果数据上还存在其它的共享锁,就无法给数据加上排它锁,锁的转换就不会成功。

在数据库系统中,锁的转换由系统自动完成,用户无从干预。然而,了解不同SQL语句在处理时需要的锁定类型,有助于用户设计和调整应用程序。

6.3.5 锁的升级

事务在处理数据时申请锁,在事务结束后释放锁。如果系统中有很多个事务,或者单个事务需要执行大批量的数据处理,系统中就会存在大量的锁。随着锁数量的增加,系统需要花费大量的CPU资源、内存空间来管理锁。在锁的数目达到一定的数量之后,系统的性能就受到严重影响。

为避免太多的锁,数据库系统采用锁升级(lock escalation)机制,用来减少系统中锁的数目。系统为锁升级设定一个阀值。一个事务拥有的锁超过这个阀值后,系统就将该事务的锁级别升级到一个更高的层次。例如:事务拥有记录锁,在记录锁的数目超过阀值后,系统就将该事务的记录锁升级到表锁。使用一个表锁代替所有的记录锁,从而使锁的数目减少。

锁的升级,虽然降低了系统的资源开销,但要影响到系统的并发处理能力。数据库管理员如果在系统中发现锁的升级,就应当考虑调整系统或者应用程序。

6.3.6 锁的等待和超时

一个事务在申请锁时,如果数据上存在不兼容的锁,事务就开始等待,直到不兼容锁被释放为止。有时候,这种锁等待时间可能很长。例如:正在等待的数据被另一个事务加锁,而发出这个事务的用户在离开工作台时,没有提交该事务。

然而,一个事务不能无限期地等待,因为该事务本身也可能拥有其他锁,它的长时间等待也会影响到其它事务对锁的使用。

锁超时是数据库系统的一种锁等待处理机制。在数据库系统中,可以设置事务等待锁的最长时间。一旦事务的锁等待时间达到该参数设置后,系统就终止事务的执行,回滚相关操作。这种锁的等待和超时机制,可以防止应用程序在异常情况下无限期地等待锁的释放。

6.3.7 两阶段封锁协议

对锁机制,保证事务可串行性的最常用协议是两阶段封锁协议。该协议要求每个事务分两个阶段提出加锁和解锁申请:

(1)增长阶段。事务可以获得锁,但不能释放锁。

(2)缩减阶段。事务可以释放锁,但不能获得锁。

一开始,事务处于增长阶段,事务根据需要获得锁。一旦该事务释放了锁,它就进入缩减阶段,不能再发出加锁请求。

两阶段封锁协议实现了事务集的串行化调度,但同时,一个事务的失败可能会引起一连串事务的回滚。为避免这种情况的发生,我们需要进一步加强对两阶段封锁协议的控制,这就是:严格两阶段封锁协议和强两阶段封锁协议。

严格两阶段封锁协议除了要求封锁是两阶段之外,还要求事务持有的所有排它锁必须在事务提交之后方可释放。这个要求保证未提交事务所写的任何数据,在该事务提交之前均以排它锁封锁,防止其他事务读取这些数据。

强两阶段封锁协议,要求事务提交之前不得释放任何锁。使用锁机制的数据库系统,要么使用严格两阶段封锁协议,要么使用强两阶段封锁协议。

两阶段封锁协议并不保证不会发生死锁,数据库系统必须采取其他的措施,预防和解决死锁问题。

6.3.8 隔离级别

我们前面已经提到,数据库系统的多个事务并发执行,带来了以下问题:丢失更新、存取未落实的数据、不可重复读、幻像读现象。对这些问题,需要通过并发控制机制来解决。锁机制,作为一种并发控制机制,使用两阶段封锁协议,来解决这些问题,其具体的处理方式如下:

(1)对丢失更新问题,锁机制要求:事务对数据的更新,必须申请排它锁,排它锁在事务提交后释放。

任何数据在被更新之前,必须要给它加排它锁;在数据更新完成、事务提交后,排它锁才会释放。由于排它锁之间互相排斥,数据一旦被一个事务加上排它锁,其他事务就不能获得该数据的排它锁,进而也不能更新数据。

正是数据排他性地访问,一个事务的更新不会覆盖另一个事务的更新,从而就不存在更新丢失的问题。使用锁机制,图6-1中的事务调度就改变为图6-6。

(2)对存取未落实的数据、不可重复读、幻像读现象,锁机制使用隔离级别来解决。

隔离级别的实现,就是给查询操作施加不同程度的共享锁。在SQL-92标准中,将隔离级别划分为四级。在不同的级别,查询操作所要求的锁、锁的持有时间不尽相同。级别越高,对锁的要求就越高,持有锁的时间也越长,相应地系统的并发处理能力就越低。用户可以根据自身的需要,选择应用系统的隔离级别。

这里需要说明的是,隔离级别实现的前提是:系统已经按照两阶段封锁协议,解决了更新丢失问题,即对数据的更新需要获得排它锁,排它锁在事务提交后释放。从以下对隔离级别的描述中,我们可以看出:隔离级别0和1,使用了严格两阶段封锁协议;而隔离级别2和3,使用了强两阶段封锁协议。在SQL-92标准中,数据库系统缺省的隔离级别为3。

1. 未落实的读(read uncommitted,0级)

在这一隔离级别,对数据的查询操作,不要求给数据加共享锁。

由于不需要加共享锁,系统的并发处理能力高,但同时事务能够读取未落实的数据,数据查询会出现不一致状况。事务的调度过程可见图6-7。

2. 落实的读(read committed,1级)

在这一隔离级别,对数据的查询操作,要求给数据加共享锁。在数据读取完成后,锁就释放,不需要等到事务完成。

由于查询操作需要对数据加共享锁,因此未落实、仍旧被排它锁锁定的数据,就不允许被读取。使用这一隔离级别,解决了并发事务之间未落实读的问题。事务的调度过程可见图6-8。

在这一隔离级别,查询操作申请的共享锁,在数据读取完成后立即释放。这时事务并没有完成,还要继续处理。如果该事务再次需要对同一数据进行读取,需要重新申请共享锁。由于共享锁只是在数据读取时才持有,因此事务对同一数据的多次读取,仍旧存在不可重复读问题,事务的调度过程可见图6-9。

该隔离级别既实现了数据读的一致性,又保证了事务之间的并发处理能力,因此许多数据库系统在缺省模式下,就使用此隔离级别。

3. 可重复读(repeatable read,2级)

在这一隔离级别,对数据的查询操作,要求给数据加共享锁。在数据读取完成后,锁不会被释放。一直到事务完成,事务申请的锁才全部释放。

由于被查询数据在事务运行过程中一直被加共享锁,其他的事务无法获取排它锁而更新该数据。因此,这一隔离级别解决了一个事务对同一数据多次读取而出现的不可重复读问题。事务的调度过程可见图6-10。

然而,这一隔离级别仍旧存在幻像问题。在事务运行时,其它的事务可以向表中插入新的记录,从而使该事务对同一区间数据的多次读取,而返回不同的结果。

4. 可串行读(serializable read,3级)

在这一隔离级别,对数据的查询操作,要求给数据加共享锁。在数据读取完成后,锁不会被释放。如果查询操作是基于数据的一个区间,就对整个区间加区间锁。一直到事务完成,事务申请的锁才全部释放。

如果一个事务执行基于数据区间的查询操作,并且获得了区间锁,那么该事务的执行过程中:

(1)任何其他事务往表中插入新的记录,如果该记录符合此数据区间,那么记录插入被拒绝。

(2)任何其他事务更改表中现有记录,如果更改后的记录符合此数据区间,那么记录更改被拒绝。

这一隔离级别解决了幻像读问题,但由于对数据的隔离要求很高,因此会严重地影响事务的并发处理。事务的调度过程可图6-11。

6.3.9 死锁

假定存在两个事务A和B。事务A申请由事务B拥有的锁,进入等待状态;同时事务B申请由事务A拥有的锁,同样进入等待状态。结果A、B两个事务互相等待,都不能够继续处理。我们把这种情况称为死锁。当然,这里展示的事例是一个比较特殊的情况,只有两个事务参与了死锁。一般情况下的死锁,是在多个事务组之间形成了等待环而引起的。

一个被很好设计、编写的应用系统,不应当有死锁的发生。而一旦出现死锁,数据库系统本身应当有能力解除死锁。处理死锁问题的方法很多,主要有以下两种思路:

(1)使用死锁预防措施,使系统永不进入死锁状态。

(2)允许系统进入死锁状态,使用死锁检测与恢复机制进行恢复。

1. 死锁预防

使数据库系统不会出现死锁问题,是死锁预防策略的关键所在。可以使用以下两种方式实现:

(1)事务同时获得所有锁

每个事务在开始之前,一次性锁定它要处理的所有数据。只要有一个锁没有被授予,事务就不能执行,并且立即释放所有已经获得的锁。事务只有在同时获得所有的锁之后,才能开始处理。

这种处理死锁的方式,虽然简单、明了,但缺点也很明显。首先,在事务开始前通常很难预知那些数据需要锁定。其次,数据使用率很低,因为许多数据虽然被封锁,但长时间不会被使用。

(2)对加锁请求进行排序

在应用系统设计和开发时,对所有的数据定义一个序列,要求所有事务只能按照该序列规定的顺序锁定数据。由于对数据的访问按照相同的顺序进行,不会出现事务之间的互相等待,从而避免了死锁的发生。

这种策略操作简单,实现起来很方便,应用系统设计及开发人员应当考虑这种避免死锁的方法。另外,对先查询、然后更新数据的事务,可以在查询时直接使用更新锁,也会降低死锁发生的可能性。

2. 死锁检测与恢复

死锁检测与恢复机制用来发现、然后处理系统中发生的死锁。系统周期性地检测,判断有无死锁发生。如果发生死锁,则系统必须从死锁中恢复。

数据库系统的运行需要维护事务的锁信息,包括:已经拥有、正申请以及正等待的锁信息。死锁检测程序利用这些信息,运用相关算法,判定系统是否出现了死锁。当检测算法判定存在死锁时,系统必须从死锁中恢复。解除死锁最通常的做法是回滚一至多个事务。

3. 锁等待超时机制

我们前面讲到的锁等待超时机制,也可以用来处理死锁。申请锁的事务至多等待一个给定的时间。如果在此期间锁没有被授予该事务,那么此事务超时,然后回滚。如果系统中确实存在死锁,那么卷入死锁的一个或者多个事务将由于超时而回滚,从而使其他事务继续执行。

该机制介于死锁预防与死锁检测及恢复之间。但是,一般很难确定一个事务超时之前应该等待多长时间。如果已经发生死锁,则等待时间太长而导致不必要的延迟。如果等待时间太短,即使没有死锁,也可能引起事务回滚,造成系统资源的浪费。

6.4 多版本机制

锁机制使用锁,要么延迟一项操作(锁等待),要么中止发出该操作的事务(锁超时),依此来保证事务的串行化调度。而我们这里要讲的多版本机制,则是使用数据的多个版本来实现事务的串行化调度。

多版本是另外一种并发控制机制。使用这种机制,系统对数据的更新操作,需要为数据创建一个新的版本,数据的更新操作在新版本上处理。在更新完成后,这个版本就成为该数据的最新可用版本。任一时刻,系统只允许一个事务对数据进行更新操作,避免了更新的丢失。

对数据的查询操作,系统选择数据最近的可用版本进行读取,并在事务中记录该版本信息,事务在整个处理过程中都只参照数据的这个版本。由于这个数据版本不会被更新,从而有效地解决了存取未落实的数据、不可重复读、幻像读现象问题。

多版本机制有一个好的特性:读请求从不失败,且不必等待。在典型的数据库系统中,读操作比写操作频繁,因而这个优点对于实践来说至关重要。出于性能的考虑,系统应当能够容易而且快速地判定一个事务需要读取数据的那一个版本。

尽管多版本机制能够很好地支持查询处理,但系统对多个数据版本的读取和维护,需要花费更多的CPU、内存、I/O、磁盘空间等系统资源,这反过来也影响了系统对查询处理的快速支持。

常用的多版本机制有:多版本时间戳机制和多版本两阶段封锁机制,我们下面将对这两种机制分别进行讨论。

6.4.1 多版本时间戳机制

使用多版本时间戳机制,每一个事务在开始执行前,系统都为它分配一个静态的、唯一的时间戳。对系统中的每一个数据项,有一个版本序列与之关联。每一个数据(记为Q)的版本包含三个方面的内容:

(1)版本的数值:C(Q)

(2)创建数据版本的事务时间戳:W(Q)

(3)读取数据版本的所有事务的最大时间戳:R(Q)

事务对数据的查询操作,不用等待、直接选择数据的最近可用版本进行读取。如果一个事务的时间戳大于数据版本的R(Q),就使用该事务时间戳更新R(Q)。

事务对数据的更新操作,要使用数据最近可用版本的W(Q)、R(Q),和事务的时间戳进行比较,根据一系列规则,决定事务是回滚、继续执行还是创建新的数据版本。

数据库中不再需要的数据版本,需要进行删除以释放磁盘空间。系统根据以下的规则进行删除。假设有某数据项的两个版本,两个版本的W(Q)都小于系统中最老只读事务的时间戳,那么两个版本中较旧的那个版本将不会再被使用,因而可以删除。

对多版本时间戳机制的使用,要考虑所存在的以下两个问题:

(1)读取数据项,要求更新数据版本的R(Q),于是产生两次潜在的磁盘访问而不是一次。

(2)事务间的冲突通过回滚解决而不是等待,这种做法开销可能很大。

6.4.2 多版本两阶段封锁机制

多版本两阶段封锁机制,将多版本并发控制的优点与两阶段封锁协议的优点结合起来。该机制对只读事务与更新事务加以区分。更新事务执行强两阶段封锁协议,即它们持有全部锁直到事务结束。因此,它们可以按照提交的次序进行串行化。

数据项的每一个版本有一个时间戳,这种时间戳不是真正基于时钟的时间戳,而是一个计数器,我们称之为ts_counter,这个计数器在事务提交时增加计数。

在只读事务开始执行前,系统读取ts_counter的当前值来作为该事务的时间戳,并依照ts_counter读取数据版本。该版本是数据当前的最新版本,事务在整个处理过程中都只参照数据的这个版本。

当更新事务读取一个数据项时,它在获得该数据项上的共享锁后,读取该数据项最新版本的值。当更新事务想写一个数据项时,它首先要获得该数据项上的排它锁,然后为此数据项创建一个新的版本,写操作在新版本上进行。新版本的时间戳最初值为∞,它大于任何可能的时间戳。当更新事务完成其任务后,事务将它新创建版本的时间戳设为ts_counter + 1,完成事务提交并释放锁。

这样,在ts_counter增加之后启动的只读事务,将看到被更新事务更新后的ts_counter值;而那些在增加之前就启动的只读事务,将看到被更新事务更新之前的ts_counter值。无论哪种情况,只读事务均不必等待加锁,它们访问、并只允许访问所看到ts_counter值对应的数据版本。

在多版本两阶段封锁机制中,对数据版本的删除类似于多版本时间戳机制中采用的方式。假设有某数据项的两个版本,两个版本的时间戳都小于系统中最老只读事务的时间戳,那么两个版本中较旧的版本将不会再被使用,因而可以删除。

由于两阶段封锁协议不能解决死锁问题,因此使用多版本两阶段封锁机制的数据库系统,在运行过程中仍旧会出现死锁。但由于查询处理不需要任何锁,因此系统中出现死锁的概率会大大降低。对该问题的处理和解决,可以参看第6.3.9一节。

6.5 系统并发控制的调整

我们前面讲述了锁机制和多版本机制,不同的数据库系统会采用不同的并发控制。用户在管理和使用数据库系统时,应首先了解系统使用了那种并发控制,然后结合相关知识,根据自己的需要对系统的并发控制进行管理和配置。

1. 锁机制

锁机制是通过锁来实现事务之间的并发处理和可串行化调度,因此避免锁的等待、减少系统在锁管理上的开销是优先要考虑的问题。对使用锁机制的数据库系统,我们在管理时应当坚持以下的原则:

(1)选择合理的锁粒度。

记录级别上的锁具有最大的并行处理能力,但对锁的存储和管理需要更多的系统资源。这些正好和表级别上的锁相反。一般来说,交互式的应用系统由于其事务多、涉及的数据量小,可以考虑使用记录级别锁;而对大批量数据的查询和汇总,表级别锁应该是更好的选择。用户应当根据自己的需要,进行合理地选择。

(2)应当避免锁的升级。

如果系统中出现锁的升级,就表明系统对锁的使用存在问题。这时,要么调整系统的配置参数,增大整个系统以及单个事务可用的锁数量;要么调整应用程序,改变程序中事务对锁的使用方式。

(3)在事务处理中,使用满足业务需要、最小的隔离级别。

由于数据被访问时,隔离级别决定数据如何被加锁、如何与其他事务隔离,因此为事务选择合适的隔离级别很重要。隔离级别不仅影响事务之间的隔离程度,也影响单个事务的性能特性,因为获取、释放锁所要求的CPU、内存资源随隔离级别而改变,隔离级别越高,需要的系统资源就越多。

(4)应当在应用系统设计和开发时,就考虑对死锁问题的处理。

不要将死锁问题的解决和处理完全交由数据库系统完成,在应用系统设计和开发时对死锁问题的考虑,将会达到事半功倍的效果。对于死锁很少出现、甚至不会发生的应用系统,可以增大死锁检测的时间间隔,从而减少死锁检测的执行次数,避免不必要的系统开销。

2. 多版本两阶段封锁机制

多版本两阶段封锁机制使用数据的多个版本和两阶段封锁协议,来实现事务之间的并发处理和可串行化调度。对使用这种并发控制机制的数据库系统,我们在管理时应当做以下的考虑:

(1)合理地分布数据的存放。

相对于锁机制,一个数据项存在多个版本,需要更多的磁盘空间倒是其次,关键是可能有多个事务要同时访问这些数据版本,系统需要消耗更多的I/O操作去读取这些数据。为减少磁盘的I/O竞争,应当仔细地规划数据的存放方式和位置。

(2)保证有足够的CPU、内存资源。

相对于锁机制,系统对多版本数据的管理和维护,需要更多的CPU处理时间、更多的内存存放空间。使系统有足够的CPU、内存资源,可保证数据库系统的正常运行。

(3)系统一般不会出现锁的问题,但也要合理地使用锁。

由于查询处理不需要任何锁,大大降低了系统使用的锁数目以及对锁的管理,死锁发生的可能性也大大减少。但对死锁的避免以及数据更新处理时对锁级别的选择,也需要在应用系统设计和开发时仔细地考虑和规划。

6.6 常用数据库系统的并发控制机制

通过掌握系统的事务处理和并发控制机制,用户可以在应用系统设计和开发过程中,根据自身需要有效地使用系统资源,可以在系统运行过程中更好地进行管理和维护。

常用的数据库系统:DB2、ORACLE、INFORMIX、SYBASE,在事务的使用和处理上有着大体相同的执行方式,但在系统并发控制的使用和配置上有着明显的区别。下面我们就仅仅针对各个数据库系统的并发控制机制,进行简单的介绍。

6.6.1 DB2数据库系统

DB2系统采用锁机制作为其并发控制机制。配置参数LOCKLIST,设定内存中锁表缓冲区的最大空间,每一个数据库都有这样一个锁表缓冲区,存放该数据库中所有的锁定,该参数也决定了数据库中可用锁的数目。

1. 锁的类型

除了共享锁、排它锁、意图锁、更新锁之外,DB2系统引入了超级排它锁(super exclusive lock)、弱排它锁(weak exclusive lock)等一些锁。这些锁在修改表、索引结构时使用,为的是减少对系统性能影响。

2. 锁的粒度

DB2系统锁的粒度,可以划分为:数据库、表、页、记录。其中,页锁只能在多维聚集(multi-dimensional clustering,MDC)表中使用,而经常被使用的标准表不能使用页锁。

可以通过以下方式使用数据库锁:在连接而打开数据库时,使数据库处于锁定模式。要使用表锁,可以在命令行或者应用程序中,使用lock table命令。也可以直接改变表的锁模式为表锁。

数据库系统缺省的锁模式为记录锁。所有被创建的表,其最初的锁模式均为记录锁。用户可以根据自己的需要,使用命令更改锁的模式。

3. 锁的升级

在DB2系统中,存在两种锁升级方式:由记录锁升级为表锁、由页锁升级为表锁。只有多维聚集表,才会出现页锁到表锁的升级。

在以下两种情况下,会引起锁的升级:

(1)配置参数LOCKLIST,设定内存中锁表缓冲区的最大空间。当数据库中所有锁使用的内存空间超过该参数的设置后,就引起锁的升级。如果出现锁的升级,系统就检查所有活动的表,从最多拥有记录锁的表开始,将记录锁升级为表锁,直至系统中所有锁的内存空间使用达到配置参数LOCKLIST的一半为止。

(2)配置参数MAXLOCKS,设定数据库的锁表缓冲区中,一个事务所拥有锁可以使用的内存空间百分比。一旦一个事务所有锁的内存空间使用达到此参数设置,就引起锁的升级。如果出现锁的升级,系统就检查该事务所访问的表,将拥有最多记录锁的表升级为表锁。

4. 锁的等待和超时

配置参数LOCKTIMEOUT,设定事务在获取锁时等待的最大时间。一旦等待时间达到此参数设置,而事务仍旧没有被授予锁,发出事务的应用程序就会收到错误信息,并进行事务的回退。

5. 隔离级别

尽管DB2系统也使用四级隔离级别,但仍旧和SQL-92标准(第6.3.8一节所述)略有不同。DB2系统的四级隔离级别如下:

(1)未落实的读(uncommitted Read,UR),隔离级别0级。对应SQL-92标准的0级。

(2)游标稳定性(cursor stability,CS),隔离级别1级。在使用游标(cursor)操作数据时,当前被处理的记录需要加共享锁。在游标指针移动到下一条记录后,当前记录的共享锁被释放,而下一条记录被加共享锁。如果不使用游标访问数据,此隔离级别就等同于SQL-92标准的1级。

(3)读稳定性(read stability,RS),隔离级别2级。对应SQL-92标准的2级。

(4)可重复读(repeatable read),隔离级别3级。对应SQL-92标准的3级。

缺省情况下,DB2系统使用隔离级别1。用户可以通过以下方式,来设定自己需要的隔离级别:

(1)在应用程序的预编译或者绑定时,为要生成的函数包指定隔离级别。

(2)通过应用程序和数据库系统的连接方式(如:CLI、ODBC、JDBC等),指定一个会话连接所使用的隔离级别。

(3)在单个SQL语句中,为该语句指定隔离级别。该指定有最高优先级。

6. 死锁

配置参数DLCHKTIME,设定了系统检查死锁的时间间隔。

6.6.2 ORACLE数据库系统

ORACLE系统采用多版本两阶段封锁机制。同一个数据在系统中存在多个版本,查询操作不需要任何锁。对数据的更新需要加排它锁,排它锁直到事务提交后才释放。由于不需要为查询处理加锁,因此相对于其他数据库系统,ORALCE系统的锁管理很简单。

1. 回退表空间(undo tablespace)

回退表空间用来存放数据的写前镜像。在ORACLE系统中,任何数据在被更新之前,都要将数据的拷贝存放在回退表空间中,这样系统中就存在同一个数据的多个版本。利用回退表空间中的数据拷贝,可以做到:

(1)如果当前正在更新数据的事务失败,就可以直接使用这些数据拷贝,回退事务的更新。

(2)查询操作使用这些数据拷贝中的最新版本进行处理,不用等待当前更新事务的完成。

回退表空间不可能有无限大的磁盘空间,因此ORALCE系统循环地使用回退表空间中的磁盘空间。在回退表空间被写满后,系统就覆盖表空间中的最早数据。

下列配置参数和回退表空间有关:

(1)UNDO_TABLESPACE:设定系统使用的回退表空间。

(2)UNDO_MANAGEMENT:设定回退段的管理模式。可以使用自动管理,也可以使用手工管理。

(3)UNDO_SUPPRESS_ERRORS:设定在自动管理模式下,对不再支持的手工管理操作,是否返回出错信息。

(4)UNDO_RETENTION:设定回退段中的数据在被覆盖之前,需要保留的时间。

2. 更新操作

在ORALCE系统中,更新操作使用两阶段封锁协议。对要更新的数据加排它锁,同时将数据的拷贝存放在回退表空间中。排它锁直到事务提交后才释放。

ORALCE系统在最低限制的级别上锁定数据,系统中的锁可以分为两大类:

(1)字典锁(dictionary locks)。在执行DDL操作时,系统自动获得该锁。

(2)数据锁(data locks)。在执行DML操作时,系统为表加共享型的表锁(shared table lock)对每一个需要处理的行都加排它型的记录锁。

可以手工使用lock table命令,给要处理的表加锁。配置参数ROW_LOCKING,其确省值为always,表明在数据更新操作期间,系统除了自动使用相应的共享表锁外,只在要处理的记录上加排它锁;如果配置参数取值intent,在数据更新操作期间,系统对整个表使用排他锁,相当于手工使用了lock table命令

如果要锁定整个数据库,可以在打开数据库时,使数据库处于静止状态,这时只有用户SYS、SYSTEM可以建立会话。

3. 查询操作

在ORALCE系统中,查询操作可以不使用任何锁。系统在进行查询处理时,首先检查要处理数据是否正由其他事务执行更新操作。如果正在执行更新操作,系统就从回退表空间中找到数据的上一个版本,整个查询操作就使用数据的这一个版本,一直到查询操作所在的事务结束。由于存放在回退表空间中的这一数据版本,不会被任何事务所更新,因此在查询事务中可以反复、多次地读取该数据,而不会出现不可重复读、幻像读现象,相当于SQL-92标准中的隔离级别3——可串行读,尽管ORALCE系统中不存在隔离级别的说法。

然而数据的多版本带来了另外的问题。由于事务的数据查询处理,自始至终都是使用同一个版本,如果一个事务处理的时间很长,而存放在回退表空间中的这一数据版本被覆盖,在事务再次需要读取该数据版本时,由于找不到该数据版本,系统就会报:“ORA-01555 snapshot too old”错误。如果一个历时数小时或者10多个小时的查询事务,最后遭遇ORA-01555错误而失败,将会是多么让人沮丧的一件事。

4. 死锁

ORACLE系统自动检测和解决死锁。在两个或多个事务发生死锁后,如果一个事务的当前语句发现了死锁,该事务就开始回退。用户可以通过系统的告警、跟踪信息文件,查看死锁信息。

6.6.3 INFORMIX数据库系统

INFORMIX系统采用锁机制作为其并发控制机制。配置参数LOCKS,设定系统初始锁的数目。如果被要求的锁数目超过此参数设置,系统就自动加倍该参数设置,使系统可以使用的锁数目增加一倍。在连续加倍15次之后,如果锁的数目仍旧不足,系统就会报错。

1. 锁的类型

除了共享锁、排它锁、意图锁、更新锁之外,INFORMIX系统引入了字节锁(byte lock)、字节范围锁(byte-range lock)。对可变长字符(varchar)类型字段的更新操作,系统会使用字节锁;对快捷大对象的处理,可以使用字节范围锁,以提高数据利用率。

2. 锁的粒度

INFORMIX系统锁的粒度,可以划分为:数据库、表、页、记录。可以通过以下方式使用数据库锁:

(1)可以在打开数据库时,使数据库处于锁定模式。

(2)在整个数据库系统启动时,使系统进入静止状态。

要使用表锁,可以在命令行或者应用程序中,使用lock table命令。另外表锁也可能由于锁的升级而获得。

页锁和记录锁是表要使用的锁模式。在创建表时,可以指定表所使用的锁模式。如果没有指定,系统就依次检查配置参数DEF_TABLE_LOCKMODE、环境变量IFX_DEF_TABLE_LOCKMODE的设定。缺省情况下,系统使用页锁。表一旦被创建,锁的模式就确定下来,但用户可以使用命令改变它的锁模式。

3. 锁的升级

在INFORMIX系统中,存在两种情况下的锁升级:由记录锁升级为表锁、由页锁升级为表锁。对这两种情况下的锁升级,相关资料中并没有给出有关的说明和限制。

除此之外,对快捷大对象,如果一个事务所拥有的字节范围锁数目,超过系统中当前锁数目的33%,就会出现锁的升级。

4. 锁的等待和超时

可以使用set lock mode命令,在命令行或者应用程序中为单个会话设定锁的等待、超时时间。在缺省情况下系统不会等待,直接返回错误信息。

5. 隔离级别

尽管INFORMIX系统也使用四级隔离级别,但仍旧和SQL-92标准(第6.3.8一节所述)略有不同。INFORMIX系统的四级隔离级别如下:

(1)脏读(dirty read),隔离级别0级。对应SQL-92标准的0级。

(2)落实的读(committed read),隔离级别1级。对应SQL-92标准的1级。

(3)游标稳定性(cursor stability),隔离级别2级。在使用游标(cursor)操作数据时,当前被处理的记录需要加共享锁。在游标指针移动到下一条记录后,当前记录的共享锁被释放,而下一条记录被加共享锁。如果不使用游标,此隔离级别就等同于隔离级别1级。

(4)可重复读(repeatable read),隔离级别3级。对应SQL-92标准的2级和3级。

缺省情况下,INFORMIX系统使用隔离级别1。用户可以使用set isolation命令,为随后要执行的SQL语句,设置要使用的隔离级别。而对不记录日志信息的数据库,则只能使用隔离级别0。

6. 死锁

配置参数DEADLOCK_TIMEOUT,设定了系统检查死锁的时间间隔。

6.6.4 SYBASE数据库系统

SYBASE系统采用锁机制作为其并发控制机制。配置参数NUMBER OF LOCKS,设定了整个系统可以使用的锁数目。如果所要求的锁数目超过此参数设置,系统就会报错。

1. 锁的类型

除了共享锁、排它锁、意图锁、更新锁之外,SYBASE系统引入了需求锁(demand lock)。在第6.3.3一节中,我们讲到:由于不断有新的事务给数据加共享锁,一个事务可能长时间无法获取排它锁,SYBASE系统使用需求锁来解决这个问题。

一个事务申请数据的排它锁,它要等待数据上现有的共享锁被释放。这时其它新的事务可以继续访问该数据,给数据加共享锁。在连续有三个新的事务获取了数据的共享锁之后,最初申请排它锁的事务就给该数据加上需求锁。在数据上存在需求锁之后,系统就不允许新的事务访问该数据,给数据加共享锁,所有新的事务必须等待。这样,在所有的共享锁被释放后,最初的事务就将需求锁转换为排它锁,从而可以进行事务的处理。

2. 锁的粒度

SYBASE系统锁的粒度,可以划分为:数据库、表、页、记录。可以通过以下方式使用数据库锁:

(1)在整个数据库系统启动时,使系统进入单用户模式。

(2)在数据库系统正常运行过程中,更改单个数据库的属性,使该数据库只可为特权用户使用。

要使用表锁,可以在命令行或者应用程序中,使用lock table命令。另外表锁也可能由于锁的升级而获得。

对于页锁和记录锁,系统提供了三种锁模式(lock scheme):allpages、datapages、datarows。allpages和datapages模式属于页锁,而datarows属于记录锁。

对allpages模式,事务申请锁时,不但要锁定数据所在的页,而且数据所对应的索引页也要被锁定。如果事务申请的是排它锁,这些锁定一直到事务结束才释放。

对datapages模式,事务申请锁时,只锁定数据所在的页,数据所对应的索引页不需要锁定。如果事务要更新数据,就要为数据所在的页加排它锁。这时,如果表的数据更新要引起索引中数据的更新,就需要在更新执行时为索引页加排它锁,在更新完成后,就立即释放索引页上的排它锁。而数据页上的排它锁,一直到事务结束才释放。

对datarows 模式,其处理方式等同于datapages模式,区别在于datarows模式使用的是记录锁。

在创建表时,可以指定表所使用的锁模式。如果没有指定,就使用配置参数LOCK SCHEME的设定。缺省情况下,系统使用allpages锁模式,该模式虽说降低了系统的并发处理能力,但减少了锁管理的系统开销。

一旦被创建,表的锁模式就确定下来。使用allpages或者datapages模式的表,只能使用页锁和表锁;使用datarows模式的表,只能使用记录锁和表锁。可以使用命令改变一个表的锁模式。

3. 锁的转换

锁转换在SYBASE系统中出现的频率比较高,除了需求锁的使用之外,还与系统使用了不同的排它锁申请机制有关。在SQL语句需要数据的排它锁时,系统在SQL语句处理的初始阶段,为它分配一个更新锁,即使这时该数据上不存在任何锁。在SQL语句开始执行时,才将更新锁转换为排它锁。因此,在SQL语句最后执行之前,其他的SQL语句仍旧可以访问该数据,从而提高了数据的利用率,提高了事务的并发处理能力。但这种方法同时也增加了锁机制的复杂程度,使系统容易出现锁的问题。

4. 锁的升级

在SYBASE系统中,只存在两种情况下的锁升级:对使用datarows模式的表,可以由记录锁升级为表锁;对使用allpages或者datapages模式的表,可以由页锁升级为表锁。在整个系统范围内,对锁的升级可以使用下列配置参数进行设定:

PAGE LOCK PROMOTION HWM:设定页锁升级的高水平值。一个事务在一个表上获得的页锁,达到该参数的设置后,系统就试图升级为表锁。

PAGE LOCK PROMOTION LWM:设定页锁升级的低水平值。一个事务在一个表上获得的页锁,低于该参数的设置,系统不会进行锁的升级。在页锁的数目超过此参数设置,但没有达到参数PAGE LOCK PROMOTION HWM的设置时,系统就根据参数PAGE LOCK PROMOTION PCT的设置,决定是否需要锁的升级。

PAGE LOCK PROMOTION PCT:设定进行页锁升级的页使用百分比。一个事务在一个表上获得的页锁,超过参数PAGE LOCK PROMOTION LWM的设置,但低于参数PAGE LOCK PROMOTION HWM的设置时,系统就计算事务使用的数据页占表中总数据页的百分比。如果达到此参数设置,系统就试图升级为表锁。

ROW LOCK PROMOTION HWM:设定记录锁升级的高水平值。其处理方式同页锁。

ROW LOCK PROMOTION LWM:设定记录锁升级的低水平值。其处理方式同页锁。

ROW LOCK PROMOTION PCT:设定记录锁升级的使用百分比。其处理方式同页锁。

可以使用命令sp_setpglockpromote、sp_setrowlockpromote,在数据库、表的级别上对锁的升级进行设定。对单个表的锁升级设定,具有最高的优先级。

5. 锁的等待和超时

为了防止事务对锁的无限期等待,SYBASE系统引入了需求锁。配置参数LOCK WAIT PERIOD,在系统范围内指定了锁的等待、超时时间,可以使用set lock命令在命令行或者应用程序中为单个会话设定。

6. 隔离级别

SYBASE系统使用的四级隔离级别,同SQL-92标准(第6.3.8一节所述)。缺省情况下,系统使用1级的隔离级别。用户可以通过以下方式,对隔离级别进行设置:

(1)使用set isolation level命令,在命令行或者应用程序中,设定单个会话的隔离级别。

(2)在SQL语句中,设定单个SQL语句的隔离级别。

(3)在SQL语句中,为SQL语句中要访问的表设定隔离级别。

7. 死锁

配置参数DEADLOCK CHECKING PERIOD,设定了系统检查死锁的时间间隔。

6.7 本章小结

事务是构成单一逻辑工作单元的操作集合,数据库系统中的所有处理都以事务为单位。事务中的所有操作,要么全都执行,要么全都不执行。

事务具有原子性、一致性、隔离性、持久性四个特征,简称为ACID。原子性要求整个事务在逻辑上是一个整体,不可以分割;一致性要求事务对数据的更新,能够真实、正确地反映现实世界中的业务处理;隔离性保证多个事务的并发执行,事务之间不会互相影响;持久性保证事务所作的更新在事务完成后能够长久地反映在数据库中。

多个事务的并发执行、共享数据,给系统带来了以下问题:丢失更新、存取未落实的数据、不可重复读、幻像读现象。数据库系统使用并发控制机制,来解决这些问题。常见的并发控制机制有锁机制和多版本机制。

锁机制使用锁来实现事务的并发处理,常用的锁类型有:共享锁、排它锁、意向锁和更新锁。在不同级别上给数据加锁,锁的作用范围、影响到的数据量大小也不一样,可以将锁的粒度从高到低依次划分为:数据库、表、页、记录。事务对锁的申请并不总是成功,只有数据上不存在相冲突的锁时,锁才被授予。如果锁没能被授予,申请锁的事务就开始等待,一直到锁超时为止。

事务对一个数据只能拥有一个锁定,在需要更严格的锁定时,系统将进行锁的转换。系统中出现锁的升级,表明系统对锁的使用存在问题,需要进行调整。为了防止死锁的发生,可以采取死锁预防、死锁检测与恢复等策略。

两阶段封锁协议保证了事务调度的可串行性,使系统不会出现更新丢失问题。而四级的隔离级别,分别用来解决存取未落实的数据、不可重复读、幻像读现象问题。

多版本机制使用数据的多个版本来实现事务的串行化调度。系统对数据的更新操作,需要为数据创建一个新的版本;对数据的查询操作,选择数据的最近可用版本进行读取。

系统在任一时刻只允许一个事务对数据进行更新操作,避免了更新的丢失;查询操作所参照的数据版本不会被更新,有效地解决了存取未落实的数据、不可重复读、幻像读现象。

多版本机制有以下特性:读请求从不失败,且不必等待。但系统对多个数据版本的读取和维护,需要花费更多的CPU、内存、I/O、磁盘空间等系统资源。

多版本两阶段封锁机制,将多版本并发控制的优点与两阶段封锁协议的优点结合起来。该机制对只读事务与更新事务加以区分,要求更新事务执行强两阶段封锁协议。

DB2、INFORMIX、SYBASE系统的并发控制均使用锁机制,而ORACLE系统则采用多版本两阶段封锁机制。


火龙果软件/UML软件工程组织致力于提高您的软件工程实践能力,我们不断地吸取业界的宝贵经验,向您提供经过数百家企业验证的有效的工程技术实践经验,同时关注最新的理论进展,帮助您“领跑您所在行业的软件世界”。
资源网站: UML软件工程组织