zoukankan      html  css  js  c++  java
  • 事务(业务事务/系统事务)并发策略选择

    乐观锁:

    乐观锁示例http://xmuzyq.javaeye.com/blog/295639
    方式1.JDBC通过下面的语句:
    Select  a.version....from Account  as a where (where condition..)
    Update Account set version = version+1.....(another field) where version =?...(another contidition)

    方式2.HIBERNATE:
    public class Account{
        Long id ;
        .......
        @Version  //也可以采用XML文件进行配置
        Int version
        .......
    }
    hibernate内部会生成相应的SQL语句将版本字段加1,并且进行相应的版本检测,如果检测到并发乐观锁定异常,那么就抛出StaleObjectStateException.


    相比于乐观离线锁,乐观锁相当相当好理解。乐观锁与悲观锁都发生在一个系统事务的范围内,不会象离线锁跨N个系统事务。实现也是利用了原句1.乐观锁可以省去checkConcurrent.省去了保存第一次请求后将verson放入http session。

    比较1:
    JDBC使用version字段,hibernate使用@version注解。
    hibernate的@version就是将使用JDBC情况下应用程序需要手动做的事情交给了框架自动化。比如jdbc事务中,涉及到并发修改的update sql语句同时需要手动更新version字段的值(version=version+1 where version =?),进而判断update影响的行数是否为1.如果不为1,说明在修改的过程中,有其它事务修改了该记录,此时应用手动抛出自定义的乐观锁定异常(或者也可以采用spring封装的异常体系),而hibernate减少了人为去做这样的事情。它会自动生成带version更新的update sql.

    比较2:
    version针对的数据源是DB中的记录。而针对内存中的对象,则使用锁定赋值或CAS。version与CAS比较相似,一般CAS可以放到一个while循环里,当compare=false的时候,重新从内存同步一个值过来,再进行CAS,直到在该线程赋值之前,没有其它线程修改,然后赋值成功。参java_example。对于version,同样可以捕获并再次重新执行update操作,直到update返回的更新行数为1(即不会在提交更新的时候,发现已经被修改而抛出异常).

    悲观锁:单个系统事务内,锁定访问的查询记录集。直到更新完毕后,才释放锁。一旦锁定,别的系统事务如果要操作记录集里的部分记录,则只能等待该系统事务释放锁定。 实现都是利用了DB提供的实现。
    悲观锁较悲观离线锁简单很多。悲观离线锁跨多个系统事务且需要应用程序来控制锁的释放和获取。


    悲观锁与事务隔离级别关系:
    1.悲观锁可以加强事务的隔离级别。比如本身事务是低级别的读取已提交或者更低,但是当事务使用了悲观锁之后,相当于在其基础之后做了增加。变成可重复读的级别。
    具体参http://xmuzyq.javaeye.com/blog/295639
    这很好理解,因为事务在第一次查询之后,会对[[[选择的行加排它锁]]]。注意,并不是针对整个表加锁。这样当有新的insert以及delete非选中的记录是允许的,这是出现幻影读的原因,也指悲观锁不能增加到避免幻读,如果能避免幻读,相当于增加到了序列化级别。

    2.如何避免幻影读的问题
    要想避免我们就需要设置数据库隔离级别为Serializable,而一般情况下由于伸缩性都会采取读取已提交或者更低隔离级别,同时配合乐观或者悲观锁来实现并发控制。所以,幻读问题在考虑性能的前提下是不能避免的。而幸运的是幻影读问题一般情况下不严重。


    DB实现悲观锁原理:
    1、使用select ... where ... for update时,可以同时进行insert操作么?我尝试过好像可以。
    2、使用select ... where ... for update,只有执行commit后,才能继续使用select ... where ... for update执行?如果同时有两条:select ... where ... for update,那肯定只有一条在执行,当commit后,另外一条才能继续执行?我尝试过好像可以
    规则是:FOR UPDATE语句将锁住查询结果中的元组,这些元组将不能被其他事务的UPDATE,DELETE和FOR UPDATE操作,直到本事务提交。
    所以
    1、可以,原因就是for update不会锁定整个表,而只会锁定select出来的行。
    2、是的

    比较1:乐观锁与悲观锁的选择---成本
    重新执行业务的成本与悲观锁定的成本(在java应用中使用synchronized导致的线程切换成本,而在数据库事务里则为使用悲观锁的成本)进行比较。如果前者大于后者,那么就要选择悲观锁,如果小于后者,那么使用乐观锁。这与乐观离线与悲观离线的选择是一样的。悲观锁与乐观锁的选择与锁定还是CAS的成本比较相同,选择锁定还是CAS取决于线程切换的成本与业务执行的成本比较,比如一个计数器的设计,使用synchronized会导致线程的切换成本高于简单的业务成功(累加)。而使用CAS的乐观锁定方式则将线程的切换成本降了下来。
    比较2:悲观锁针对库记录,synchronized针对内存对象,二者非常相似。


    悲观锁的示例:http://xmuzyq.javaeye.com/blog/295639
    1.JDBC中使用悲观锁:
    每个冲突的事务中,我们必须使用select for update 语句来进行数据库的访问,如果一些事务没有使用select for update语句,那么就会很容易造成错误,这也是采用JDBC进行悲观控制的缺点。
    假如我们系统中有一个Account的类,我们可以采用如下的方式来进行:
    Select * from Account where ...(where condition).. for update.


    2.Hibernate中使用悲观锁:
    在hibernate中使用悲观锁将会容易很多,因为hibernate有API让我们来调用,从而避免直接写SQL语句。
    首先先要明确一下hibernate中支持悲观锁的两种模式LockMode.UPGRADE以及LockMode.UPGRADE_NO_WAIT.(PS:在JPA中,对应的锁模式是LockModeType.Read
    假如我们系统中有一个Account的类,那么具体的操作可以像这样:
    ......
    session.lock(account, LockMode.UPGRADE);
    ......
    或者也可以采用如下方式来加载对象:
    session.get(Account.class,identity,LockMode.UPGRADE).
    这样以来当加载对象时,hibernate内部会生成相应的select for update语句来加载对象,从而锁定对应的记录,避免其它事务并发更新。


    乐观离线锁:每个记录都会一个版本对象(表会有一个version的列,或者如果version信息多的话,比如包含计算器等,可以单独抽取成一个version表。与记录的表有一个关联关系。)。第一次读取对象之后,版本号放入http session里。以供第二次修改请求时,session取出头一次的版本号与从数据库重新读取的版本号进行比较,如果一致,执行更新操作。

    理解:checkConcurrent与系统事务最后一步的更新语句中的版本号的更新
    还是以最简化的二个请求的业务事务为例:第一个请求的系统事务将版本号存入http session。假设用户在发送第二个请求之前,思考了N秒。发送第二个更新的请求。
    最初的认识:
    第二请求对应系统事务中的checkConcurrent成功,并且以为checkConcurrent与更新sql在一个系统事务里,就认为更新sql就会成功。从而认为更新sql中的version=version+1只是用来记录版本号,没其它作用----这是对带版本更新的sql语句最大的误解。
    更新理解:
    考虑到在场景中,有二个时间段可能会修改共享数据。第一个时间段是二个请求之间。第二个时间段发生在更新请求的系统事务的执行过程中,前提是事务隔离级别低于可重复读。
    所以checkConcurrent与sql中的version=version+1这就象一个双保险一样。checkConcurrent可以理解为第一保险,就象方法中一开始会有参数的校验一样,第一时间判断第一个时间段内是否有其它业务事务的系统事务修改了共享数据。进而判断是否有接着执行的价值。version更新可看成是第二保险,在第二时间段内,如果事务的隔离级别低于可重复读,其它的事务可能会修改共享数据,这样带版本号更新的sql语句可以进行最终的check。

    马丁的原话1:通常实现乐观离线锁是通过update或delete语句中加上版本号检查来实现。
    马丁的原话2:可以将版本号比较的过程理解为获取锁,马丁还讲到如果没有checkConcurrent,如果原话1的sql能成功执行,也可以理解为[[[获取锁并执行sql]]]。
    我看POEAA,有时候,被这二个获取锁混淆,后来干脆,版本号比较不称为获取锁,而称为checkConcurrent.用来在执行更新sql之前,先做一次拦截判断,提前排除出现并发的修改的可能。1是乐观离线锁的核心原理。光使用1就完全可以实现乐观离线锁。


    乐观离线锁可以阻上并发修改,但可能不能防止不一致读:马丁讲并发会导致二个问题,一是并发修改,二是不一致读。 不一致概念读参阅POEAA 5.5.3。 对于乐观离线锁产生不一致读的场景参新建帐单时读用户地址计算税率。
    乐观离线锁对不一致读的解决:
    一种是重读版本号。类似于第一保险方式,同时保证事务的隔离级别必须是可重复读以上。可重复读的级别会在事务执行的过程中阻止其它用户修改自己的地址,可以保证计算帐单的用户的事务中读到的用户地址恒定的,也用户修改地址之前的数据。
    另一种方式是与解决并发更新的方式类似,对版本号进行增量操作。但是这无疑增加了复杂度,因为建单用户只是想正确的读一下用户的地址,而不得不做地址更新的修改来辅助判断不一致读。
    GYB:我觉得比较好的方式还是将该事务的隔离级别设置为可重复读外加重读版本号检查地址信息好理解。虽然可重复读会影响一些性能。


    隐式锁类似于将原句1的操作语句由应用层抽象到了泛型DAO里面进行。避免应用层开发人员写sql时忘记加(version=version+1 where versoni=?)操作而导致的问题。


    这样的乐观离线锁是在应用程序中进行了版本比较。注意,checkConcurrent与更新操作必须要在一个系统事务里才能保证一致性。如果将版本检查直接绑定到update 或delete的语句中,则没有这个问题。

    乐观离线锁示例:
    1.
    马丁乐观离线锁的示例中,没有系统的实现一个业务事务下的乐观离线锁,比如没有通过http sessoin保存版本状态,没有实现第一保险。示例围绕如何使用原话1避免并发修改的问题。所有的更新语句的结果用来判断是否获取锁成功(由于是第二保险,成功意识着获取锁并执行sql。看来以前的”获取锁“+更新sql中的"获取锁"要换个名称了,比如checkConcurrent更合适。因为那个“获取锁”并不一定会让更新sql一定成功,但能起到提前拦截的作用)。并且它希望由最初的应用层构造带版本更新的sql语句抽取到层超类型(比如我们讲的泛型DAO)。避免应用层的开发人员忘记构造带版本更新的sql,统一由下面的持久层做这样的工作。这也是隐式锁概念。锁对应用开发人员是透明的。

    2.
    http://xmuzyq.javaeye.com/blog/295639中举例更适合理解乐观离线锁,一种是应用层控制,将checkConcurrent(版本比较)+更新sql放到一个系统事务里(其实上面也提到里面的checkConcurrent有没有都可以,只要更新sql里有version=version+1 where versoni=?,有checkConcurrent可以提前拦截并发修改的情况)。另一种方式,如果在持久层使用的hibernate,可以利用session.update(detach object),底层的实现原理应该与原句1类型。

    问:如果一个业务事务包含多个系统事务,并且其中的多个有更新的操作,当最后一个修改事务提交失败之后,之前提交成功的系统事务如何rollback呢?
    最初理解:后来我觉得不应该存在这样的一个业务事务。每个业务事务都应该是N-1个查询+1个更新。所以,前面的情况解决为多个业务事务。

    最后理解:通过阅读hibernate文档发现,如果之前的事务中包含了修改,可以设置session的FLUSHMODE=Never来避免commit的时候,把变化flush到数据库,而在最后一次系统事务中,手动flush来把之前所有的更新一次性刷新到库,然后commit该系统事务。

    悲观离线锁
    悲观离线锁对共享记录集可选三种锁类型
    1.独占写锁
    独占写锁忽略了对数据的读,因此不能保证读出最新的数据。它用在二个业务事务同时修改一份共享数据时消除冲突。适用的场景是存在并发修改冲突,但系统对读的要求不是很高比如允许读不是最新的值,可以考虑使用独占写锁。
    2.独占读锁
    独占读锁“仅仅”为了读出最新的数据才使用。显然这种锁会限制系统的并发性。毕竟通常系统读远远大于写,光为了读新数据牺牲了这些性能,有些得不偿失
    独占写锁的性能强于独占读锁。
    3.结合上面二种特点的读写锁:读锁与写锁是互斥的。读锁可以并发持有。

    明确了锁之后,就可以定义锁管理对象,它包括的属性有:私有的锁以及一个持有锁的数据结构,这个数据结构可以对应一个内存散列表或是一张库表。应用层只能通过锁管理对象操作锁,而不能直接访问锁。

    这个数据结构可以理解为key-value的结构,比如{业务事务1--锁1,.......业务事务N--锁N},业务事务N+1要使用锁1,必须等到里面的锁1被业务事务1释放才可以。
    而由于不方便把业务事务做为参数传给锁管理器,则使用概念偷换,由于会话与业务事务可以等同理解。所以,可以把用户的sessionid做为key.

    示例参POEAA

    粗粒度锁:如果一个客户与一组地址信息相关。如果有业务事务修改这组地址中的任意一个时,都需要将整个一组地址都锁定住。(我的理解是避免其它业务对象修改了另一个不同地址并提交导致相互复盖)而为每个地址对象加一个锁很费事,此时考虑将用户的version值设置为每个地址的versoin。这样锁定了用户也就锁定了它的地址集合。



    POEAA第五章提到一些关于事务与并发策略的概念:

    离线:系统的很多交互并不是在一个数据库事务中能完成的。这就引出在跨事务的处理中管理好并发问题。这样的问题称为离线并发(offline concurrency)
    并发修改最典型的场景是SCM。企业应用中的并发修改场景是什么呢? 多个用户具有相同的权限。而这些权限会操作某个文件或某个库表。


    事务可以是N条语句的执行单元。事务必须要有明确的开始与终止的指令定界。事务最佳的执行方式是一个请求对应一个事务。


    业务事务不是说不能放入一个长的系统事务中执行,只是满足了可用性,不能满足可伸缩性。


    深刻理解事务的ACID
    A:automicity
    C:consistency
    I:isolation
    D:durability

    系统事务的ACID(参POAEE)

    业务事务的ACID(较少接触)
    最容易做到的是AD。A是因为当用户点击save按钮,想要提交其所有的修改时,业务事务会先启动一个系统事务。系统事务将保证被修改的数据将做为一个单元而提交,并将被持久化。唯一要担心的是业务事务的生命周期内,维持一个正确的修改集。所以,AD可以保证。
    业务事务最难保证的是I,因为没有隔离性从而导致没有一致性,一致性要求是不要将记录集置于一种无效的状态下。但是业务事务在其周期里,存在被其它业务事务修改共享记录集导致记录集无效。


    应该尽可能的让事务系统自己来处理并发问题,跨系统事务并发将让你掉入自己处理并发的黑水里,里面满是攻击性的动物。只有在不得已的时候,才将业务事务分成多个系统事务,如果能将业务事务放到单个系统事务中完成,就这样做。如果可以忍受长事务带来的可伸缩性的问题,那就这样做。通过把并发事务处理交给事务软件(主要指数据库)可以避免很多麻烦。


    乐观离线锁的局限是当提交数据时,才发现业务事务将要失败,而失败的成本可能会很大。比如用户花了一个小时输入一份租约而导致提交失败。(灵感,使用乐观离线锁时,如果要求用户输入过多的内容。可以要求提示用户在内容提交之前先保存一下,以便提交失败后,copy重新提交)。优点是提供了最好的灵活性。
    如果数据提交失败的成本很大的情况下,才考虑使用悲观离线锁。它的优点是最早的时间发现数据已经被修改,否则在业务事务的生命周期内,其它业务事务是无法修改共享记录集的,直至它提交成功。它的缺点是实现难度大,降低系统的灵活性。

    除了关注的业务事务并发,还有另一类指应用服务器的并发:与应用事务并发不一样的地方不经过系统事务访问共享的数据源。而是针对内存对象中状态的并发更新。
    从隔离性最好的request/Process到性能最好的request/Thread到尽量每个请求创建一个新的业务对象。而不共用业务对象。象struts2中的方案一样。

    注:上面的笔记主要来自POEAA以及http://xmuzyq.javaeye.com/blog/295639的思考。

  • 相关阅读:
    2014025640《嵌入式程序设计》第二周学习总结
    基于Struts2的SpringMVC入门
    2014025640《嵌入式设计》第一周学习总结
    Hadoop综合大作业
    hive基本操作与应用
    用mapreduce 处理气象数据集
    熟悉常用的HBase操作,编写MapReduce作业
    爬虫大作业
    熟悉常用的HDFS操作
    中文词频统计
  • 原文地址:https://www.cnblogs.com/highriver/p/1901473.html
Copyright © 2011-2022 走看看