3

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

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

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

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

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

选项 1 - 让数据库处理它

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

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

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

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

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

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

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

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

编辑:我见过的与这个问题最相似的是如何处理库存和并发,但它没有讨论选项 1(可能是因为这是一个糟糕的主意)

4

4 回答 4

3

我认为选项 2 通过一些调整会更好。

如果我必须处理这种情况,这就是我要做的

  1. 每当用户尝试为插槽预订车辆时,我都会输入(条目应包含由唯一汽车 ID + 插槽时间组成的唯一密钥,并且在此组合中不应允许重复条目)如果两个用户尝试同时为同一个插槽预订同一辆车,那么您的应用程序中会出现错误,这样您就可以通知其他用户面包车已经走了)在临时存放区(普通桌子可以,但如果您有更高的交易您想要研究一些缓存数据库解决方案。)
  2. 因此,在第二个用户尝试预订车辆之前,用户必须检查该车位的锁定情况。(或者您可以使用此数据显示该插槽的汽车不可用)。
于 2018-06-30T07:22:10.207 回答
0

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

于 2018-06-30T18:17:47.740 回答
0

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

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”有一个唯一的约束,这样如果任何插入都已为该时间段保留,则该插入将被拒绝。

于 2019-02-08T15:04:46.230 回答
0

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

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

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

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

下面是对此的 postgresql 查询:

SELECT *
FROM   vehicle
WHERE  id = ?
FOR UPDATE nowait
于 2019-02-08T17:39:57.203 回答