SQL Server 2014 并发输入问题

Joe*_*Joe 5 sql-server concurrency unique-constraint

在表中Orders,我存储了我们从所有商店收到的订单。由于一个订单可以有多个行,列中有OrderIDOrderLineID 在那里OrderID可以复制,但OrderLineID必须是一个顺序中是唯一的。

由于订单可以修改,存储过程首先检查收到的订单OrderLineID是否已经存在于表中,然后决定插入还是更新。为此,我们:

  • 从 XML 输入动态构建插入和更新语句
  • 插入客户表
  • 插入到 shippingAddresses 表中

然后是主表:

IF NOT EXISTS (Select 1 from Orders where OrderLineID=@OrderLineID ......)
INSERT INTO Orders () VALUES ()
ELSE UPDATE Orders SET ... WHERE OrderLineID=@OrderLineID
Run Code Online (Sandbox Code Playgroud)

或者该MERGE功能是否提供更好的性能/控制?

但问题如下:

由于线路问题/服务器繁忙等,Order消息(或修改)可能会被多次发送,我们不知道按哪个顺序发送。因此,为了避免Order修改后到达,从而覆盖修改,我们添加了一个时间列:

IF NOT EXISTS (Select 1 from Orders where OrderLineID=@OrderLineID)
INSERT INTO Orders () VALUES ()
ELSE UPDATE Orders SET ... WHERE OrderLineID=@OrderLineID AND LastModified<@CreatedTime
Run Code Online (Sandbox Code Playgroud)

这样,如果后一条消息比前一条消息旧,则对表没有影响。

但是,消息及其修改可能会在很短的时间内发送两次(或更多),以至于后一条消息在保存前一条消息之前到达。因此,对于存储过程的两次执行,它IF NOT EXISTS (Select 1 from Orders where OrderLineID=@OrderLineID)都是TRUE,并且两次它都会生成一个INSERT并且我们找到重复的行。

也许这可以通过简单地设置OrderLineID唯一键来避免?

我还阅读了有关使用的内容:

set transaction isolation level serializable
Run Code Online (Sandbox Code Playgroud)

但我不确定这是如何处理的。我不仅要避免重复行,还要确保后面的消息执行UPDATE查询而不是抛出唯一键冲突

小智 12

社区维基答案

MERGE有一些您应该注意的问题,请参阅Aaron Bertrand对 SQL Server 的 MERGE 语句使用小心

更具体地说,对于您目前的情况,MERGE除了自动包含事务之外,没有针对并发问题提供额外的保护。您还需要在语句中添加一个SERIALIZABLE提示(或其同义词,HOLDLOCKMERGE,请参阅Dan Guzman 的“UPSERT” Race Condition With MERGE。请注意,UPDLOCK提示不是必需的MERGE

有关问题和选项的更一般概述,请参阅 Michael J. Swart 的以下内容:

一个流行且健壮的解决方案是WITH (UPDLOCK, SERIALIZABLE)NOT EXISTS检查中添加表访问提示,并在整个UPSERT操作中包含一个事务。

使用您的示例:

BEGIN TRANSACTION;

IF NOT EXISTS
(
    SELECT * 
    FROM Orders WITH (UPDLOCK, SERIALIZABLE)
    WHERE OrderLineID=@OrderLineID
)
    INSERT INTO Orders () VALUES ()
ELSE
    UPDATE Orders 
    SET ... 
    WHERE OrderLineID=@OrderLineID 
    AND LastModified<@CreatedTime;

COMMIT TRANSACTION;
Run Code Online (Sandbox Code Playgroud)

或者同样:

BEGIN TRANSACTION;

INSERT INTO Orders ()
SELECT ...
WHERE NOT EXISTS
(
    SELECT * 
    FROM Orders WITH (UPDLOCK, SERIALIZABLE)
    WHERE OrderLineID=@OrderLineID
);

IF @@ROWCOUNT = 0
BEGIN
    UPDATE Orders 
    SET ... 
    WHERE OrderLineID=@OrderLineID 
    AND LastModified<@CreatedTime;
END;

COMMIT TRANSACTION;
Run Code Online (Sandbox Code Playgroud)

可以切换INSERTUPDATE操作的顺序,以便首先尝试您期望最常成功的操作。提示必须始终应用于第一个操作。

例如,如果预计更新数量最多:

BEGIN TRANSACTION;

UPDATE Orders WITH (UPDLOCK, SERIALIZABLE)
SET ... 
WHERE OrderLineID=@OrderLineID 
AND LastModified<@CreatedTime;

IF @@ROWCOUNT = 0
BEGIN
    INSERT INTO Orders ()
    SELECT ...
END;

COMMIT TRANSACTION;
Run Code Online (Sandbox Code Playgroud)

问题中提到的唯一性约束一般来说是保持一致性的好主意,但对于此处提到的方法并不是严格需要的。如果您要使用 Michael Swart 的Just Do It模式,则需要唯一性保证才能正确操作。

提醒一下,如果您MERGE出于任何原因喜欢,您必须SERIALIZABLE对目标表使用提示。

虽然通常应该尽可能避免锁定提示,但这是有效用例的一个示例。