SQL 插入最大金额的争用条件

SQL Insert Race Conditions on Max Amount

提问人:mattsmith5 提问时间:11/7/2023 最后编辑:Dale Kmattsmith5 更新时间:11/9/2023 访问量:78

问:

我们正在创建一个拍卖竞价系统。我们只允许在 AuctionBid ID 表格中插入产品的金额高于上次出价的情况。

此查询适用于单个操作。

select MAX(Amount) from dbo.AuctionBid where ProductId = 9   // Amount: 150

但是,在对同一产品进行多个竞价的多线程并发环境中,可能会导致争用条件。其中一个插入读取 (150) 并即将插入例如 170,而另一个插入同时已经插入(例如 190)。

如何在SQL中解决这个问题?研究不同类型的锁。如果可能,尝试防止完全锁定表。

我们的桌子有

AuctionBidId 产品 Id
1 2 (沙发) 175
2 9 (电视) 100
3 9 (电视) 150

当前使用 Microsoft SQL Server 2019。(T-SQL)

sql-server 数据库 并发 azure-sql-database

评论

0赞 JimmyV 11/7/2023
您是否尝试过在桌子上放置插入/更新触发器?
0赞 mattsmith5 11/7/2023
我们公司现在正试图避免在数据库@JimmyV中触发
0赞 JimmyV 11/7/2023
然后,我会将所有更新汇集到一个队列中,并让单个作业处理队列并根据您的业务逻辑更新出价。但是,这并不能阻止另一个源直接操作该表。
1赞 digital.aaron 11/7/2023
保罗·怀特(Paul White)关于该主题的有趣信息如下: dba.stackexchange.com/a/187411/200032
0赞 siggemannen 11/7/2023
为什么不跟踪应用程序内存中的最高出价,然后按该值过滤掉新的出价,一直到数据库的解析听起来很慢

答:

0赞 Xedni 11/7/2023 #1

这样的事情能解决问题吗?将其放入您的插入过程中,如果提供的低于最高出价,则不会插入。您可以添加额外的处理来大惊小怪或记录一些东西,以防出价未被接受,但这里的关键点是,这会消除桌子上的锁,防止任何其他 SPID 尝试做同样的事情。@CurrentBid

insert into AuctionBid
(
    Bid
)
select
    @CurrentBId
where @CurrentBId > (select max(Bid) from AuctionBId)

-- if you need to handle subsequent logic differently if the bid insert failed, do something like this:
if @@rowcount = 0
begin
    raiserror('Oh noes! ur bid wasn''t accepted!', 0, 1) with nowait
end

我在全局临时表上运行了一个测试,首先插入了一行出价为 50(为表设定种子)。然后,我打开了一个新表,并以 9000 的出价插入到该表中,但保持交易未结状态。最后,我打开了第三个选项卡并尝试插入值 8999,它挂起等待我的其他事务完成。这似乎意味着在插入完成时,该表被锁定在插入物中。

在另一个事务仍处于打开状态时尝试插入的 SPID 正在尝试获取共享锁,并且在另一个事务提交之前无法插入。LCK_M_S

当另一个 SPID 具有锁定时,您甚至无法从表中选择;正如你所希望的那样。max(Bid)

评论

0赞 mattsmith5 11/7/2023
我认为它运行了一个完整的表锁,因为您这样做了“从 AuctionBId 中选择 max(Bid)”,可能需要在 id 上添加 Where 子句,并且结果可能会更改,您的数据库的隔离级别是多少?谢谢
0赞 mattsmith5 11/7/2023
另外,我敢打赌,如果你继续完成 9000 交易?会发生什么,8999 交易之后会通过吗?谢谢你的帮助
0赞 Xedni 11/7/2023
如果 9000 事务通过,则 8999 事务不会通过。这是有道理的,因为在另一个事务正在处理时,表被锁定以供读取。因此,在另一笔交易完成之前,它不会弄清楚是什么。max(bid)
0赞 mattsmith5 11/7/2023
啊,让我尝试一下 where 子句,看看它是如何再次锁定的
1赞 Ben Thul 11/7/2023 #2

考虑 ff 过程

create or alter procedure dbo.addBid (
   @ProductID int,
   @BidAmount int
)
as
begin
   declare @maxBid int;
   begin transaction
      set @maxBid = (
         select max(Amount)
         from dbo.AuctionBid with (holdlock, updlock)
         where ProductId = @ProductID
      );

     if (@bidAmount > @maxBid)
     begin
        insert into dbo.AuctionBid
           (ProductId, Amount)
        values
           (@ProductID, @BidAmount);
     end
     else
     begin
        print 'Specified bid amount is not larger than current bid and is thus rejected';
     end
   commit
end

您可能还想要其他一些错误处理,但这是要点。这里的想法是,您以这样一种方式获得当前最高出价,以防止并发进程同时这样做,并在与该选择相同的事务中执行插入。


我们在此处使用的查询提示包括:

HOLDLOCK(可序列化)- “在当前事务完成之前,其他事务无法插入键值属于当前事务中任何语句读取的键范围的新行。

UPLOCK - 在这种情况下,我们将强制使用独占锁,因为 SQL Server 只能接受一个 UPLOCK ,而不是共享的 READLOCK

1赞 Joe 11/7/2023 #3
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
BEGIN TRANSACTION;
-- Get the current max bid for the product
SELECT MAX(Amount) FROM dbo.AuctionBid WHERE ProductId = 9;
-- Your application checks if the new bid is higher and proceeds to insert
INSERT INTO dbo.AuctionBid (ProductId, Amount) VALUES (9, <new_bid>);
COMMIT TRANSACTION;