在并发更新期间选择查询会跳过记录

Boo*_*Boo 14 t-sql sql-server concurrency locking sql-server-2008

我有由N个线程并发处理的表.

CREATE TABLE [dbo].[Jobs]
(
    [Id]                    BIGINT          NOT NULL    CONSTRAINT [PK_Jobs] PRIMARY KEY IDENTITY,
    [Data]                  VARBINARY(MAX)  NOT NULL,
    [CreationTimestamp]     DATETIME2(7)    NOT NULL,
    [Type]                  INT             NOT NULL,
    [ModificationTimestamp] DATETIME2(7)    NOT NULL,
    [State]                 INT             NOT NULL,
    [RowVersion]            ROWVERSION      NOT NULL,
    [Activity]              INT                 NULL,
    [Parent_Id]             BIGINT              NULL
)
GO

CREATE NONCLUSTERED INDEX [IX_Jobs_Type_State_RowVersion] ON [dbo].[Jobs]([Type], [State], [RowVersion] ASC) WHERE ([State] <> 100)
GO

CREATE NONCLUSTERED INDEX [IX_Jobs_Parent_Id_State] ON [dbo].[Jobs]([Parent_Id], [State] ASC)
GO
Run Code Online (Sandbox Code Playgroud)

作业正在添加到表中State=0 (New)- 它可以被此状态下的任何工作者使用.当worker获取此队列项时,State更改为,50 (Processing)并且其他使用者无法使用该作业(工作程序[dbo].[Jobs_GetFirstByType]使用参数调用:) Type=any, @CurrentState=0, @NewState=50.

CREATE PROCEDURE [dbo].[Jobs_GetFirstByType]
    @Type           INT,
    @CurrentState   INT,
    @NewState       INT
AS
BEGIN

    SET TRANSACTION ISOLATION LEVEL READ COMMITTED;

    DECLARE @JobId BIGINT;

    BEGIN TRAN

        SELECT      TOP(1)
                    @JobId = Id
        FROM        [dbo].[Jobs] WITH (UPDLOCK, READPAST)
        WHERE       [Type] = @Type AND [State] = @CurrentState
        ORDER BY    [RowVersion];


        UPDATE  [dbo].[Jobs]

        SET     [State] = @NewState,
                [ModificationTimestamp] = SYSUTCDATETIME()

        OUTPUT  INSERTED.[Id]
                ,INSERTED.[RowVersion]
                ,INSERTED.[Data]
                ,INSERTED.[Type]
                ,INSERTED.[State]
                ,INSERTED.[Activity]

        WHERE   [Id] = @JobId;

    COMMIT TRAN

END
Run Code Online (Sandbox Code Playgroud)

处理完成后,State可以0 (New)再次将作业更改为或者可以将作业设置为100 (Completed).

CREATE PROCEDURE [dbo].[Jobs_UpdateStatus]
    @Id         BIGINT,
    @State      INT,
    @Activity   INT
AS
BEGIN

    UPDATE  j

    SET     j.[State] = @State,
            j.[Activity] = @Activity,
            j.[ModificationTimestamp] = SYSUTCDATETIME()

    OUTPUT  INSERTED.[Id], INSERTED.[RowVersion]

    FROM    [dbo].[Jobs] j

    WHERE   j.[Id] = @Id;

END
Run Code Online (Sandbox Code Playgroud)

乔布斯具有层次结构,State=100 (Completed)只有当所有子项都完成后才能获得父作业.一些工作者调用存储过程([dbo].[Jobs_GetCountWithExcludedState]with @ExcludedState=100)返回未完成的作业数,当它返回0时,父作业State可以设置为100 (Completed).

CREATE PROCEDURE [dbo].[Jobs_GetCountWithExcludedState]
    @ParentId       INT,
    @ExcludedState  INT
AS
BEGIN

    SET TRANSACTION ISOLATION LEVEL READ COMMITTED;

    SELECT  COUNT(1)

    FROM    [dbo].[Jobs]

    WHERE   [Parent_Id] = @ParentId
    AND     [State] <> @ExcludedState

END
Run Code Online (Sandbox Code Playgroud)

主要问题是此存储过程的奇怪行为.有时它为父作业返回0,但它确实有未完成的作业.我尝试打开更改数据跟踪和一些调试信息(包括分析) - State=100当SP返回0时,100%没有子作业.似乎SP跳过记录,不在100 (Completed)状态,但为什么会发生以及我们如何可以防止这个吗?

UPD:[dbo].[Jobs_GetCountWithExcludedState]当父作业有孩子时, 呼叫开始.当工作人员在没有存在的情况下开始检查子作业时,不存在任何情况,因为创建子项并设置为包含在事务中的父作业检查活动:

using (var ts = new TransactionScope())
{
    _jobManager.AddChilds(parentJob);

    parentJob.State = 0;
    parentJob.Activity = 30; // in this activity worker starts checking child jobs

    ts.Complete();
}
Run Code Online (Sandbox Code Playgroud)

Bri*_*ler 2

如果实际上您的程序Jobs_GetCountWithExcludedState返回 0 条记录,而实际上有符合您的条件的已提交记录,那么这将是非常令人不安的。这是一个非常简单的过程。所以有两种可能:

  • 由于 SQL Server 问题或数据损坏,查询失败。
  • 程序运行时实际上没有符合条件的已提交记录。

腐败是一个不太可能但可能的原因。您可以使用DBCC CHECKDB检查损坏情况。

最有可能的是,实际上没有已提交的作业记录,其值Parent_ID等于该@ParentId参数,并且在运行时不处于 100 的状态。

我强调承诺,因为这就是交易将看到的。

您在问题中从未真正解释过Parent_ID工作是如何设置的。我的第一个想法是,也许您正在检查未处理的子作业,但它没有找到,但另一个进程将其添加为Parent_ID另一个未完成的作业。这有可能吗?

我看到您添加了一个更新,以表明当您添加子作业记录时,父记录和子记录的更新都包含在事务中。这很好,但不是我问的问题。这是我正在考虑的一种可能性:

  • 为父级插入并提交作业记录。
  • Jobs_GetFirstByType抢了父母的工作。
  • 工作线程处理它并调用Jobs_UpdateStatus并将其状态更新为 100。
  • 有东西Jobs_GetCountWithExcludedState随作业调用并返回 0。
  • 创建子作业并将其附加到已完成的父作业记录中...这使得它现在再次不完整。

我并不是说这就是正在发生的事情......我只是问这是否可能以及您正在采取哪些措施来防止它?例如,在上面更新问题的代码中,您选择将ParentJob孩子附加到交易之外。是否可能是您正在选择一个父作业,然后在运行将子作业添加到父作业的事务之前它已完成?或者也许父作业的最后一个子作业完成,因此工作线程检查并将父作业标记为完成,但其他一些工作线程已经选择该作业作为新子作业的父作业?

有许多不同的情况可能会导致您所描述的症状。我相信问题出在您尚未与我们共享的一些代码中,特别是关于如何创建作业以及围绕调用的代码Jobs_GetCountWithExcludedState。如果您可以提供更多信息,我认为您将更有可能找到可用的答案,否则我们能做的最好的事情就是猜测我们看不到的代码中可能发生的所有事情。