`
chenfei
  • 浏览: 25921 次
  • 来自: ...
最近访客 更多访客>>
社区版块
存档分类
最新评论

处理共享数据的并发访问

阅读更多

n  使用悲观锁和乐观锁

n  用iBATIS、JDO和Hibernate处理并发

n  用Spring处理并发操作失败后的恢复

很多企业应用程序把公司和客户的关键数据存储起来。比如,考 虑一下你的银行如何存放你的钱财。哈里波特在古灵阁的金库里存着真金加隆,但是你在银行里的钱却只是银行数据库里脆弱的0和1。为保护那些数据,银行必须 做很多事情,最重要的就是当多个数据库事务同时更新数据的时候,保证数据的完整性。

企业应用程序基本上都是多用户的,而且,很多情况下,还包括 任务调度器或者外部系统触发的后台任务。所以,多个事务同时读写数据库是很常见的情况。企业应用程序开发人员面对的主要挑战是:多个事务并发更新数据库带 来的数据不一致。你也许希望数据库自己就能够避免数据不一致的情况,但是,保持数据一致通常是应用程序的职责。

我们用两个章节描述应用程序如何处理并发更新,这是其中的第 一个章节。本章你将学习基本并发机制,可以用来处理不牵涉用户交互的数据库事务并发更新。我们描述三种并发机制,并描述如何在iBATIS/JDBC、 JDO和Hibernate应用程序中使用这三种并发机制。而且你将学到如何从并发失败的事务中恢复。下一章讲述如何处理长业务(long- running business transactions)的并发更新,长业务通常跨多个数据库事务,而且通常牵涉到用户交互。

12.1  处理共享数据的并发访问

同时执行多个事务的结果,应该和一个一个(任意顺序)串行执 行的结果一样。从数学上来说,如果存在N 个事务,将有factorial(N )即N的阶乘种排列组合的合法结果。这意味着, 在Food to Go订餐网应用程序中,同时执行“发送订单到餐馆”和“取消订单”这两个用例,将有两种合法结果。一种结果是订单发出了,没有取消(因为已经发出了)。另 一种结果是订单取消了,没有发到餐馆。

如果应用程序和数据库都不能保证多个事务同时执行的结果和串 行执行的结果一样,那么数据库的数据就可能变得不一致,应用程序也会出错。一个常见的问题是更新丢失:一个事务把另一个事务的修改盲目地覆盖重写。两个事 务都认为自己已经更新了数据库,却不知其中一个修改已经丢失。例如,Food to Go订餐网应用程序中的更新丢失能够导致这样的结果,一个订单已经取消了,餐馆却不知道,仍然根据这个订单准备了食物。如果是银行,钱就消失了,这种事故 不可能发生在古灵阁里的金子身上——除非有人施了魔法!

另一个常见的问题是读取不一致:一个事务读取了一部分数据, 另一个数据却修改了那部分数据。读取数据的事务在不同时间读取的数据不一样,可能会引起错误。只要应用程序多次查询相同条件的数据,这种情况就有可能发 生。应用程序用多个事务读取关联数据(比如订单和明细条目)的时候,这种情况也可能发生。在两次查询之间,另一个事务可能更改了数据。修改丢失和读取不一 致的问题,还有其他更微妙的问题,请参见《事务处理:概念与技术》(Transaction Processing: Concepts and Techniques [Gray 1993]),这本书提供了更多更详细的信息。

并发访问共享数据有3种主要方法。下面我们逐一考察这些方 法。

12.1.1  使用完全隔离的事务

一种解决方案是使用完全和其他事务隔离的事务,用数据库的话 来说,就是隔离级别为serializable(串行化)的事务。数据库保证:执行多个serializable事务的结果和一个个串行执行它们的结果一 样。serializable事务避免了更新丢失、读取不一致等问题。关于serializable事务和不同数据库支持的细微差别的更多信息,请参见 [Gray 1993]或者你所用数据库的文档。

如你所见,使用serializable事务非常直观。你可 以配置Spring、JDO、Hibernate或者JDBC DataSource,指定它们使用serializable隔离级别。数据库会设法串行执行事务,如果因为某种错误(比如死锁)无法成功执行,数据库就 返回一个错误码。应用程序可以rollback(回滚)并重新尝试执行失败的事务。

Serializable只是数据库提供的事务隔离级别之一 种。一些数据库还提供了repeatable read(能够保持一致的重复读取)隔离级别,从名字可以看出,这个级别保证每次读取相同记录能够得到相同的结果。然而,不像serializable事 务,repeatable read事务执行查询的时候,可能得到不同的结果,因为其他事务可能增加或者删除一些查询条件相关的记录,这叫作phantoms。

使用serialization和repeatable read级别事务带来的的问题是付出了系统性能和规模扩展性(scalability)方面的代价。因为数据库采用锁之类的机制控制对共享数据的并发访 问,降低了系统的并发数。所以,为了提高性能,很多系统使用第3种隔离级别——read committed。Read committed比serializable和repeatable read的隔离级别要小。Read committed无法防止读取不一致和修改丢失。应用程序用乐观锁或悲观锁来弥补这个缺失,本节后面会描述。

优缺点

完全隔离事务有两个主要优点。

n  使用简单。

n  避免了很多并发问题,包括修改丢失和读取不一致的问题。

完全隔离事务的主要缺点是开销太大,降低了性能和规模扩展 性。而且,由于死锁和其他并发相关问题,完全隔离事务比低隔离级别的事务的失败频率更高。

使用完全隔离事务的时机

应用程序应该在如下情况下使用完全隔离事务:

n  读取一致是非常关键的需求。

n  完全隔离事务的额外开销是可以接受的。

典型应用程序很少需要使用完全隔离事务。相反的,应该使用 read committed隔离级别,并结合使用乐观锁/悲观锁。

12.1.2  乐观锁

完全隔离事务的问题在于,不管有没有并发更新,都需要额外开 销。并发更新通常很少,所以理想机制应该是并发更新发生的时候,才需要额外开销。能够达到这个目标的一个常用方法是乐观锁(optimistic locking)。尽管名字里面带“锁”,但是,实际上乐观锁不锁定任何东西。当事务更新记录时,事务会进行检查,看看自从自己上次读取了这条记录之后, 是否有其他事务修改了这条记录。如果被其他事务修改了,这个事务通常会回滚,然后重新尝试。更新数据的时候执行这个代价不高的检查,能够避免弄丢其他数据 的修改。而且,只有发现修改冲突的时候,才引起事务重来一遍的额外开销。

JDBC/iBATIS应用程序必须自己实现乐观锁。不过你 将看到,JDO/Hibernate应用程序使用乐观锁只是简单的配置问题。应用程序照常装载和修改数据,JDO/Hibernate负责跟踪记录实现乐 观锁机制所需要的信息。

跟踪数据修改

应用程序或持久层框架有三种方法可以判断一条记录自从上次读 取出来后是否被修改过。第一种方法是用一个version(版本)字段来跟踪记录修改状况,每次修改,version都会递增。事务只需要把原来读出的 version和当前version进行比较,就可以判断一条记录是否被修改过。应用程序检查和修改version字段是比较简单的做法,通常也是最好的 做法。

第二种方法是用时间戳字段,每次应用程序修改数据,时间戳也 会更新。事务只需要把原来读出的时间戳和当前时间戳进行比较,就可以判断一条记录是否被修改过。这个表结构也很容易实现,尤其是这种情况下,数据表经常已 经有一个时间戳字段来记录用户修改记录的时间。然而,时间戳的问题是,如果两个修改操作之间的时间差小于时钟最小单位,那么一个事务可能覆盖另一个事务的 修改。所以,只有在无法增加version字段的遗留系统中,才应该使用时间戳,否则,尽量使用version(版本)。

第三种方法是把上次读出的字段值和现有字段值进行比较。这种 方法最大的好处是,不需要引入version或者时间戳字段,所以可以用在遗留系统中。这个方法的一个缺点是使得SQL UPDATE更加复杂,因为WHERE子句里面包含所有的字段的条件(具体原因我们后面会详述)。还必须正确处理null字段,可能比较复杂。比如,有一 次我发现,一个持久层框架不能正确比较空字符串,因为Oracle把空字符串认为是null,这和Java不一样。我们在数据表里面增加了一个版本字段, 解决了这个问题。

第三种方法的另一个缺点是,浮点数字段不能精确比较,浮点数 字段的修改可能发现不了。由于这些问题,应用程序只有在别无选择,无法应用版本和时间戳的情况下,才应该使用这种方法。

高效实现乐观锁检查

通过把乐观锁检查放到UPDATE语句里 面,JDBC/iBATIS/持久层框架可以高效地实现乐观锁检查。例如,下面是一条UPDATE语句,修改订单,并用version字段检查修改状态:

UPDATE PLACED_ORDER

SET VERSION = VERSION + 1,

  STATUS = 'SENT'

WHERE ORDER_ID = ? AND VERSION = ?

这条UPDATE语句修改订单状态并增加版本号。WHERE 子句里面检查版本号没有变。如果其他的事务修改或者删除了这个订单,这条UPDATE语句不修改任何记录,执行这条UPDATE语句JDBC PreparedStatement.executeUpdate()方法,返回的修改记录个数为0。应用程序可以检查这个值,发现为0的时候,回滚事 务。使用时间戳或字段比较的UPDATE语句也是类似的。

使用乐观锁

我们来看这样一个场景,一个事务试图发订单到餐馆,而另一个 事务试图取消订单,这个时候如何用乐观锁来防止修改丢失?记住这些都是read committed或者更低的隔离级别的情况下。在图12.1的场景中,两个事务都使用SQL SELECT查询PLACED_ORDER数据表取出订单的版本号。当修改订单的时候,它们验证版本号没有变。

事务A读取订单并把版本号存起来,然后事务B也这么做。然后 事务A使用UPDATE语句来修改订单,UPDATE语句验证版本号不变,并增加版本号。当事务B试图修改订单的时候,UPDATE语句失败,因为版本号 已经变化了。PreparedStatement.executeUpdate()返回0。这个时候,事务B有两个选择。可以回滚重来,或者重读记录,然 后重试一部分操作。两种情况下,它都会发现,订单已经发出,无法撤销。

图12.1  乐观锁如何处理并发更新的例子

优缺点

乐观锁有如下优点。

n  乐观锁在JDBC/iBATIS应用程序中的实现比较简单,而且很多持久层框架都支持。

n  和悲观锁不一样,乐观锁不阻碍应用程序使用某些SQL SELECT语句特性。稍后你将看到,有些数据库的限制,比如,在某些数据库视图和嵌套SELECT语句等情况下,悲观锁不能工作。

当然,乐观锁还存在各种缺点和问题。

n  所有可能冲突的事务都需要使用乐观锁。不然就可能发生错误。幸运的是,使用持久层框架,就没有这个问题,因为乐观锁是在类级别上声明的,能够保证没有遗 漏。

n  实现乐观锁的最简单方法是使用版本字段。但是对于一些我们没有控制权的遗留数据库模式,我们就无法增加version字段。而且,你可能无法修改基于遗留 数据库模式的遗留代码,以便递增版本字段值。

n  乐观锁不保证事务一定能修改读取的记录。如果记录被其他事务修改了,事务就会失败,必须重来,这可能影响效率。

n  乐观锁不能避免读取不一致。幸运的是,很多应用程序能够容忍一定程度的不一致。

何时使用乐观锁

尽管有上述缺点,乐观锁仍然是有用的并发机制。一个具有普遍 意义的建议是,一般情况下应用程序都应当使用乐观锁,存在下面情况的不应该使用乐观锁。

n  数据库模式不支持乐观锁。遗留数据库模式的数据表里面含有不能比较的浮点数字段,而且也没有办法增加一个版本或者时间戳字段。

n  应用程序必须保证能够更新读取的记录。

n  应用程序要求读取的一致性。

12.1.3   悲观锁

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

悲观锁如何工作

获取锁的机制是数据库相关的,并非所有数据库都支持。在 Oracle数据库中,应用程序通过SELECT FOR UPDATE语句锁住选出的记录,从而实现悲观锁。记录一直锁着,直到事务提交或者回滚,锁会阻塞其他事务的更新、删除操作,也阻塞其他事务用 SELECT FOR UPDATE选取记录的事务。这里是一个SELECT FOR UPDATE语句的例子:

SELECT *

FROM PLACED_ORDER o, PLACED_ORDER_LINE_ITEM l

WHERE o.DELIVERY_TIME < SYSDATE

  AND o.STATUS = 'PLACED'

  AND o.ORDER_ID = l.ORDER_ID

FOR UPDATE

这条SELECT FOR UPDATE语句选取并锁住那些状态是'PLACED'并且送餐时间在某个特定时间之前的订单记录。

如果已经有事务锁住记录,执行SELECT FOR UPDATE语句的事务就会被阻塞。如果其他事务更新、删除或者试图用SELECT FOR UPDATE选取这些记录,阻塞情况就会发生。事务一直阻塞,直到事务提交或者回滚。如果事务不想等待,可以采用SELECT FOR UPDATE NO WAIT,如果不能立即锁住记录,就返回ORA-00054错误。你还可以用SELECT FOR UPDATE WAIT <n seconds>来指定等待时间。

使用悲观锁

我们来看这个应用程序如何使用SELECT FOR UPDATE语句来防止sendOrders/cancelOrder场景中的修改丢失。图12.2的场景中,两个事务都用SELECT FOR UPDATE查询PLACED_ORDER表。这个场景中,事务A先执行PLACED_ORDER,锁住记录。事务B执行的SELECT FOR UPDATE会阻塞到事务A提交并释放锁为止。这时候,事务B会发现订单已经发送,无法撤销。

事务可以用悲观锁提供一定程度的读取一致性。因为 SELECT FOR UPDATE读出的记录被锁住了,不能被其他事务删除或者修改。如果该事务再次查询数据库,那些记录不会变化。然而,由于悲观锁不能阻止其他事务插入新的 记录,再次查询可能会返回更多的记录。

图12.2  悲观锁防止并发更新的一个例子

优缺点

悲观锁有如下优 点。

n  不像乐观锁,悲观锁不要求任何数据库模式的变化。

n  阻止一个事务覆盖其他事务的修改。通过锁住读取的记录,事务可以保证稍后更新记录的时候,不会覆盖其他事务的修改(因为其他事务也没有修改的机会)。

n  悲观锁可以用来在这样的场景中保持读取一致性:一个事务从一个表读取记录,但是更新另一个表。一个事务可以用SELECT FOR UPDATE读取记录,但是并不修改这些记录,这样就可以保证事务提交的时候,这些记录保持不变。

n  在实现完全隔离事务的数据库中,悲观锁会锁住读取的记录,减少了死锁的可能性。

但同样的,悲观 锁也有一些缺点和问题。

n  所有可能冲突的事务都必须用SELECT FOR UPDATE,这样悲观锁才能工作,这可能引起错误。例如,sendOrders/cancelOrder的场景中,如果事务B使用一条普通的 SELECT语句,事务B不会阻塞,到头来会覆盖事务A的修改。

n  Oracle这样的数据库中,SELECT一般不锁住记录,增加锁的使用会降低并发性,维护很多锁带来的额外开销会降低性能。增加锁的使用也提高了死锁的 可能性。当两个事务相互等待对方手中的锁的时候,死锁就会发生。Oracle自动判断死锁,并返回ORA-00060

错误,通知参与这 场锁争夺战的其中一个事务。收到出局通知的这个事务或者回滚整个事务,或者痴心不改地重新运行这条引起死锁的SQL语句。其他数据库发出死锁通知的方式, 也是类似的。

n  一些数据库对SELECT FOR UPDATE的用法有限制。例如,Oracle里面,SELECT FOR UPDATE只能用在顶层SQL,而不能嵌在子查询里面。还有一些SQL特性不能和SELECT FOR UPDATE一起使用。这些特性包括DISTINCT,集合统计函数(max、min、sum、count),GROUP BY。SELECT FOR UPDATE也不能用在某些类型的view和嵌套的SELECT里面。当应用程序使用持久层框架的时候,对产生的SQL没有控制权,这种限制的影响就更不 容忽视了。

n  使用持久层访问数据库的应用程序只有在持久层框架支持悲观锁的时候,才能使用乐观锁。应用程序无法在持久层以上自己实现悲观锁。

n  使用悲观锁的应用程序不能使用进程级别的缓存,因为应用程序必须访问数据库来锁住记录。

尽管有这些限制,悲观锁在很多情况下仍然很有用。

何时使用悲观锁

悲观锁应该在如下场景使用。

n  数据库模式由于一些原因不支持乐观锁,比如,数据表没有版本或时间戳字段;或者数据库含有一些不能进行比较的字段,比如浮点数、BLOB。

n  应用程序要求一定程度的读取一致性。

n  你不希望引入serializable事务的额外开销。

12.1.4  锁机制的组合使用

最简单的做法是整个应用程序从头到尾采用同一种并发策略,但 有时你需要组合使用并发策略。比如,若没有特殊情况,你可以对所有事务都使用乐观锁。特殊情况包括如下情况:访问不支持乐观锁的数据表的事务可以使用悲观 锁;需要读取一致性的事务可以使用serializable事务隔离级别。你需要检验每个具体用例的需求,才能作出正确的决定。

现在我们已经概览了完全隔离的事务、悲观锁、乐观锁这三种方 法,下面我们来看如何在企业应用程序中用它们处理并发更新。

分享到:
评论
2 楼 kinson 2012-02-01  
什么时候出下篇
1 楼 56553655 2011-03-28  
好长,写得蛮详细的。

相关推荐

    飞机售票系统中的数据共享和并发访问

    对飞机售票系统中的数据共享和并发访问的描述

    一种动态共享数据结构的并发访问控制分析方法.pdf

    #资源达人分享计划#

    深入理解Java并发之synchronized实现原理.docx

    我们可能需要这样一个方案,当存在多个线程操作共享数据时,需要保证同一时刻有且只有一个线程在操作共享数据,其他线程必须等到该线程处理完数据后再进行,这种方式有个高尚的名称叫互斥锁,即能达到互斥访问目的的...

    并发编程面试题汇总.docx并发编程是指在一个程序中同时执行多个独立的任务或操作的能力 在面试中,常常会问到与并发编程相关的问题

    共享资源和竞态条件:并发编程中,多个线程对共享资源的并发访问可能导致竞态条件和数据不一致的问题。竞态条件指的是多个线程访问共享资源时的执行顺序和时间导致的不确定结果。为了避免竞态条件,需要使用同步机制...

    Java多线程编程 线程同步机制.docx

    线程安全问题的产生是因为多个线程并发访问共享数据造成的,如果能将多个线程对共享数据的并发访问改为串行访问,即一个共享数据同一时刻只能被一个线程访问,就可以避免线程安全问题。锁正是基于这种思路实现的一种...

    负载均衡3中session共享demo

    大量的并发访问或数据流量分担到多台节点设备上分别处理,减少用户等待响应的时间;其次,单个重负载的运算分担到多台节点设备上做并行处理,每个节点设备处理结束后,将结果汇总,返回给用户,系统处理能力得到大...

    Kmalloc 共享内存池技术架构详解-KaiwuDB

    Kmalloc 共享内存池技术架构详解》,KaiwuDB 为优化内存池技术,将内存池分为多个 Heap,每个 Heap 使用不同的数据结构管理内存,在申请和释放内存时,允许多个进程访问同一块内存,使用并发访问控制管理内存释放...

    操作系统--第七章进程同步

    协作进程(cooperating process)不但影响系统中其它的进程,也受...对共享数据的并发访问可能会导致数据的不一致。在本章,我们要讨论 各种确保共享逻辑地址空间的协作进程有序执行的机制,以此来维护数据的一致性。

    论文研究-分布式卫星资源高效共享平台研究.pdf

    针对当前海量卫星资源难以高效共享的问题,采用分布式架构实现卫星资源的整合。各卫星数据中心依据本领域的知识本体构建目录服务,之后...仿真实验结果表明,系统能够实现不同数据中心的资源共享,高并发访问性能良好。

    SpringBoot开发的高并发限时抢购秒杀系统

    传统秒杀系统之所以挂,请求都压倒了后端数据层,数据读写锁冲突严重,几乎所有请求都超时,流量虽大,下单成功的有效流量甚小,我们可以通过限流、降级等措施来最大化减少对数据库的访问,从而保护系统。...

    Java并发编程(学习笔记).xmind

    事件处理器与访问共享状态的其他代码都要采取线程安全的方式实现 框架通过在框架线程中调用应用程序代码将并发性引入应用程序,因此对线程安全的需求在整个应用程序中都需要考虑 基础知识 线程安全性 ...

    并发编程下的锁机制,乐观锁、悲观锁、共享锁、排他锁、分布式锁、锁降级原理篇

    比较悲观,担心拿数据时被别人修改,所以查询时先加锁在修改,保证操作时别人修改不了,期间需要访问该数据的都会等待。 select version from user where id=1 for update  update user set version=2 where id=1 ...

    大数据导论-6.1.4-熟悉大数据处理技术——大数据的处理模式.pptx

    MapReduce批处理 MapReduce设计上具有以下主要的技术特征: 1)向"外"横向扩展,而非向"上"纵向扩展 2)失效被认为是常态 3)把处理向数据迁移 4)顺序处理数据、避免随机访问数据 5)为应用开发者隐藏系统层细节 6...

    Linux 线程间同步机制

    互斥以排他方式防止共享数据被并发修改。互斥锁是一个二元变量,其状态为开锁(允许0)和上锁(禁止1),将某个共享资源与某个特定互斥锁绑定后,对该共享资源的访问如下操作: (1)在访问该资源前,首先申请该互斥...

    RAC日常维护知识

    比如放在共享磁盘上 而各个节点的对数据有相同的访问权限 这时就必须有某种机制能够控制节点对数据的访问 Oracle RAC 是利用DLM Distribute Lock Management 机制来进行多个实例间的并发控制"&gt;在集群环境中 ...

    JAVA实现Modbus RTU或Modbus TCPIP数据采集.rar

    2.多线程之间为更方便的实现数据共享采用了共享相同内存地址空间的形式,并且是并发运行,导致多个线程可能会同时访问或修改其他线程正在使用的变量值,导致安全性,同时如果线程之间相互等待对方拥有的锁,会出现...

    go语言开发技巧入门教程总结.docx

    问题描述:并发访问共享数据时未加锁,可能导致数据竞争和死锁。 解决方案:使用互斥锁(sync.Mutex)、读写锁(sync.RWMutex)或channels进行同步;使用原子操作(sync/atomic包);遵循最小权限原则,尽量减少共享数据。...

    92道Java多线程与并发面试题含答案(很全)

    同步(Synchronization):同步是控制多个线程访问共享资源的方式,以防止数据不一致和竞态条件。Java提供了多种同步机制,包括synchronized关键字、Lock接口和Semaphore类。 线程间通信(Inter-Thread ...

    JAVA并发编程实践-线程安全-学习笔记

    线程安全就是对共享的、可变的状态进行管理,对象的状态就是它的数据,换句话说就是在不可控制的并发访问中保护数据。

    数据库课程设计.pdf

    它支持数据的增加、删除、修改和查询等操作,并提供了事务管理、并发控制等功能,以支持多用户并发访问和数据处理。 从应用的角度来看,数据库在企业管理、社交网络、电子商务、教育管理和医疗管理等领域都有广泛的...

Global site tag (gtag.js) - Google Analytics