我的表由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();
}
答案 0 :(得分:2)
如果事实上已提交的记录与您的条件匹配,那么实际上您的过程Jobs_GetCountWithExcludedState
返回了0条记录,这将是非常令人不安的。这是一个非常简单的程序。所以有两种可能性:
腐败是不太可能的,但可能的原因。您可以使用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;并查看是否可以解决您的问题。有关隔离级别的更多信息,文档非常有用:
答案 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还是稍后更新了?它们是批量插入的吗?是否有附加代码来做其他事情?
这也是我建议你去验证上述假设的地方,因为这个问题最有可能出现在程序中。
可能的重构使所有这些假设无效
让数据库告诉应用程序直接完成父项任务。
答案 3 :(得分:-1)
我有建议审查客户端以及如何处理每个线程的事务和连接生存期。因为所有命令都在客户端事务上运行。