如何让 Firebird 客户端应用程序等待行解锁

How to make Firebird client application wait for row to unlock

我熟悉使用 ADO (dbGo) 的 Microsoft SQL 服务器世界,并且我已经为该环境编写了许多应用程序。现在我有一个遗留的 Delphi 7 应用程序,它带有我必须维护的 Firebird 2.5 数据库。

但我发现如果 2 个客户端应用程序执行此操作:

SQLQuery.SQL.Text := 'Update mytable set field1 = 11 where keyfield = 99'
SQLQuery.Execute;

几乎同时,第二个应用程序立即收到 "deadlock" 错误。在SQL服务器中,会有一个等待期

ADOConnection.Isolationlevel = ilCursorstability;
ADOConnection.CommandTimeout := 5;

在第二个客户端应用程序引发任何异常之前。异常处理可能涉及在批处理过程中被视为非常不寻常的情况下的回滚。这是合理。 5 秒在计算机处理时间中是一个非常长的时间。

现在我尝试在 Firebird Client 上使用相同的方法没有结果,因为 "deadlock"(实际上是正在使用的记录)立即发生。

如果数据库引擎不能配置为等待条件改善(记录锁被释放),责任现在必须落在客户端应用程序开发人员身上,他们必须编写极其缓慢的代码来克服出现的问题对我来说是 Firebird 的主要失败。

一旦检测到"deadlock",除非断开连接组件

,否则不会清除该条件
while rowsupdated = 0 and counter < 5 do
begin
  try
    rowsupdated := SQLQuery.Execute;
  except
    SQLConnection.Connected := False;
    SQLConnection.Connected := True;
  end;
  Inc(Counter)
end;

当您在 Delphi 中使用 DBX,在 Firebird 中没有任何实质性的锁容限时,您如何制作健壮的多用户 table-更新客户端?

客户端可以指定事务是否应该等待死锁解决。如果在您的情况下死锁立即发生,可能是因为您的配置(在客户端上使用 nowait 事务参数)。不使用 nowait 将导致服务器端检测到死锁并(在可配置的超时后)在客户端引发异常。

因为 Firebird 2.0 您还可以在客户端的事务上指定锁定超时,覆盖服务器配置的超时值。

Firebird 事务可以配置为 nowait 或 wait(有或没有特定超时)。如何配置取决于驱动程序,由于我不熟悉 Delphi 我无法对此发表评论。不等待通常是默认设置,因为在大多数情况下等待只会延迟不可避免的事情。

"deadlock" 错误有点用词不当,因为它不是普通并发术语中的死锁。错误的第二部分通常更具描述性,例如 Update conflicts with concurrent update.,它是属于 "deadlock" 下的历史产物(尽管在大多数情况下这错误意味着您需要重新启动您的交易,所以从这个意义上说它是 "dead")。

我正在使用 "answer your own question" 按钮。我找到了解决办法。

  1. 为 Firebird/Interbase
  2. 安装 IBPhoenix 开源 ODBC 驱动程序
  3. 配置 ODBC DSN 以连接到 Firebird.fdb,并选中 nowait,并根据需要设置 LockTimeout(以秒为单位)。我选择了15秒。
  4. 使用 Delphi 7 ADO (dbGo) TADOConnection 配置为使用 Microsoft OLE DB Provider for ODBC 驱动程序。
  5. 这是重要的一点:将 ADOConnection.TransactionIsolation 设置为 ilReadUncommited 或 ilDirtyRead。

如果 TADOQuery.ExecSQL 发现记录已经更新,它实际上会 等待 (最多指定的 15 秒)尚未提交或回滚的事务!

这与 DBX 驱动程序不同,DBX 驱动程序在这种情况下会立即引发所谓的 "deadlock" 异常。 (正如我们上面讨论的)

因此,如果两个查询都这样做

Update MYTABLE set NUM = NUM + 1 where keyvalue = 99;

并且起始值(在任何更新之前)为 0,如预期的那样,两个事务都提交后 NUM 的值为 2。

从 NUM = 0 重新开始。如果第一个事务回滚,第二个事务可以提交(或回滚)。第二次更新提交后的值仅为 1。

我不知道它是如何或为什么如此有效,特别是因为 Firebird 不应该支持 ReadUnComitted 或 DirtyRead,但我很高兴它按照我想要的方式工作。