【问题标题】:Concurrency control并发控制
【发布时间】:2011-03-10 22:08:11
【问题描述】:

你好
我想知道在 3 层应用程序中实现并发控制的最佳方式? 可能首先想到的是:

  1. 客户想要编辑数据集中的记录。
  2. 向服务器发送请求,请求锁定该记录
  3. 服务器根据锁表接受/拒绝编辑请求

根据这种情况,锁应该引用被锁定的记录和使用该记录的客户端。
客户端必须定期向服务器发送保持活动消息。保持活动状态用于释放锁定的记录,以防我们在编辑操作中丢失了客户端。

我将使用 Delphi 和 datasnap。也许这是一个新手问题,但我必须问!

【问题讨论】:

    标签: delphi datasnap


    【解决方案1】:

    我正在 jachguate 的 Optimistic Concurrency Control 答案的基础上回答 cmets 中提出的问题。

    我更喜欢尽可能使用 OCC,因为实施起来更容易。我将讨论一个使用object persistence framework 的三层应用程序。我的首选方案分为三个级别:

    1. 行或对象级别的控件,其中每个对象上都存储了唯一的版本 ID。如果您尝试更新对象,版本 ID 会自动更改。如果您的版本 ID 与已有的不匹配,您的更新将失败。

    2. 字段或列级别锁定。您发送原始对象的完整副本以及更新的对象。在应用新值之前,更新中的每个字段都会比较实际值和旧值。可以要求用户解决冲突而不是丢弃它们,但这会随着提交中数据量的增加而变得混乱。

    3. 悲观锁定。每个对象都有一个锁所有者,通常为空(对象未锁定)。当您要编辑对象时,您首先将其锁定。这里的问题是锁需要整理,而围绕它的业务规则可能很难看(需要什么超时)。

    这样做的好处是大部分时间都采用低成本的 OCC 路径。对于发生很多但争用较少的事情,好处是显着的。想想仓库中的产品跟踪 - 产品一直在移动,但很少有相同的物品同时移动,而且当它们移动时很容易(剩余数量 = 原件减去我的移除和您的移除)。对于(例如)产品被重新安置的复杂情况,在产品运输过程中锁定产品可能是有意义的(因为这反映了物理情况)。

    当您不得不回退到锁定时,能够通知两个用户并拥有一个沟通渠道通常很有用。至少在有锁时通知想要锁的用户,最好允许他们向锁持有者发送消息,甚至可能允许他们强制锁。然后通知锁丢失者“Jo Smith 已经锁定了你,你丢失了你的更改”。让办公室政治解决这个问题:)

    我通常通过用户投诉而不是错误报告来推动回退流程。如果用户抱怨他们在特定流程中过于频繁地丢失编辑,请更改它。如果用户抱怨记录被锁定太频繁,您将不得不重构您的对象映射以增加锁定粒度或更改业务流程。

    【讨论】:

    • 不错的答案,但它似乎与问题中所述的 DataSnap 无关。 DataSnap 是(或可以是)无状态应用程序服务器,不支持您在此处提到的锁定模式。
    • 那是因为我没有使用过 DataSnap,但我认为设计级别的细节适用。如果需要,可以将无状态服务器设为隐式状态,但这需要更多工作。
    【解决方案2】:

    我在设计我的应用程序时考虑到Optimistic concurrency control,当用户想要编辑它或试图控制并发时不锁定任何记录。

    在处理客户端应用的更新时设置适当的内置数据库锁定功能后,重要的计算和更新在服务器端(应用程序或数据库)完成。 DataSnap 自动事务回滚防止这些锁在发生故障时阻塞其他并发用户。

    使用 DataSnap,您可以通过适当地为您的字段使用 ProviderFlags 来完全控制防止两个用户编辑冲突时的数据丢失。将要自动检查的任何字段的 pfInWhere 设置为在编辑/删除时与读取记录时具有相同的值。

    此外,当发生冲突时,您可以在应用程序服务器(提供程序 OnUpdateError 事件)、客户端(TClientDataSet OnReconcileError 事件)以编程方式做出反应,甚至要求用户进行适当的冲突解决(查看 New 中的 ReconcileErrorDialog项目存储库)。

    同时,恕我直言,避免维护锁定列表、客户端列表、每个客户端列表的锁定、保持活动消息、强大的应用程序服务器故障恢复以及所有可能的故障所需的复杂性,您将以更清洁和更简洁的方式结束更好的解决方案。

    【讨论】:

    • 正是我要建议的。请注意,这在低争用情况下效果更好,但我发现在商业应用程序中经常出现这种情况。如果可以,请使用 OCC 并仅在实际使用证明需要时回退。你可能会发现你从来没有真正回退过,所以在你不得不写之前不要写锁定代码。我使用的回退模式是行级 OCC,然后是字段级 OCC,然后是显式锁定。
    • 我在考虑悲观锁定,因为只编辑一条记录可能需要一些时间并更改很多值。以错误结束会很糟糕,例如对不起,我们无法保存您刚刚所做的更改。
    • @Najem:重新阅读我的答案,你不必以那种错误结束,你只需要换一种方式思考:在发生碰撞时做出反应。如果可以以编程方式确定要做什么,您可以对用户透明地执行它,如果没有,请询​​问用户要做什么而不会丢失她的编辑。恕我直言,它在很多方面都更好,例如因为编码和维护它的复杂性比编写适当的锁定机制要低得多,仅举一个。您将减少最终用户的抱怨,因为有人在编辑要求很高的记录时去吃午饭。
    • @moz:我认为你可以通过适当的设计在高并发环境中有效地使用 OCC(实际上是混合,但锁总是通过事务控制下的健壮数据库机制进行管理),并且它可以扩展比悲观好多了。并发越多,您就越不可能在自己的应用服务器中陷入悲观态度。
    • @jachguate:高并发、高依赖的病态案例我见过几次,所以我很谨慎。最好告诉用户“Sam Jones 已锁定此记录”,而不是试图让他们在事后解决高度冲突的编辑(想想当客户打电话给用户 2 修改工作时,用户 1 设置制造流程细节)。
    【解决方案3】:

    jachgate 提供的方法很棒,而且可能更好,但如果您确实想要实现此功能,则需要在服务启动时创建的服务器上使用TThreadList。使用TThreadList,因为它是线程安全的。您可以在每个表上使用TThreadList,以便最大限度地减少导航列表的性能影响。 要控制锁定的内容,您需要一个已创建并传递给列表的对象

      TLockedItem = class(TObject)
      public
        iPK: Integer;
        iClientID: Integer;
      end;
    

    要进行实际的锁定,您需要这样的东西:

    function LockItem(pPK, pClientID: Integer): Boolean;
    var
      oLockedItem: TLockedItem;
      oInternalList: TList;
      iCont: Integer;
      bExists: Boolean;
    begin
      bExists := False;
      if (Assigned(oLockedList)) then
      begin
        oInternalList := oLockedList.LockList;
        try
          if (oInternalList.Count > 0) then
          begin
            iCont := 0;
            while ((not bExists) and (iCont < oInternalList.Count)) do
            begin
              oLockedItem := TLockedItem(oInternalList[iCont]);
              if (oLockedItem.iPK = pPk) then
                bExists := True
              else
                Inc(iCont);
            end;
          end;
        finally
          oLockedList.UnlockList;
        end;
        if (not bExists) then
        begin
          oLockedItem := TLockedItem.Create;
          oLockedItem.iPK := pPK;
          oLockedItem.iClientID := pClientID;
          oInternalList := oLockedList.LockList;
          try
            oInternalList.Add(oLockedItem);
          finally
            oLockedList.UnlockList;
          end;
        end;
      end;
      Result := bExists;
    end;
    

    这只是您需要的一个想法。您必须使用类似的逻辑执行解锁方法。您可能需要一个客户端列表,以便在连接丢失的情况下保留每个客户端持有的每个 TLockItem 的一个点。这不是一个明确的答案,只是推动方向,以防您想实施这种方法。
    祝你好运

    【讨论】:

    • 对不起,你在这里实现什么?使用 OPF 和悲观锁定,您可能会使用类似这样的方式锁定对象,但您的答案是代码太多,现阶段解释不够。
    • @pascal 你能解释一下这种方法将如何在故障转移/负载平衡场景中扩展吗?我认为您遇到了 1 个应用服务器限制。
    • @moz 和@jachgate 我只是向@Najem 展示一些他可能会看的东西。正如我在自己的帖子中所说,您的实施是要走的路。我只是在展示一些可能性,这样他就可以自己决定做什么。它不适用于故障转移/负载平衡场景,但我们不知道@Najem 需要什么。我们向某人提出的想法越多,他就能做出更好的决定。 :)
    • @Najem 我们不客气。理论讨论很棒,也很重要,但一些代码也确实有帮助。如果您需要任何东西,请随时告诉我。 ;-)
    • 投了反对票而没有评论为什么是彻头彻尾的粗鲁。写这个问题的人感谢我的代码,说它会很有帮助。你没有看到他感谢其他任何人......
    猜你喜欢
    • 2013-08-20
    • 2012-01-25
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-03-10
    • 2010-09-10
    • 2015-06-03
    • 2011-05-07
    相关资源
    最近更新 更多