如何在库存分配方案中阻止这种竞争条件? [关闭]

Posted

技术标签:

【中文标题】如何在库存分配方案中阻止这种竞争条件? [关闭]【英文标题】:How do I block this race condition in an inventory allocation scenario? [closed] 【发布时间】:2018-11-30 13:01:49 【问题描述】:

我正在努力解决库存分配和并发的经典问题,我想知道是否有人可以指导我在这种情况下的最佳实践。

我的情况是,我们准备了一个带有多个“插槽”的订单,这些“插槽”将在流程的某个阶段由独特的库存物品填充,此时我想确保没有人将相同的独特单元分配给插槽在不同的顺序。例如,一个用户下周四想要一辆面包车,所以我预订了一个“面包车”插槽,但在稍后的时间点,我将一辆特定的车辆从院子里分配到这个插槽。我想确保两个不同的运营商下周四不能将同一辆货车分配给两个不同的客户。

我们已经有一个库存可用性检查流程,我们比较一个日期范围内两个表的汇总,将这两个表相加的结果(一个是进货,另一个是出货)告诉我我们是否有特定的我想在该日期分配给此插槽的项目,但我想防止其他用户在同一时间点将相同的项目分配给他们自己的插槽。

我已经在这个网站上进行了一些谷歌搜索和研究,看起来我需要一个“悲观锁定”解决方案,但我不确定如何有效地实施。

分配过程将从具有实体框架的 Web API(使用 .Net 的其余 api)调用,我考虑了以下两种解决方案:

选项 1 - 让数据库处理它

在分配点,我开始一个事务并在用于评估库存可用性的两个表上获得一个排他锁。

该过程确认库存可用性,将单元分配到插槽,然后释放锁。

我认为这将防止两个用户试图将相同的唯一单元分配给两个不同的订单的竞争条件,但我不喜欢为需要查询这些表的每个其他进程锁定两个表,直到分配过程完成我认为这可能会对尝试读取这些表的其他进程造成瓶颈。在这种情况下,我认为尝试执行重复分配的第二个进程应该排队,直到第一个进程释放锁,因为它无法查询可用性表,并且当它执行时它将无法通过可用性检查并报告缺货警告 - 如此有效地阻止第二个订单分配相同的库存。

在纸面上,这听起来可行,但我有两个担忧;第一个是它会影响性能,第二个是我忽略了一些东西。我也是第一次在这个项目中使用 Postgres(我通常是 SQL Server 人),但我认为 Postgres 仍然具有执行此操作的功能。

选项 2 - 使用某种手动锁定

我认为我的情况类似于票务网站在音乐会或电影院的销售过程中遇到的情况,我看到他们设置了计时器,说“您的票将在 5 分钟后过期”,但我不知道如何他们在后端实现了这种系统。他们是否会在分配过程开始之前创建一个“保留”库存表并在它们上设置某种到期时间,然后将尝试分配相同单位的其他用户“列入黑名单”,直到该计时器到期?

很抱歉,介绍很长,但我想完整地解释这个问题,因为我已经看到很多关于类似场景的问题,但没有什么能真正帮助我决定如何继续。

我的问题是这两个选项中的哪一个(如果有)是“正确的方法”?

编辑:我见过的与这个问题最相似的是How to deal with inventory and concurrency,但它没有讨论选项 1(可能是因为这是一个糟糕的主意)

【问题讨论】:

没有“正确的方法”。两者都有效。选项 1) 有规模限制,但更容易,你可以在这里优化很多东西,有整本关于锁定关系数据库的书。这取决于请求/客户端的数量等。如果你想扩展到天空,选项 2) 可能更好。 选项 1 似乎过于简化,您不会锁定整个表,您应该只锁定当前事务正在使用的记录。因此,如果我从库存中拉出一辆货车,则其他所有交易都应等到我完成后才能检查该特定货车的状态。这并不意味着我无法读取其他货车或车辆类型的状态。 让数据库处理吧!! ,无需在客户端设置任何类型的锁定,也无需使服务器过载以在多个客户端之间分配锁定。此外,在大多数情况下,您根本不需要锁定,我使用单个 INSERT SELECT 或 UPDATE SELECT 组合语句解决了类似的问题,从而消除了任何锁定需求。唯一的缺点是单元测试变得有点笨重。 EF 只适用于 UI 相关的东西、简单的 CRUD 和报告生成,但对于服务器上的计算绝对是个坏主意。 @AkashKava 您的解决方案不起作用,因为 INSERT SELECT 和 UPDATE SELECT 不是原子的。它们作为一个子查询执行,然后是一个 INSERT,在这两个 INSERT 之间可以(并且最终会)发生,从而破坏您的数据模型多次分配相同的资源。 【参考方案1】:

我认为选项 2 稍加调整会更好。

如果我必须处理这种情况,我会这样做

    每当用户尝试为车位预订车辆时,我都会创建一个条目(条目应包含由唯一汽车 ID + 车位时间组成的唯一键,并且此处不应允许重复条目用于该组合如果两个用户尝试同时为同一个插槽预订同一辆车,那么您将在您的应用程序中收到错误,这样您就可以通知其他用户面包车已经走了)在临时存放区(普通桌子可以,但如果你有更高您想研究一些缓存数据库解决方案的事务。) 因此,在第二个用户尝试预订车辆之前,用户必须检查该车位的锁定情况。 (或者您可以使用此数据显示该时段的汽车不可用)。

【讨论】:

这与我最终采用的路线非常相似,我构建了一个轻量级应用程序级缓存并保留了一个 guid 列表来唯一标识我正在使用的项目请求的生命周期,然后在我退出时从列表中删除这些项目。它在请求期间有效地维护列入黑名单的项目的生命周期,并且对这些相同项目的任何其他请求都被阻止,直到锁被释放(或超时)。【参考方案2】:

我不确定您的数据库是如何布局的,但是如果每个库存项目在数据库中都是它自己的记录,那么只需在表上添加一个 IsUsed 标志。当您去更新记录时,只需确保将 IsUsed = 0 作为 where 子句的一部分。如果总修改量返回为 0,那么您知道在您之前有其他东西更新了它。

【讨论】:

问题是在检查库存是否可用和认领之间可能发生的竞争条件。如果第二个进程在这两个点之间跳转,它将在第一个进程声明它之前看到可用库存,然后它也会尝试声明它。此时“IsUsed”尚未更新为 true,因此第二个进程仍会看到无人认领的项目。 啊,我明白了。您必须执行存储过程或事务,首先更新它,然后再选择它以确保在您之前没有其他任何东西抓住它...... 事务或存储过程本身并不能保证选择和随后的插入是原子执行的。【参考方案3】:

如果您有一个用于在数据库中存储车辆的表,那么您可以对车辆进行悲观无等待锁定,以便在用户选择的插槽中分配。

一旦获得该锁,该锁将由一个事务持有,直到它提交或回滚。如果尝试获取车辆上的锁,则所有其他事务将立即失败。因此无需在数据库中等待事务。 这将可扩展,因为 db 中的 txns 没有等待队列来获得要分配的车辆锁定。

对于失败的交易,您可以立即回滚它们并要求用户选择不同的车辆或插槽。

现在,如果您拥有多辆相同类型的车辆并且您有机会分配相同的车辆,我的意思是在同一个插槽中将相同的注册号分配给两个用户,这也适用。因为只有一笔交易会赢,而其他交易会失败。

以下是对此的 postgresql 查询:

SELECT *
FROM   vehicle
WHERE  id = ?
FOR UPDATE nowait

【讨论】:

【参考方案4】:

解决这个问题有不同的方法,我只是回答我的想法,并最终决定为客户解决这个问题。

1. 如果您的 INSERT 和 UPDATE 对这些资源的流量不高,您可以通过执行类似的操作(例如在存储过程中)来完全锁定表,但也可以这样做在简单的客户端代码中:

CREATE PROCEDURE ...
AS
BEGIN
  BEGIN TRANSACTION

  -- lock table "a" till end of transaction
  SELECT ...
  FROM a
  WITH (TABLOCK, HOLDLOCK)
  WHERE ...

  -- do some other stuff (including inserting/updating table "a")



  -- release lock
  COMMIT TRANSACTION
END

2. 通过让您的代码获得您自己创建的锁来使用悲观锁。放入要锁定的额外表 pr 资源类型,并在要锁定的资源的 Id 上设置唯一约束。然后,您通过尝试插入一行来获得锁,并通过删除它来释放锁。设置时间戳,以便您可以清理丢失的锁。该表可能如下所示:

Id         bigint
BookingId  bigint        -- the resource you want to lock on. Put a unique constrain here
Creation   datetime      -- you can use these 2 timestamps to decide when to automatically remove a lock
Updated    datetime
Username   nvarchar(100) -- maybe who obtained the lock?

通过这种方法,您可以轻松决定哪些代码需要获得锁,以及哪些代码可以容忍在没有锁的情况下读取您的资源和预留表。

3. 如果它是由开始和结束时间分配的资源,您可以将此时间跨度的粒度设置为例如 15 分钟。一天中每个 15 分钟的时间段将获得一个从 0 开始的数字。然后您可以在预订表旁边创建一个表,其中开始和结束时间戳现在由时间段的数字组成。选择一个合理的起始时间戳作为数字 0。然后,您将根据每个预留的需要插入尽可能多的具有不同时隙编号的行。你当然需要对“Timeslot”+“ResourceId”有一个唯一的约束,这样如果任何插入已经为那个时间段保留,那么它就会被拒绝。 更新此表可以很好地在带有保留的表上的触发器中完成,这样您仍然可以在保留表上拥有真正的时间戳,并且当执行插入或更新时,您可以更新时隙表,如果您违反它可能会引发错误唯一约束,从而回滚事务并防止两个表中的更改。

【讨论】:

以上是关于如何在库存分配方案中阻止这种竞争条件? [关闭]的主要内容,如果未能解决你的问题,请参考以下文章

如果可能有多个服务器(并且每个服务器都可以有多个线程),如何处理竞争条件

goroutine 竞争条件解决方案

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

如何阻止控制台在退出时关闭? [复制]

互斥与同步

Java MySQL 防止竞争条件