Выбор запроса пропускает записи во время параллельных обновлений

У меня есть таблица, которая обрабатывается одновременно 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) - ее может потреблять любой рабочий в этом состоянии. Когда рабочий получает этот элемент очереди, 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 для родительского задания, но у него есть неполные задания. Я попытался включить отслеживание данных изменений и некоторую отладочную информацию (включая профилирование) - дочерние задания на 100% не имеют State=100, когда SP возвращается 0. Кажется, что 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();
}

Ответы

Ответ 1

Было бы очень неприятно, если бы на самом деле ваша процедура 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. Если вы можете дать больше информации, я думаю, что вы с большей вероятностью найдете полезный ответ, иначе лучшее, что мы можем сделать, - это угадать все, что может произойти в коде, который мы не можем видеть.

Ответ 2

Ваша проблема почти наверняка вызвана вашим выбором уровня изоляции "READ COMMITTED". Поведение этого зависит от вашего параметра конфигурации для READ_COMMITTED_SNAPSHOT, но в любом случае он позволяет другому потоку транзакций изменять записи, которые были бы видны вашим SELECT, между вашим SELECT и вашим UPDATE, поэтому у вас есть условие гонки.

Попробуйте еще раз с уровнем изоляции "SERIALIZABLE" и посмотрите, устраняет ли это вашу проблему. Для получения дополнительной информации о уровнях изоляции документация очень полезна:

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

Ответ 3

Ваш код sql выглядит отлично. Поэтому проблема заключается в том, как она используется.

Гипотеза № 0
Процедура "Jobs_GetCountWithExcludedState" вызывается с абсолютно неправильным идентификатором. Потому что да когда-то проблема на самом деле просто небольшая ошибка. Я сомневаюсь, что это ваш случай.


Гипотеза № 1
Код, проверяющий поле "Activity = 30", делает это в уровне изоляции "READ UNCOMMITED". Затем он вызывается "Jobs_GetCountWithExcludedState" с родительским идентификатором, который может быть не совсем готов для него, потому что транзакция ввода может еще не закончиться или была отменена.


Гипотеза № 2
Процедура "Jobs_GetCountWithExcludedState" вызывается с идентификатором, который больше не является дочерним. Могло быть много причин, почему это происходит.
Например,

  • Транзакция, по которой вставленное дочернее задание завершилось по какой-либо причине, но эта процедура была вызвана в любом случае.
  • Одно дочернее задание было удалено и должно было быть заменено.
  • и т.д.

Гипотеза № 3
Процедура "Jobs_GetCountWithExcludedState" вызывается до того, как childJob получит свой родительский указатель.


Заключение
Как вы можете видеть, нам нужна дополнительная информация о двух вещах:
 1. Как вызывается "Job_GetCountWithExcludedState".
 2. Как вставляются задания. Является ли parentId назначенным во время вставки или обновляется немного позже? Вставляются ли они в пакет? Есть ли приложенный код к нему, который делает другие вещи?

Это также я рекомендую вам взглянуть, чтобы проверить выше гипотезу, потому что проблема наиболее вероятна в программе.


Возможный рефакторинг для аннулирования всех этих гипотез
Попросите базу данных сообщить программе, какие задачи родителей выполняются напрямую.

  • Точно так же, как "Jobs_GetFirstByType", может быть Job_GetFirstParentJobToComplete ", который мог бы вернуть следующее незавершенное родительское задание с завершенными дочерними элементами, если оно есть. Это также может быть представление, которое возвращает все из них. В любом случае использование" Jobs_GetCountWithExcludedState "тогда больше не используются, тем самым исключая всю мою гипотезу. Новая процедура или представление должны быть ЧИТАЕМЫМИ КОМИТЕТОМ или выше.

Ответ 4

У меня есть рекомендация на стороне клиента и как вы обрабатываете транзакцию и срок службы соединения для каждого потока. Поскольку все команды выполняются на клиентской транзакции.