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

时间:2015-10-23 06:45:52

标签: sql-server sql-server-2008 tsql concurrency locking

我的表由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

作业正在添加到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

处理完成后,作业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

作业具有层次结构,只有当所有子项都完成后,父作业才会获得State=100 (Completed)。 一些工作者调用存储过程([dbo].[Jobs_GetCountWithExcludedState]@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

主要问题是此存储过程的奇怪行为。有时它会为父作业返回0,但确实有未完成的作业。我尝试打开更改数据跟踪和一些调试信息(包括分析) - 当SP返回0时,子作业100%没有State=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();
}

4 个答案:

答案 0 :(得分: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的代码。如果您可以提供更多信息,我认为您更有可能找到可用的答案,否则我们能做的最好的事情就是猜测我们无法看到的代码中可能发生的所有事情。

答案 1 :(得分:0)

您的问题几乎肯定是由您选择&#34; READ COMMITTED&#34;隔离级别。这种行为取决于您对READ_COMMITTED_SNAPSHOT的配置设置,但无论哪种方式,它允许另一个事务线程修改您的SELECT,您的SELECT和UPDATE之间已经看到的记录 - 因此您有竞争条件。

再次尝试使用隔离级别&#34; SERIALIZABLE&#34;并查看是否可以解决您的问题。有关隔离级别的更多信息,文档非常有用:

https://msdn.microsoft.com/en-AU/library/ms173763.aspx

答案 2 :(得分:0)

你的sql代码看起来很好。因此,问题在于如何使用它。

假设#0
程序&#34; Jobs_GetCountWithExcludedState&#34;用完全错误的ID调用。因为有时问题确实只是一个小错误。我怀疑这是你的情况。

假设#1
检查字段的代码&#34;活动= 30&#34;是在&#34; READ UNCOMMITED&#34;隔离级别。然后它将调用&#34; Jobs_GetCountWithExcludedState&#34;使用可能尚未准备好的parentID,因为插入事务可能尚未结束或已经回滚。

假设#2
程序&#34; Jobs_GetCountWithExcludedState&#34;被调用的id不再是孩子。这可能有很多原因发生 例如,

  • 插入子作业的事务因任何原因失败,但无论如何都调用了此过程。
  • 删除了一个子作业,即将被替换。

假设#3
程序&#34; Jobs_GetCountWithExcludedState&#34;在childJob获得其parentId之前调用。

<强>结论
如您所见,我们需要有关两件事的更多信息:
 1.如何&#34; Jobs_GetCountWithExcludedState&#34;被称为。
 2.如何插入作业。是在插入时分配了parentId还是稍后更新了?它们是批量插入的吗?是否有附加代码来做其他事情?

这也是我建议你去验证上述假设的地方,因为这个问题最有可能出现在程序中。

可能的重构使所有这些假设无效
让数据库告诉应用程序直接完成父项任务。

  • 就像&#34; Jobs_GetFirstByType&#34;,可能有Jobs_GetFirstParentJobToComplete&#34;这可能会返回下一个未完成的父作业(如果有的话)。它也可以是一个返回所有这些的视图。无论哪种方式,使用&#34; Jobs_GetCountWithExcludedState&#34;然后将不再使用,从而无视我的所有假设。新程序或视图应为READ COMMITTED或以上。

答案 3 :(得分:-1)

我有建议审查客户端以及如何处理每个线程的事务和连接生存期。因为所有命令都在客户端事务上运行。