繁体   English   中英

记录锁定和并发问题

[英]Record locking and concurrency issues

我的逻辑架构如下: 在此输入图像描述
标头记录可以有多个子记录。

多台PC可以通过接受子记录详细信息的存储过程和值插入子记录。

  • 插入子记录时,如果不存在具有指定值的标头记录,则可能需要插入标头记录。
  • 您只需要为任何给定的“值”插入一个标题记录。 因此,如果插入两个子记录并提供相同的“值”,则只应创建一次标题。 这需要在插入期间进行并发管理。

多台PC可以通过存储过程查询未处理的头记录

  • 如果标头记录具有一组特定的子记录,并且标头记录未被处理,则需要查询标头记录。
    • 您只需要一台机器PC查询和处理每个标头记录。 永远不应该有一个实例,其中标题记录及其子项应由多台PC处理。 这需要在选择期间进行并发管理。

所以基本上我的标题查询看起来像这样:

BEGIN TRANSACTION;
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
SELECT TOP 1
    *
INTO
    #unprocessed
FROM
    Header h WITH (READPAST, UPDLOCK)
JOIN
    Child part1 ON part1.HeaderID = h.HeaderID AND part1.Name = 'XYZ'
JOIN
    Child part2 ON part1.HeaderID = part2.HeaderID AND 
WHERE
    h.Processed = 0x0;

UPDATE
    Header
SET
    Processed = 0x1
WHERE
    HeaderID IN (SELECT [HeaderID] FROM #unprocessed);  

SELECT * FROM #unprocessed

COMMIT TRAN

因此,上述查询可确保并发查询永远不会返回相同的记录。

我认为我的问题在于插入查询。 这就是我所拥有的:

DECLARE @HeaderID INT

BEGIN TRAN

--Create header record if it doesn't exist, otherwise get it's HeaderID
MERGE INTO
    Header WITH (HOLDLOCK) as target
USING
(
    SELECT 
        [Value] = @Value,  --stored procedure parameter
        [HeaderID]

) as source ([Value], [HeaderID]) ON target.[Value] = source.[Value] AND
                                     target.[Processed] = 0    
WHEN MATCHED THEN 
    UPDATE SET
        --Get the ID of the existing header
        @HeaderID = target.[HeaderID],
        [LastInsert] = sysdatetimeoffset() 
WHEN NOT MATCHED THEN
    INSERT
    (
        [Value]
    )
    VALUES
    (
        source.[Value]
    )


--Get new or existing ID
SELECT @HeaderID = COALESCE(@HeaderID , SCOPE_IDENTITY());

--Insert child with the new or existing HeaderID
INSERT INTO 
    [Correlation].[CorrelationSetPart]
    (
        [HeaderID],
        [Name]  
    )
VALUES
(
    @HeaderID,
    @Name --stored procedure parameter
);

我的问题是插入查询经常被上面的选择查询阻止,我收到超时。 选择查询由代理调用,因此可以相当快地调用它。 有一个更好的方法吗? 注意,我可以控制数据库模式。

回答问题的第二部分

您只需要一台机器PC查询和处理每个标头记录。 永远不应该有一个实例,其中标题记录及其子项应由多台PC处理

看一下sp_getapplock

我在类似的场景中使用app锁。 我有一个必须处理的对象表,类似于您的标题表。 客户端应用程序同时运行多个线程。 每个线程执行一个存储过程,该过程从对象表返回下一个要处理的对象。 因此,存储过程的主要任务不是自己进行处理,而是返回队列中需要处理的第一个对象。 代码可能如下所示:

CREATE PROCEDURE [dbo].[GetNextHeaderToProcess]
AS
BEGIN
    -- SET NOCOUNT ON added to prevent extra result sets from
    -- interfering with SELECT statements.
    SET NOCOUNT ON;

    BEGIN TRANSACTION;
    BEGIN TRY

        DECLARE @VarHeaderID int = NULL;

        DECLARE @VarLockResult int;
        EXEC @VarLockResult = sp_getapplock
            @Resource = 'GetNextHeaderToProcess_app_lock',
            @LockMode = 'Exclusive',
            @LockOwner = 'Transaction',
            @LockTimeout = 60000,
            @DbPrincipal = 'public';

        IF @VarLockResult >= 0
        BEGIN
            -- Acquired the lock
            -- Find the most suitable header for processing
            SELECT TOP 1
                @VarHeaderID = h.HeaderID
            FROM
                Header h
                JOIN Child part1 ON part1.HeaderID = h.HeaderID AND part1.Name = 'XYZ'
                JOIN Child part2 ON part1.HeaderID = part2.HeaderID
            WHERE
                h.Processed = 0x0
            ORDER BY ....;
            -- sorting is optional, but often useful
            -- for example, order by some timestamp to process oldest/newest headers first

            -- Mark the found Header to prevent multiple processing.
            UPDATE Header
            SET Processed = 2 -- in progress. Another procedure that performs the actual processing should set it to 1 when processing is complete.
            WHERE HeaderID = @VarHeaderID;
            -- There is no need to explicitly verify if we found anything. 
            -- If @VarHeaderID is null, no rows will be updated
        END;

        -- Return found Header, or no rows if nothing was found, or failed to acquire the lock
        SELECT
            @VarHeaderID AS HeaderID
        WHERE
            @VarHeaderID IS NOT NULL
        ;

        COMMIT TRANSACTION;
    END TRY
    BEGIN CATCH
        ROLLBACK TRANSACTION;
    END CATCH;

END

应该从执行实际处理的过程调用此过程。 在我的情况下,客户端应用程序执行实际处理,在您的情况下,它可能是另一个存储过程。 我们的想法是,我们在短时间内获得了应用程序锁定。 当然,如果实际处理速度很快,您可以将其放入锁中,因此一次只能处理一个标题。

获取锁后,我们会查找最合适的标头进行处理,然后设置其Processed标志。 根据处理的性质,您可以立即将标志设置为1(已处理),或将其设置为某个中间值,如2(进行中),然后将其设置为1(已处理)。 在任何情况下,一旦标志不为零,将不再选择标题进行处理。

这些应用程序锁与DB在读取和更新行时放置的正常锁分开,它们不应干扰插入。 在任何情况下,它都应该像WITH (UPDLOCK)一样锁定整个表。

回到问题的第一部分

您只需要为任何给定的“值”插入一个标题记录。 因此,如果插入两个子记录并提供相同的“值”,则只应创建一次标题。

您可以使用相同的方法:在插入过程开始时获取应用程序锁定(使用与查询过程中使用的应用程序锁定不同的名称)。 因此,您可以保证插入顺序发生,而不是同时发生。 顺便说一下,实际上最有可能的插入无论如何也不会同时发生。 DB将在内部顺序执行它们。 它们将彼此等待,因为每个插入都会锁定一个表以进行更新。 此外,每个插入都写入事务日志,所有对事务日志的写入也是顺序的。 因此,只需将sp_getapplock添加到插入过程的开头,并在MERGE中删除WITH (HOLDLOCK)提示。

当过程没有返回任何行时,GetNextHeaderToProcess过程的调用者应该正确处理这种情况。 如果锁定获取超时,或者根本没有更多标头要处理,则会发生这种情况。 通常,处理部分会在一段时间后重试。

插入过程应检查锁定获取是否失败,并重试插入或以某种方式向调用者报告问题。 我通常会将生成的插入行的身份ID(在您的情况下为ChildID)返回给调用者。 如果procedure返回0,则表示插入失败。 呼叫者可以决定做什么。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM