16.企业应用架构模式 --- 离线并发模式

1.乐观离线锁
	通过冲突检测和事务回滚来防止并发业务事务中的冲突。
	一个业务事务的执行通常需要跨越一系列的系统事务。而一旦超出单个系统事务的范围,就不能仅仅依靠数据库管理程序来确保业务事务将会使记录数据处于一致的状态。
  当两个会话同时处理同样的记录时很难保证数据的完整性,还可能丢失对数据的更新。当某个会话正在编辑数据时,其他的会话可能读到不一致的数据。
  	用乐观离线锁可以解决这个问题,方法是要验证由一个会话提交的相关修改不会与其他会话中的修改发生冲突。一个成功的预提交验证在某种意义上可以理解为得到一个锁,
  用来表示它能够成功完成对数据的修改。只要这个验证和对数据的修改在同一系统事务中进行,就可以保证数据的一致性。
  	悲观离线锁假设会话的冲突的可能性很大,从而对系统并发进行限制。而乐观离线锁假设会话的冲突可能性很小,认为会话冲突不经常发生,这使得多个用户同时对一份数据
  进行处理成为可能。

  	1.运行机制
  		乐观离线锁通过检查在会话中读取一条记录后没有其他的会话修改该数据来保证数据的一致性。可以在任何时候获取一个乐观离线锁,但它只在获得该锁的系统事务过程
  	  中有效。因此,业务事务为了不破坏记录数据,必须对它在每个系统事务中的变更集成员申请乐观离线锁。也就是说,只要系统事务中有对数据库的修改,就需要获取乐观
  	  离线锁。
  	  	最常见的实现方式是为每条记录关联一个版本号。当某条记录被装载时,该版本号与其他会话状态一样,由会话本身来维护。获取乐观锁的本质就是将会话数据中的版本号
  	  与当前记录数据的版本号相比较。一旦验证成功,所有变化(包括版本号的增加)就可以提交。防止不一致的记录数据是通过增加版本号来完成的,因为一个拿着旧版本号的会话
  	  无法获得乐观锁。
  	  	在基于关系数据库的系统中,锁的检查是通过在所有的更新或删除记录的sql语句中加上版本号的判别来完成的。用一条sql语句就可以同时获取锁和更新数据。最后一步是
  	  检查业务事务执行的sql语句所返回的条数。行数为1代表成功,0代表记录被更改或者已经被删除,返回行数为0时,要将系统事务回滚以防止不一致的数据进入数据库。这样
  	  一来,业务事务必须要么取消,要么解决冲突并重试。
  	  	除了增加记录的版本号,可以再加上一些诸如谁在何时最后修改了一条记录等信息,以便对冲突进行管理。当通知用户更新由于并发冲突而失败时,一个恰当的应用能够
  	  告诉用户谁在什么时候修改过该数据。使用时间戳代替版本计数是糟糕的方法,因为系统时钟非常不可靠,特别是在应用跨多态服务器时。
  	    另一个方法是在进行更新的sql语句中的where字句包含对所有字段的检查。好处是不用在where语句中用到版本字段,可以在无法为数据库表加上一个版本字段的情况下
  	  使用。问题在于会导致update语句后跟上很长的where语句,也会有性能损失。
  	  	通常实现乐观锁是通过在update和delete语句中加上版本号检查来实现的,但是这样不能防止不一致读。乐观锁没有理由不能检测不一致读。上述的例子中,生成账单的会话
  	  可能要知道它的正确性依赖于顾客的地址信息。因此,它应该对地址也进行版本检查,可以把地址也加到修改语句中。如果通过重读版本号而不是人为的更新来检查不一致读,就
  	  要特别注意在更低的隔离等级下,就必须对版本号进行增量操作。
  	  	粗粒度锁能通过对一组对象使用单个锁来解决某些不一致读的问题。另一种方法是简单的把所有问题的业务事务的所有步骤都放在一个长事务中执行。
  	  	在企业应用中的并发管理问题更是一个领域问题,而不只是技术问题。
  	  	有一个大家比较熟悉的使用乐观锁的系统:源代码管理系统(SCM)。当SCM检测到冲突的时候,它通常会自动合并修改并重新提交。一个高效的合并策略使得乐观锁非常强大,
  	  不仅因为系统的并发程度很高,还因为用户基本上不需要重做任何工作。
  	  	乐观锁仅仅在业务事务提交时最后的系统事务中报告冲突。但通常提前通知冲突会更有效。

  	2.使用时机
  		乐观的并发控制仅适用于业务事务冲突率低的情况。如果冲突很可能发生,在用户结束时提交数据时才通知冲突就不是很友好了。用户最终会认为业务事务总是会失败而
  	  停止使用该系统。悲观锁在冲突率很高的情况下更适用。
  	  	由于乐观锁更容易实现,也不会总像悲观锁那样总是报错,应该在任何系统的业务事务冲突管理中优先考虑。悲观锁可以作为乐观锁的补充,因此不要考虑何时适用乐观锁,而
  	  应该考虑什么情况下有乐观锁还不够。并发管理的正确目标是尽量增加对数据的并发访问,同时减少冲突。

2.悲观离线锁
	每次只允许一个业务事务访问数据以防止并发业务事务中的冲突。
	由于离线并发处理牵扯到为跨多个请求的业务事务操作数据,因此最简单的方案似乎是为整个业务事务保持一个系统事务。很不幸,由于事务系统不适合于处理长事务,因此这种方案
  不能很好的应用。
  	应该首先使用乐观离线锁,然而,它也有自己的问题。如果多个人在业务事务中访问同一数据,其中只有一个人能够正常提交,而其他人注定失败。由于只在业务事务结束时才检测冲突,
  因此那些提交失败的人不得不重做所有的工作,而其中的大多数会发现提交再次失败。
  	悲观锁从一开始就避免冲突,它要求业务事务在对数据进行操作之前必须获得该数据的锁。

  	1.运行机制
  		通过3步来实现悲观锁:决定使用哪种类型的锁,构建一个锁管理对象,定义业务事务使用锁的过程。另外,如果将悲观锁作为乐观锁的补充来使用,需要决定对哪些记录
  	  类型加锁。
  	  	就锁的类型而言,第一个选择是使用独占写锁,只在业务事务获取锁是为了编辑会话数据时才使用该锁。它通过避免两个业务事务同时编辑一份数据来消除冲突。这种锁模式
  	  忽略了对数据的读,因此如果对数据读出的要求不是很高时,应该使用这种。
  	  	如果业务事务必须读出最新的数据,而不在乎它是否要修改该数据,应该使用独占读锁。这要求业务事务仅仅为了读出该数据才获得锁。很显然这种策略势必严重限制系统的
  	  并发性。
  	  	第三种策略结合了上面2种锁,既提供互斥读锁的限制,又有互斥写锁的并发性。称为读写锁,读锁和写锁的管事是获得两种方式长处的关键:
  	  	1.读锁和写锁是互斥的。如果有其他的业务事务获得了记录的读锁,那么该记录就不能再加上写锁;如果有其他的业务事务获得了记录的写锁,那么该记录就不能加上读锁。
  	  	2.并发的读锁是允许的。一个读锁能防止其他的业务事务修改记录,因此尽管记录加了读锁,允许其他的会话读数据并没有坏处。

  	  	在选择合适的锁类型时,应考虑尽量增加系统的并发度,满足业务需求和减少编码的复杂度。还要记住要让领域建模人员和系统分析师明白加锁策略。加锁不只是一个技术问题,
  	  选择错误的所类型,把所有的东西都加上锁,或者加上了不适当的锁,会导致一个低效的悲观锁策略。
  	  	一旦选好了锁类型,就可以定义锁管理对象了。锁管理对象负责授予或者拒绝业务事务对获取或释放锁的请求。为了完成工作,它需要知道锁住的是什么和锁的所有者是谁---
  	  业务事务。很可能你的业务事务概念无法唯一标志,这使将业务事务传递给锁管理对象有些困难。在这种情况下可以参考下会话的概念,因为这时很可能有一个现成的会话对象。
  	  '会话'和'业务事务'被认为是可互换的概念。只要在业务事务在一个会话中是依次执行的,那么会话就可以看成是悲观锁的锁的所有者。
  	  	锁管理对象不应包含一张所有者映射表以外的太多东西。简单的锁管理对象可能只有一张内存散列表,或者是一张数据库表。因此,如果表在内存中,就应该使用单子模式。如果
  	  应用服务器进行了集群,内存中的锁映射表是无法工作的,除非将它固定在单个服务器实例上。在集群应用服务器的环境中,最好使用基于数据库的锁管理对象。
  	  	锁,不管是内存对象还是数据库的sql实现,必须是锁管理对象的私有属性。业务事务只能与锁管理对象打交道,决不能直接操作锁对象。
  	  	现在应该定义业务事务使用锁管理对象的协议了。协议必须说明什么时候加锁,何时加锁,何时释放锁,以及在无法获得锁的动作。

  	  	对什么加锁取决于何时加锁。通常,业务事务应该在读取数据前获得锁,因为没有理由在不能保证加锁的是最新数据的情况下去获得锁。由于是在一个系统事务中获取锁的,也可能
  	  不用关心加锁和读数据顺序的情况。这取决于锁的类型,如果使用的隔离等级是顺序的或者可重复的事务,那么加锁和读数据的顺序就不那么重要了。一种方法是在获取悲观锁后进行
  	  一次乐观的检查,必须确信被加锁的数据是最新的,因而经常是在读数据之前获取锁。
  	  	现在,对什么加锁呢?似乎应该是对记录或者对象,或者任何东西加锁,但实际上通常只对id,或者主键加锁,它们用来查找对象。这样可以在加载数据之前获得锁。锁住对象也能
  	  达到目的,只要不在获取锁之后违反该对象的约束。
  	  	最简单的释放方式是在业务事务结束时释放。在业务事务结束之前释放锁也是可以的,这取决于锁的类型和在事务中使用该对象的意图。然后,除非有非常特别的理由要求提前释放锁,
  	  比如一个特别讨厌的系统灵活性原因,否则应该在事务结束时再释放锁。
  	  	业务事务可能会在一开始就由于无法获取锁而终止。这对用户来说是可以接受的,因为悲观锁就是尽可能早的检测出冲突。
  	  	对任何想加锁的对象,对锁表的访问必须是序列化的。对内存中的锁,很容易通过编程语言提供的机制对锁管理对象的访问序列化。如果锁表存储在数据库中,第一条要注意的就是在
  	  一个系统事务中操作它们。要充分利用数据库系统提供的序列化能力。在独占读锁和独占写锁中,最好对存放加锁对象的id列设置唯一性约束。在数据库中存放读/写锁要麻烦一些,因为
  	  除了向表中插入数据,还要进行读操作,这样就必然导致不一致读。一个序列化隔离等级的系统事务能保证不产生不一致读。而使用这样的系统事务会影响性能,但只能对获取锁的事务采用
  	  序列化读,而对其他的事务采用弱一些的隔离等级消除部分影响。另一种选择是看能否用存储过程来解决问题。
  	  	锁管理时的顺序性导致了性能瓶颈,可以考虑一下加锁的粒度,因为锁越少瓶颈就越少。
  	  	在一个使用悲观锁模式的系统事务中,比如'select for update ...',很可能出现死锁,因为这些锁机制会一直等到锁可用为止。在处理跨系统的事务时,等待一个锁是没有什么意义
  	  的。只需要让锁管理对象在锁不可用时抛出异常就可以了,这样就没有和死锁打交道的麻烦了。
  	  	最后需要处理的是那些丢失的会话中的锁的超时。如果客户端在事务进行的中途中垮掉了,这个丢失的事务就无法完成了,从而无法释放它占有的锁。理想的情况是让应用服务器的超时
  	  机制来处理,而不是应用自己处理。另外一个方法是给每个所加一个时间戳,定期清除哪些超过一定时间的锁。

  	2.使用时机
  		悲观锁适合用于在冲突很高的并发会话中。用户不用丢弃已经完成的工作。悲观锁也适用于在冲突处理代价很高的情况下,而不管冲突的发生概率如何。对系统的每一个实体加锁必然导致
  	  频繁的数据竞争,因此要记住悲观锁只是作为乐观锁的补充,只在真正需要的时候才是用欧冠悲观锁。
  	  	如果不得不使用悲观锁,还可以考虑下长事务。使用长事务绝不是好方法,但是有些情况下要比使用悲观锁要好,也简单。请在选择前做一下负载测试。
  	  	当业务事务能在单个系统事务中完成时,不要使用介绍的方法。很多应用服务器和数据库服务器的系统事务自带了悲观锁机制,其中包括数据库中的'select for update'的sql语句和
  	  应用服务器中的实体EJB。

3.粗粒度锁
	用一个锁锁住一组相关的对象。
	通常,总是按组来修改多个对象。粗粒度锁是覆盖多个对象的单个锁。

	1.运行机制
		实现粗粒度锁的第一步是为一组对象建立一个控制点。这使得只需要用一个锁就可以锁住一堆对象,然后尽量提供最直接的方法查找到他们的锁,从而减少在获取锁时为了标识该组而
	  读取的对象数目。
	  	用乐观锁让组中的每个对象共享同一个版本号来建立一个控制点,这意味着它们共享同一个版本号,而不是说他们的版本号相同。增加这个版本号时,就成为一个锁住组中的所有对象的
	  共享锁。
	  	一个共享的悲观锁要求组中的每个成员共享某种锁标记,通过这个锁标记锁住它们。由于悲观锁常常作为乐观锁的补充来使用,一个共享的版本对象很适合作为锁标记。
	  	把一簇相关对象看成一个聚集,作为数据修改的基本单位。每个聚集有唯一的,提供对集合中各个成员访问的根对象,以及定义聚集中究竟包含什么的边界对象。聚集的特性需要使用
	  粗粒度锁,因为其中任何一个成员的访问都需要对整体加锁。锁住一个聚集提供了另一种我们称之为根对象锁的共享锁。锁住根对象就锁住了聚集中所有的对象。根锁提供了单一控制点。
	  	要使用根锁来作为粗粒度锁,就必须为聚集对象提供到根对象的导航方法。

	2.使用时机
		使用粗粒度锁最明显的理由是为了满足业务需要。使用粗粒度锁的一个好处是获取和释放锁的代价很小。

4.隐含锁
	允许框架或层超类代码获取离线锁。
	对于任何加锁模式,关键是要与他们的应用紧密结合。在获取锁时少些一行代码,可能会导致整个离线锁设置失去作用。
	一种解决办法是不给开发人员犯这种错的机会。必须的加锁任务不应该显示的由开发人员完成,而应该隐式的由应用完成。大多数企业应用都使用了一些框架,层超类型和代码生成组成,
  这样就很容易在其中加入隐含锁。

  	1.运行机制
  		要实现隐含锁就是要分解代码,在应用程序框架中完成那些绝对不能跳过的锁机制。
  		第一步是列出业务事务中哪些任务是必须在加锁的情况下工作的。对于乐观锁来说,任务包括为每条记录数据存储版本计数,在修改操作的sql语句中包含版本检查和修改数据时的版本
  	  增量等。对于悲观锁来说,任务包括为读取数据而进行的一系列获取锁的动作 --- 典型的如互斥读锁和读/写锁中进行的读的部分---和在业务事务或会话结束时释放所有锁的动作。
  	  	其二,也同样重要,这种锁最大程度的限制了系统的并发度。
  	2.使用时机
  		除了最简单的,没有框架的应用以外,隐含锁基本上都使用。

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值