zoukankan      html  css  js  c++  java
  • .Net中锁在企业应用架构中的应用之道

    首相我们讨论一下并发性问题,然后讨论处理乐观锁的3种方法,乐观锁不能从根本上解决并发问题,由此而产生了悲观锁,我们对各个级别列举了实例进行说明,使概念更清晰。

    为什么使用锁?

       在多用户环境下,在同一时间可能会有很多个用户跟新相同的记录,就会产生冲突。这个就是就是并发性问题,我们图来展示下:

    并发性问题会造成什么矛盾?

    并发性一般会造成四种常见的矛盾

    1、更新丢失(Lost Update) :两个会话都同时更新同一个资源,但是第二个会话却覆盖了第一个会话事务的更新结果。

            比如,两个用户A,B同时要修改值5,A将5改成3,B用户将5改成了2,然后A先提交了结果,B后提交,这样就造成了A用户更改丢失了。

    2、脏读取(Dirty Reads):一个会话开始读取了资源,但是另一个会话已经更新了此数据,也就是当一个会话读取了其他会话中的资源,或者会话前资源。

            比如,两个用户A,B同时读值5,A的到值5,B用户将5改成了2,然后还未提交,当然也可能提交了,这时候数据为2,但是A的得到的值是5.

    3、不可重复读(Non-Repeatable Reads):一个会话对同一个资源进行了重复的读取,但是却得到了不同的结果。

            比如:用户A第一次读取值5,在第二次读取之前,用户B将5改成了2,这时候用户A第二次读取的时候就成了2了,就出现了不可重复读。

    4、幻读(Phantom Reads):也成幻像。会话在操作过程中进行了两次查询,第二次查询结果包含了第一次查询中未出现的数据,出现了虚的数据。

            比如:用户将A所有值从5改为2,但这时候B正巧插入了一条2的记录,用户A再读取的时候就出现了B用户插入的数据,产生了虚幻的数据。

    如何解决并发性问题?

    借助正确的锁定策略可以解决并发性问题,我们提供两种机制:乐观离线锁(Optimistic Offine Lock)通过冲突监测和事务回滚来防止并发生业务事务中的决定;悲观离线锁(Pessimisitc Offine Lock)每次只允许一个事务访问数据以防止并发业务事务的冲突。

    乐观离线锁(Optimistic Offine Lock):解决方法就是验证由一个会话提交的相关修改不会与其他会话中的修改发生冲突。一个成功的预提交验证在某种意义上可以理解为得到一个锁,用来表示它能够成功完成对数据的修改。只要这个验证和对数据的修改在同一个系统事务中进行,就可以保证数据的一致性。

    悲观离线锁(Pessimisitc Offine Lock):和乐观离线锁相比,悲观离线锁正好相反,悲观离线锁先假设会话冲突的可能性很大,从而对系统并发性进行限制。先将资源锁定,其他进程想访问它就被阻止。

    两种锁的应用视场景而定,一般当会话冲突的可能性小的情况下,我们使用乐观离线锁,因为如果冲突很可能发生,在用户结束工作提交数据时才通知是很不友好的。用户最终会认为业务总是会失败而停止使用该系统,悲剧离线锁在冲突率很高或者冲突的代价很高时更适用。

    由于乐观锁更容易实现,也就不会总像悲观离线锁那样总是报错,应该在任何系统的业务事务冲突管理中优先考虑。悲观锁可以作为乐观锁的补充,因此不需要考虑何时使用乐观锁,而应该考虑什么情况下光有乐观锁还不够,并发管理的正确目标是尽量增加对数据的并发访问,同时减少冲突。

    乐观离线锁的工作方式:

    实现乐观锁的方式有很多种,但基本原则都一样,一般分为五个步骤:

    1、记录当前的版本记录器

    2、开始修改值

    3、在更新前,检查是否其他人更新了值(通过对比版本记录器实现)

    4、如果不相等就回滚,否则就提交

    提示:有很多喜欢用时间戳来对比,但是从系统架构角度来讲,这并不是很好的设计,因为系统时钟是非常不可靠的,特别是在应用跨平台服务器时。

    当然也可以记录修改前数据,提交时和资源中的数据进行对比,这种方式有性能耗损,尤其当资源中单挑数据量比较大的情况下。

    我们来看序列图:

    在.Net中,实现乐观离线锁的方法主要有三种:

    1、数据集(dataset):数据集市实现乐观锁的默认方法,在更新前它会检查新旧值。

    2、另一种方式就是我们自己在库表里面新建version字段,利用逻辑比较。

    3、直接检查新旧值,在更新旧值和新值是否相等。如果不相等就会滚。否则提交。

    通过序列图可以看出,该方法虽然简单可行,但是又会出现一个新的问题,就是不能防止不一致读,我们只是通过版本比较进行了新数据是否能录入,但不能保证在录入以前时候别的用户已经读取了该数据,从而产生了不一致读问题。

    为了解决此问题,我们需要在查询的时候也加上版本比较,这样的话就不会产生不一致读的情况了,但是你想过没有,这时候又会引出一个新问题:不可重复读,比如,当我们在当然版本读取到了数据,然后第二次读取数据之前,别的会话对数据进行了更改,我们第二次读取数据的时候和第一次读取的数据就不一致了,或者说读不到了。

    到这里我们就需要引入跟高级别的隔离等级。比如,我们可以使用粗粒度锁通过对一组对象使用单个锁来解决某些不一致读问题,另一种方法就是把所有的问题描述成一个长事务来执行。对于逻辑简单使用长事务还是值得的。

    但是在这种场景中就不适用了,当事务所依赖的不是某些特定的记录,而是动态的查询结果时,检测不一致读比较困难了。与其他锁模式一样,对于企业应用领域的某些棘手的并发和时序问题,乐观离线锁本身并不能提供充分的解决方案。为此,出现了悲观离线锁。其实在我们程序开发所应用到的vss和源代码管理器(SCM)就是一个典型的悲观离线锁和乐观离线锁的解决方案。

    悲观离线锁的工作方式:

    悲观离线锁一开始就独占资源避免冲突。每次只允许一个业务事务访问数据以防止并发业务事务中的冲突。

    当我们业务场景中涉及多个会话请求操作数据,在我们程序设计时最简单的方式似乎就是将这些会话请求整个业务事务保持一个系统事务来进行。比如应用数据库中的系统事务操作,但是这种事务操作有一定的局限性,不适合于处理长事务,因此有些场景就不适用了。出于这个原因我们需要自己来管理数据的并发性访问。

    在实际应用中,我们首先使用的是乐观离线锁,当然它也有自己的缺陷,例如,如果多个人在业务事务中访问数据,其中只有一个人能正常提交,而其他则必定失败。由于只在业务事务结束时才检测冲突,因此那些提交失败的人不得不重新所有的工作,而其中的大多数会发现提交再次失败,这种系统显然是不友好的。我们采用悲观离线锁一开始就避免冲突,要求业务在对数据进行操作前必须获取该数据的锁,因此大多数情况下,一旦开始了一个业务事务,就能确信不会由于并发冲突而返回提交的数据。

    在使用悲观离线锁时,我们先来查看下锁的类型

          锁                                       使用场景                                                 
    独占写锁 只在业务获取锁是为了编辑会话数据时需要使用该锁,避免两个业务事务同时编辑一份数据来消除冲突
    独占读锁 这要求事务仅仅为了读出数据才获取锁。策略识别重限制系统的并发性
    读/写锁 第一种情况,读锁和写锁是互斥的,也称排它锁,当读取的时候禁止写,当写的时候就禁止读取
    第二种情况,并发的读锁是允许的,也称共享锁,可以同时读取多个文件

    当然还有其他种类的锁,基本都是有独占写锁和独占读锁的业务逻辑来设计,在业务场景中分情况而做决定,正是同时允许多个读锁增加了系统的并发性。缺点是这种模式实现起来有点麻烦,而且给领域专家在给系统建模时增加了麻烦。

    在选择合适的锁类型是,应该考虑到尽量增加系统的并发性,满足业务需求和减小编码的复杂度。还要记住要让领域模型建模人员和系统分析师明白加锁策略。加锁并不只是一个技术问题,选择了错误的锁类型,把所有的东西都加锁,或者加上了不适当的锁,会导致一个低效的悲观离线锁策略。低效的悲观离线锁策略是指那些在业务事务涌现的时候不能正确的阻止或降低多用户系统的并发性使其看起来更像一个单用户系统的策略。错误的加锁策略是不能靠运用技术适当的技术实现挽回。当然,有的场景这种锁应用还是必然的。

    文件下载

  • 相关阅读:
    移动端网络优化
    性能优化之Java(Android)代码优化
    性能优化之布局优化
    性能优化之数据库优化
    Android性能调优
    Android性能检测--traceview工具各个参数的意思
    RDIFramework.NET ━ .NET高速信息系统开发框架钜献 V2.9 版本震撼发布
    【C++基金会 04】vector详细解释
    Chromium-Dev一些缩写
    怎么样CSDN Blog投机和增加流量?
  • 原文地址:https://www.cnblogs.com/zhijianliutang/p/2404113.html
Copyright © 2011-2022 走看看