处理事务数据库表中的竞争条件

Posted

技术标签:

【中文标题】处理事务数据库表中的竞争条件【英文标题】:Dealing with race condition in transactional database table 【发布时间】:2014-10-07 12:08:48 【问题描述】:

让我先介绍一下场景。假设您有一个用于业务应用程序的数据库,并且它跟踪的内容之一是库存。系统显示您有 5 个螺钉库存。假设您需要全部 5。系统为 -5 创建库存交易记录。在您提交该交易后,由于您知道您之前有 5 个并且您取出了 5 个,如果您总结该螺丝的所有库存交易记录,总数应该为 0。当两个人试图在同时。假设一个人想要 4,另一个想要 2。两个客户端应用程序都事先检查了数量,并且都被告知 5。同时,一个人为 -4 创建一个交易,另一个为 -2 创建一个交易。结果总库存数量为 -1,这是不可能的,因为系统不应允许负库存。

如果您没有服务器应用程序来帮助您,您将如何解决这个问题?我提到,因为协调库存交易的服务器是我解决它的方法,但现在我们的产品没有服务器应用程序。我们只有直接与 Firebird 数据库对话的客户端应用程序。我试图弄清楚如何仅使用客户端应用程序和数据库来做到这一点。可能有帮助的一件事是 Firebird 有一个叫做 Generator 的东西,它基本上是一个唯一的原子数字生成器,所以你可以保证,如果你要求 Firebird 增加生成器并给你下一个数字,它不会给任何其他人同一个号码。

我的想法是尝试使用生成器创建临时记录锁。我想我可以让他们都检查项目表上的“锁定”字段。如果它为空,则没有人拥有锁。如果它是非空的,它被锁定,所以你需要继续检查直到它没有被锁定。如果没有锁,您向生成器询问唯一编号并将其存储在您要锁定的项目的锁定字段中。您提交该事务,然后返回并检查是否确实是 Item 表的锁定字段包含您放在那里的数字。如果是,则您已成功锁定,如果没有,则表示有人同时锁定了它,而您输掉了比赛。完成后,您将锁清空,等待的客户端将看到空值,自己锁定并重复。

我相信这本身就有一个竞争条件。 Trxn1(事务 1)检查锁并找到空值。 Trxn2 检查锁并找到空值。 Trxn1 从生成器获取新的锁号。 Trxn2 从生成器获得新锁。 Trxn1 说如果锁仍然为空,则使用我的锁更新项目记录。 Trxn1 提交 trxn 然后启动一个新的 Trxn1 并证明锁包含他的锁 id 并且它确实知道它有权进行库存交易并开始这样做。在 Trxn1 检查它是否获得锁之后,如果锁为空,则 Trxn2 立即提交其存储其锁的更新语句。如果 Trxn2 在 Trxn1 提交锁之前执行了他的更新语句,那么 Trxn2 仍会将值视为 null 并且会发生更新。如果 Trxn2 的锁提交发生在 Trxn1 提交锁并且已经验证它之后,我们就有问题了。 Trxn1 正在更改项目事务表。 Trxn2 提交了他的锁,因为当它执行它并且提交时,它的事务世界中的锁是空的,当它提交时,Trxn2 的更新语句将覆盖 Trxn1 的锁,因为更新语句中的空检查发生在两者提交之前,而不是在提交时。所以现在双方都认为他们有锁,我们最终会得到负库存。

谁能想出一种方法来解决这个缺少具有某种排队系统 (FIFO) 的服务器应用程序的问题?我希望这一切都可以通过客户“与数据库交谈”来协调这一点,但从技术上讲这可能是不可能的。对不起,如果这有点罗嗦:D

解决方案编辑: jtahlborn 似乎有正确的想法。我不知何故没有意识到 Firebird 实际上具有行级锁定。简单的选择语句(无连接、分组依据等)可以在语句末尾附加“with lock”,并且语句返回的任何行都将被锁定,直到事务提交或回滚。没有其他人可以获得对该行的锁定,也无法对其进行更改。因为我不想在向 Item 事务表中插入行时锁定整个 ITEM 表,所以我将创建一个仅用于锁定的具有一列(ItemID 字段)的表。因为第二个事务在尝试自己锁定时会出错,所以我实际上从未修改锁定表本身的任何内容并不重要。未能获得锁给了我我需要的所有信息。我将在 ITEM 表的插入/删除上放置触发器,这样对于每个 Item 记录,这也是 ITEMLOCK 表中的一条记录。这是我要使用的过程。

    启动数据库事务 尝试使用您要更改的项目的 ItemID 获取 ITEMLOCK 行的锁定 如果您无法获得锁,请继续尝试直到记录解锁 一旦锁定,去证明该项目的手头数量足以覆盖你的东西 想要取出,因为他们可能有旧数据,这可能不是 案例,它将退出此处并向用户发送消息 如果数量充足,请将您的库存交易记录插入库存交易表中 提交事务,然后释放锁

注意:Matthieu M 提到了 FOR UPDATE 子句。它在文档中与 WITH LOCK 子句一起提到。据我了解,当您使用一条语句锁定多行时,您可以使用它。我不是百分百确定,但似乎使用 WITH LOCK 执行此操作会尝试全有或全无的方法,而 FOR UPDATE 将一次单独锁定每个。我不确定如果它锁定了您要求的前 100 条记录但在第 101 条记录上它无法获得锁定会发生什么。然后它会释放你得到的 100 个锁吗?我一次需要锁定多个项目,但我对 FOR UPDATE 感到不舒服,因为我觉得我并不真正理解其中的区别。我可能还想知道哪个项目已被锁定以用于用户消息传递(将设置一个超时,以便 trxns 不会永远等待锁定)所以我将使用 WITH LOCK 一次锁定一个。

注意 2:我想提醒任何在自己的代码中使用它的人要小心。在等待释放锁时,我将有一个非常简单的循环(它已经释放了吗?现在怎么样?现在?)。如果我有大量用户可能试图同时锁定同一行,则可能会出现死锁情况。假设您有一个缓慢的客户端。该客户端可能总是以短棒结束,因为每次释放锁时,其他客户端会比慢速客户端更快地抓住它。如果这种情况一遍又一遍地发生,这本质上将是一个僵局。如果我担心这一点,我需要一种方法来确定谁排在第一位。就我而言,数据库事务应该是短暂的,我们永远不会有超过 50 个用户(不是云系统),而且他们都不太可能同时使用系统的这一部分来尝试修改完全相同的商品的库存数量。

【问题讨论】:

添加一列(或多列),其伪造键指向上一个交易。然后向该列添加唯一约束。现在数据库将强制没有 2 个事务可以指向相同的先前事务,从而导致所有事务都被序列化。 你应该阅读select for update。 您无法使用 select for update 锁定不存在的行。 有趣的想法 JoG。我想我有一个更好的解决方案,但这无疑是解决问题的一种创造性方法,而且听起来也可以。 您甚至可以更进一步,在更改金额旁边的记录中保留总计。所以你有changeamount,total,previoustotal,然后你可以添加total = previoustotal + change的约束。通过这种方式,您可以保留所有交易的完整历史记录,并且只需通过查看最新交易来查找总数,避免对大量交易进行聚合查询。 【参考方案1】:

最简单的解决方案是锁定一些主行(如主“项目”)并将其用作您的分布式锁定机制。 (假设您的数据库支持行级锁,就像大多数现代数据库一样)。

【讨论】:

或者在 SQL 中:select ... from ... where ... FOR UPDATE.【参考方案2】:

我建议阅读 CAP 定理以及它如何解释您所描述的场景。编辑:详细阅读后,我的评论可能用途有限,因为您似乎已经知道这一点并试图在 Firebird 中解决问题。

【讨论】:

以上是关于处理事务数据库表中的竞争条件的主要内容,如果未能解决你的问题,请参考以下文章

NHibernate 事务和竞争条件

使用数据库事务防止竞争条件 (Laravel)

实体框架事务锁定

《mysql必知必会》读书笔记--触发器及管理事务处理

MySQL事务没有停止for循环的竞争条件

第四章 数据更新 4-3 事务