SQL Server 并发插入和删除

5 database-design sql-server concurrency ddl update

来自:SQL Server 2014 并发输入问题

后续问题:
我们如何在并行、多线程环境中删除和重新插入行,同时避免竞争条件、死锁等?我们仍然使用 ( UPDLOCK, SERIALIZABLE) 或其他锁吗?

我们有两个表:OrderOrderLineDetail。表Order是存储一般信息的父级;和OrderLineDetail是子表,因为订单有多个明细行项目。

订单可以更新,所以我们首先检查OrderId表中是否存在,并相应地插入或更新。

由于客户修改、线路问题、服务器繁忙等,订单文件可以多次发送。我们有一个时间Lastfiledatetime栏。如果传入的时间戳较旧,则对表没有影响。

对于OrderLineDetail表,有时我们的文件中没有自然键或代理键,因此我们删除所有子OrderLineDetail项,然后重新填充(这是一个较旧的遗留系统的 xml 文件)。

Create Table Orders 
(OrderId bigint primary key,  -- this is in xml files
 Lastfiledatetime datetime null
)

Create Table OrderLineDetail 
(OrderLineDetailid bigint primary key identity(1,1),  -- this is Not in the xml files
 OrderLineDescription varchar(50), 
 OrderLineQuantity int, 
 OrderId bigint foreign key references Orders(OrderId),
 Lastfiledatetime datetime
)
Run Code Online (Sandbox Code Playgroud)

我们在多线程、并行处理环境中工作,使用 SQL Server 2016 Read Committed Snapshot Isolation level。

要更新Order父级,我们这样做:

BEGIN TRANSACTION;

IF NOT EXISTS
(
    SELECT * 
    FROM Order WITH (UPDLOCK, SERIALIZABLE)
    WHERE Order ID=@OrderID
)
    INSERT INTO Order () VALUES ()
ELSE
    UPDATE Order
    SET ... 
    WHERE OrderID=@OrderID 
    AND LastFileDatetime<@CreatedTime;

COMMIT TRANSACTION;
Run Code Online (Sandbox Code Playgroud)

新问题

我们如何OrderLineDetail在并行、多线程环境中删除和重新插入子表,同时避免竞争条件、死锁等?我们仍然使用 ( UPDLOCK, SERIALIZABLE) 或其他锁吗?

BEGIN TRANSACTION;

IF EXISTS
(
    SELECT * 
    FROM OrderLineDetail WITH (UPDLOCK, SERIALIZABLE)
    WHERE Order ID=@OrderID
)
    DELETE OrderLineDetail
    WHERE OrderId=@OrderId
    AND LastFileDatetime<@CreatedTime;

    INSERT INTO OrderLineDetail () VALUES ()

COMMIT TRANSACTION;
Run Code Online (Sandbox Code Playgroud)

Pau*_*ite 9

UPDLOCK, SERIALIZABLE模式是为了避免由于竞争条件导致的错误结果(包括错误的键违规错误),当执行称为UPSERT- 更新现有行(如果存在)的特别常见操作时;否则插入新行。

...同时避免赛车状况、僵局等?

您似乎正在寻找一种神奇的提示组合,以允许数据库系统在没有冲突的情况下执行高度并发的操作。一般来说,没有这样的事情。完全避免死锁的唯一方法是始终以相同的顺序修改对象。

这在外键关系中可能很难甚至不可能实现。考虑到插入新对象时,您必须在子行之前添加父行。删除对象时,必须先删除子对象,然后再删除父对象。

这并不是说小心锁定仍然不会防止竞争条件,但它不能保证在所有情况下都避免死锁

就问题而言,是的,使用UPDLOCK, SERIALIZABLE提示可以防止UPSERT竞争条件,但,它不会防止死锁。它甚至可能有助于增加它们的频率。这很难提前评估,即使是经验丰富的 SQL Server 数据库设计人员也可能会出错。


对于涉及多个对象的更复杂的需求,一个常见的解决方案是在修改子行之前总是显式地独占锁定父行,即使自然顺序是首先处理子行。

例如,删除订单时:

DECLARE @OrderID bigint = 12345;

BEGIN TRANSACTION;

    -- EXTRA STEP
    -- Dummy update with XLOCK to exclusively lock the parent row
    UPDATE TOP (1) dbo.Orders WITH (XLOCK)
    SET Lastfiledatetime = NULL
    WHERE OrderId = @OrderID;

    -- Remove children
    DELETE dbo.OrderLineDetail
    WHERE OrderId = @OrderID;

    -- Remove parent
    DELETE dbo.Orders
    WHERE OrderId = @OrderID;

COMMIT TRANSACTION;
Run Code Online (Sandbox Code Playgroud)

如果所有修改都遵循相同的修改顺序,则独占锁定父行的额外步骤将有助于消除死锁:首先是父行,然后是子行。如果任何进程以相反的顺序访问对象,则可能会由于不兼容的锁而发生死锁。

请注意,实际修改父行中的某些内容可能很重要,因为 SQL Server在某些情况下可能不接受排他锁 ( XLOCK) ,如果它可以判断正在执行的操作不需要它。


大致相同想法的第二种实现是使用应用程序锁,如 Q & A在 SQL Server 中实现应用程序锁(分布式锁定模式)中所述(请参阅那里的文档链接)。

这在某些方面更容易,但同样,您必须确保修改受保护对象的所有代码使用相同的方案。一旦任何东西都可以访问底层对象而无需获取所需的应用程序锁,整个方案就会崩溃。

下面的示例显示了我们如何在处理订单元素之前对特定订单号进行排他应用程序锁定:

BEGIN TRANSACTION;

    -- The order number we want exclusive access to
    DECLARE @OrderID integer = 12345;

    -- Compute the locking resource string
    DECLARE @Resource nvarchar(255) = N'dbo.Order' + CONVERT(nvarchar(11), @OrderID);

    -- Return code from sys.sp_getapplock
    DECLARE @RC integer;

    -- Build dynamic SQL
    DECLARE @SQL nvarchar(max) =
        N'
        EXECUTE @RC = sys.sp_getapplock
            @Resource = ' + QUOTENAME(@Resource) + N',
            @LockMode = Exclusive, 
            @LockTimeout = -1;'

    -- Try to acquire the lock
    EXECUTE sys.sp_executesql
        @SQL,
        N'@RC integer OUTPUT',
        @RC = @RC OUTPUT;

    -- Do something with the return code value if necessary
    SELECT @RC;

    --- Sensitive operations go here

    -- Release the application lock early if you can
    -- using sys.sp_releaseapplock

ROLLBACK TRANSACTION;
Run Code Online (Sandbox Code Playgroud)

并发很难;对于那个很抱歉。