您好
,我想知道在 3 层应用程序中实现并发控制的最佳方式?可能首先想到的是:
- 客户想要编辑数据集中的记录。
- 向服务器发送请求,请求锁定该记录
- 服务器根据锁表接受/拒绝编辑请求
基于这种情况,锁应该引用锁定的记录和使用该记录的客户端。
客户端必须定期向服务器发送保持活动消息。保持活动状态用于释放锁定的记录,以防我们在编辑操作中丢失了客户端。
我将使用 Delphi 和 datasnap。也许这是一个新手问题,但我必须问!
我在设计我的应用程序时考虑到了乐观并发控制,当用户想要编辑它或试图控制并发时不锁定任何记录。
在处理客户端应用的更新时,在设置适当的内置数据库锁定功能后,重要的计算和更新在服务器端(应用程序或数据库)完成。DataSnap 自动事务回滚防止这些锁在失败的情况下阻塞其他并发用户。
使用 DataSnap,您可以完全控制以防止在两个用户编辑发生冲突时通过适当地为您的字段使用 ProviderFlags 来防止数据丢失。将要自动检查的任何字段的 pfInWhere 设置为在编辑/删除时与读取记录时具有相同的值。
此外,当发生冲突时,您可以在应用程序服务器(提供程序 OnUpdateError 事件)、客户端(TClientDataSet OnReconcileError 事件)以编程方式做出反应,甚至要求用户正确解决冲突(查看新项目存储库中的 ReconcileErrorDialog) .
同时,恕我直言,避免维护锁定列表、客户端列表、每个客户端锁定列表、保持活动消息、强大的应用程序服务器故障恢复和所有可能的故障所需的复杂性,您将以更清洁和更好的解决方案结束。
我正在构建 jachguate 的Optimistic Concurrency Control answer 以回答评论中提出的问题。
我更喜欢尽可能使用 OCC,因为实现起来更容易。我将讨论使用对象持久性框架的三层应用程序。我的首选方案分为三个级别:
行或对象级控件,其中每个对象上都存储了唯一的版本 ID。如果您尝试更新对象,版本 ID 会自动更改。如果您的版本 ID 与已有的不匹配,则您的更新将失败。
字段或列级别锁定。您发送原始对象的完整副本以及更新的对象。在应用新值之前,更新中的每个字段都会比较实际值和旧值。可以要求用户解决冲突而不是丢弃它们,但这会随着提交中数据量的增加而变得混乱。
悲观锁定。每个对象都有一个锁所有者,通常为空(对象未锁定)。当您要编辑对象时,您首先将其锁定。这里的问题是锁需要整理,并且围绕它的业务规则可能很难看(需要什么超时)。
这样做的好处是大部分时间都采用低成本的 OCC 路径。对于发生很多但争用较少的事情,好处是显着的。想想仓库中的产品跟踪 - 产品一直在移动,但很少有相同的物品同时移动,而且当它们移动时很容易(剩余数量 = 原件减去我的移除和您的移除)。对于(例如)产品被重新定位的复杂情况,在产品运输过程中锁定产品可能是有意义的(因为这反映了物理情况)。
当您必须退回到锁定时,能够通知两个用户并拥有通信渠道通常很有用。至少在有锁时通知想要锁的用户,最好允许他们向锁持有者发送消息,甚至可能允许他们强制锁。然后通知锁丢失者“Jo Smith 已经锁定了你,你丢失了你的更改”。让办公室政治解决这个问题:)
我通常通过用户投诉而不是错误报告来推动回退过程。如果用户抱怨他们在特定流程中过于频繁地丢失编辑,请更改它。如果用户抱怨记录被锁定太频繁,您将不得不重构您的对象映射以增加锁定粒度或进行业务流程更改。
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 的一个点。这不是一个明确的答案,只是推动方向,以防您想实施这种方法。
祝你好运