首页 > 范文大全 > 正文

数据库并发事务的控制

开篇:润墨网以专业的文秘视角,为您筛选了一篇数据库并发事务的控制范文,如需获取更多写作素材,在线客服老师一对一协助。欢迎您的阅读与分享!

摘要:现代数据库作为多用户共享访问的资源,在各种事务的并发操作中,数据库系统和应用程序框架如何保证数据的一致性和正确性是一个不可回避的问题。本文结合实际编程对数据库并发事务的处置原理和机制进行探讨。

关键词:数据库;并发;悲观锁;乐观锁

中图分类号:TP311 文献标识码:A文章编号:1007-9599 (2011) 15-0000-02

Database Concurrency Transactions Control

Jiang Fangming

(Guilin Lijiang Tour Schedule Settlement Center,Guilin541001,China)

Abstract:Modern database as a multi-user shared access to resources in the concurrent operation of various services,database systems and application frameworks to ensure data consistency and accuracy is an unavoidable problem.In this paper,the actual programming of the database concurrent transactions disposal principles and mechanisms were discussed.

Keywords:Database;Concurrent;Pessimistic locking;Optimistic locking

数据库在计算机系统中普遍存在并广泛应用,作为一个共享资源,为用户提供各种查询、检索、更改记录、统计和存储等需求。在用户程序对数据库的使用过程中,经常会出现多个用户并行地存取数据库,这样就会产生多个用户程序并发存取同一数据的情况,若对并发操作不加控制就可能会存取和存储不正确的数据,破坏数据库的一致性,所以数据库管理系统必须提供并发控制机制,并发控制机制的好坏是衡量一个数据库管理系统性能的重要标志之一。而我们在进行数据库的编程开发时也必须防止多用户并发使用数据库时造成数据错误和程序运行错误,以保证数据的完整性。本文结合实际编程探讨一下数据库并发事务的处置。

一、事务与并发控制的概念[1]

事务就是用户定义的一个数据库操作序列,这些操作要么全做要么全不做,是一个不可分割的很小的工作单位。

为了充分利用系统资源,使数据库的共享资源得以有效利用,必须可以使多个事务并行的执行,而数据库对并行执行的事务进行的控制就是并发控制。

二、事务进行并发操作可能引起的数据不一致问题

由于种种原因,都可能引起数据库的数据遭到破坏,比如多个事务在并行运行的时候,不同的事务的操作产生了交叉执行,或者,事务在运行过程中被强行停止或者中断。如果没有锁定且多个用户同时访问一个数据库,则当他们的事务同时使用相同的数据时可能会发生问题,导致数据库中的数据的不一致性。

一个最常见的并发操作的例子是超市零售系统中的销售操作。例如,在该系统中的一个活动序列:(1)甲售货员读出某商品的库存余额A,设A=20;(2)乙售货员读出同一商品的库存余额A,也是20;(3)甲售货员卖出一份该商品,修改库存余额A=A-1=19,把A写回数据库;(4)乙售货员也卖出一份该商品,修改库存余额A=A-1=19,把A写回数据库。结果明明卖出两份该商品,数据库中库存余额只减少1。

这种情况称为数据库的不一致性。这种不一致性是由甲、乙两个售货员并发操作引起的。在并发操作情况下,对甲、乙两个事务操作序列的调度是随机的。若按上面的调度序列行,甲事务的修改就被丢失。这是由于第(4)步中乙事务修改A并写回覆盖了甲事务的修改。并发操作带来的数据库不一致性可以分为四类[1]:丢失或覆盖更新、脏读、不可重复读和幻像读,上例只是并发问题的一种。

(一)丢失或覆盖更新(lost update)

当两个或多个事务选择同一数据,并且基于最初选定的值更新该数据时,会发生丢失更新问题。每个事务都不知道其它事务的存在。最后的更新将重写由其它事务所做的更新,这将导致数据丢失。上面超市售货的例子就属于这种并发问题。

(二)脏读

一个事务读取了另一个未提交的并行事务写的数据。当第二个事务选择其它事务正在更新的行时,会发生未确认的相关性问题。第二个事务正在读取的数据还没有确认并且可能由更新此行的事务所更改。换句话说,当事务1修改某一数据,并将其写回磁盘,事务2读取同一数据后,事务1由于某种原因被撤销,这时事务1已修改过的数据恢复原值,事务2读到的数据就与数据库中的数据不一致,是不正确的数据,称为脏读。

(三)不可重复读(nonrepeatable read)

一个事务重新读取前面读取过的数据,发现该数据已经被另一个已提交的事务修改过。即事务1读取某一数据后,事务2对其做了修改,当事务1再次读数据时,得到的与第一次不同的值。

(四)幻像读

如果一个事务在提交查询结果之前,另一个事务可以更改该结果,就会发生这种情况。这句话也可以这样解释,事务1按一定条件从数据库中读取某些数据记录后未提交查询结果,事务2删除了其中部分记录,事务1再次按相同条件读取数据时,发现某些记录神秘地消失了;或者事务1按一定条件从数据库中读取某些数据记录后未提交查询结果,事务2插入了一些记录,当事务1再次按相同条件读取数据时,发现多了一些记录。

产生上述四类数据不一致性的主要原因是并发操作破坏了事务的隔离性。并发控制就是要用正确的方式调度并发操作,使一个用户事务的执行不受其他事务的干扰,从而避免造成数据的不一致性。

三、数据库系统对于共享数据的并发访问控制[2]

现在的绝大部分数据库系统在系统访问级别都有其共享数据的并发访问控制,但它所解决的是在同一时刻多用户对同一记录的使用控制,避免出现同步更新写入的情况冲突,同时要兼顾数据库并发访问的性能。在并发访问控制机制中,各主流数据库采用的基本都是系统锁的方式。

(一)数据库系统锁的基本原理

数据库系统锁是用户对数据库的访问请求被允许后,由数据库根据访问的性质类型对被访问的数据进行标示,以限制其他访问请求的操作。

(二)按照封锁的程度,锁可以分为:共享锁、独占锁、更新锁

1.共享锁

共享锁用于读数据操作,它是非独占的,允许其他事务同时读取其锁定的资源,但不允许其他事务更新数据。共享锁的特征:

加锁条件:当一个事务执行Select操作时,数据库系统就会为其分配一把共享锁,来锁定查询数据。

解锁条件:在默认情况下,数据读取完毕,共享锁就解除了。

与其他锁的兼容性:如果数据资源上放置了共享锁,还能再放置共享锁和更新锁。

并发性能:具有良好的并发性能,当多个事务读相同的数据时,每个事务都会获得共享锁,因此可以同时读锁定的数据。

2.独占锁

也叫排它锁,适用于修改数据的场合。它锁定的资源,其他事务不能读取也不能更新。独占锁具有以下特征:

加锁的条件:当一个事务执行insert、update、或delete语句时,数据库就会为SQL所操作的数据使用独占锁。如果数据上有其他锁,那么就能放置独占锁。

解锁条件:到事务结束时才能被解除。

与其他锁的兼容性:独占锁不能和其他锁兼容。通用如果资源上有其他锁,那么也不能放置独占锁。

并发性能:性能较差,只允许有一个事务访问锁定的数据,如果其他事务也需要访问该数据,就必须等待,直到前一个事务结束,解除了独占锁,其他事务才有机会访问资源。

3.更新锁

在更新操作的初始化阶段用来锁定可能要被修改的资源,这可以避免使用共享锁造成死锁的现象。更新具有以下特征:

加锁的条件:当一个事务执行update语句时,数据库系统会先为事务分配一把更新锁。

解锁条件:读取数据完毕,执行更新操作时,会把更新锁升级为独占锁。

4.与其他锁的兼容性

更新锁与共享锁是兼容的。也就是说,一个资源可以同时放置更新锁和共享锁,但是最多只能放置一把更新锁。

并发性能:允许多个事务同时访问资源。但不允许修改。

四、应用程序编程中对于数据库并发事务的控制[2]

数据库系统对于的并行处理机制解决了多进程对同一数据库记录操作的顺序问题,但它不能从逻辑上保证数据并发操作后的正确性。现在的很多数据库的开发框架已经具有数据库的并发处理机制,例如Hibernate(作为一个开放源代码的对象关系映射框架,它对JDBC进行了非常轻量级的对象封装,使得Java程序员可以随心所欲的使用对象编程思维来操纵数据库),程序员进行应用软件开发时,只需按需要进行设置,但前提是:我们必须要了解其工作的原理。

在应用系统的开发过程中,我们对于可能的数据库并发事务一般采用“锁”的机制:即给特定的目标数据上锁,使其无法被其他程序进程修改。常用的锁机制就是我们常说的“悲观锁(Pessimistic Locking)”和“乐观锁(Optimistic Locking)”[3]。

(一)悲观锁(Pessimistic Locking)

悲观锁(pessimistic locking),顾名思义,先假设并发更新冲突会发生,所以不管冲突是否真的发生,都会引入额外的锁开销。但是,这个额外开销比完全隔离事务的额外开销要小得多。使用悲观锁的事务会锁住读取的记录,防止其他事务读取和更新这些记录。其他事务会一直阻塞,直到这个事务的提交或者回滚释放了锁。悲观锁能够防止修改丢失,并且能够提供一定程度读取一致性,因为它防止本事务读取的记录被其他事务修改。然而,因为悲观锁不防止新纪录的增加,所以执行同样的查询可能返回不同的结果集。

在Oracle数据库中,我们经常用select-------for update的语句实现悲观锁定记录[3],如:

Select*from Ticket where user ID=’001’for update

这样,检索出来的记录就被锁定,其他进程不能对其进行修改(但可以进行非独占性查询),直到提交事务,解除锁定。悲观锁通过限制并发达到解决冲突和数据不一致的问题,它的优缺点都比较明显。

优点:作为一种数据库级的解决方法,不论用各种途径操作数据库(JAVA、PL/SQL Developer等),都能有效地保证数据的正确性。

缺点:(1)由于限制了并发操作,造成运算性能的下降,相关进程只能排队等待。其实,大部分的数据库并发操作不会造成数据的不一致,尤其是查询类的进程。如一个金融系统,当某个操作员读取用户的数据,并在读出的用户数据的基础上进行修改时(如更改用户账户余额),如果采用悲观锁机制,也就意味着整个操作过程中(从操作员读出数据、开始修改直至提交修改结果的全过程,甚至还包括操作员中途去煮咖啡的时间),数据库记录始终处于加锁状态,可以想见,如果面对几百上千个并发,这样的情况将导致怎样的后果。(2)可移植性较差,依赖于特定的数据库,而且并不是所有的数据库都提供悲观锁。在不同的数据库中,悲观锁的使用是有一定的限制的。例如,Oracle里面,SELECT FOR UPDATE只能用在顶层SQL,而不能嵌在子查询里面。还有一些SQL特性不能和SELECT FOR UPDATE一起使用。

(二)乐观锁(Optimistic Locking)

相对于悲观锁,乐观锁的机制是确定并发更新的冲突发生的时候,才需要进行回滚的处置,产生数据库性能的额外开销。当事务更新记录时,事务会进行检查,看看自从自己上次读取了这条记录之后,是否有其他事务修改了这条记录。如果被其他事务修改了,这个事务通常会回滚,然后重新尝试。更新数据的时候执行这个代价不高的检查,能够避免弄丢其他数据的修改。而且,只有发现修改冲突的时候,才引起事务重来一遍的额外开销。

乐观锁通常采用的方法是用一个version(版本)字段来跟踪记录修改状况,每次修改,version都会递增。事务只需要把原来读出的version和当前version进行比较,就可以判断一条记录是否被修改过。应用程序检查和修改version字段是比较简单的做法,通常也是最好的做法。(1)我们通常给数据库中一个可能进行并发操作造成数据不一致的表增加一个Version字段;(2)查询该记录时,得到Version字段的值,假设为100;(3)重新对该记录进行保存时,查询取得最新的Version字段的值,如果取得的新Version字段值=100,则可认为该条记录在之前查询到现在更新没有被其他进程更改过,即进行保存,并将Version值加1;如果取得的新Version字段值不等于100,可认为该条记录在之前查询到现在更新被其他进程更改过,则不允许保存,对进程进行回滚[3]。

通过version实现的乐观锁机制是Hibernate官方推荐的乐观锁实现,同时也是Hibernate中,目前唯一在数据对象脱离Session发生修改的情况下依然有效的锁机制。因此,一般情况下,我们都选择version方式作为Hibernate乐观锁实现机制。从乐观锁的实现原理中,我们可以看到乐观锁在并发处理性能上的优势是非常明显的。

对于两种机制的选择,取决于访问频率和一旦产生冲突的严重性。如果系统被并发访问的概率很低,或者冲突发生后的后果不太严重(所谓后果应该指被检测到冲突的提交会失败,必须重来一次),可以使用乐观锁,否则使用悲观锁。绝大部分的编程人员在应用中都会使用和推荐乐观锁。

参考文献:

[1]苗雪兰.数据库系统原理及应用教程[M].北京:机械工业出版社,2004

[2]罗代均.数据库并发操作[J].信息与控制,2007,10

[作者简介]蒋方明,男,桂林市漓江游览调度结算中心,工程师,长期从事软件工程、数据库技术、人工智能的研究及计算机信息系统开发建设。