Стратегии «проверки» записей для обработки

10

Я не уверен, есть ли названный шаблон для этого или нет, потому что это ужасная идея. Но мне нужен мой сервис для работы в активной / активной среде с балансировкой нагрузки. Это только сервер приложений. База данных будет находиться на отдельном сервере. У меня есть служба, которая должна будет проходить через процесс для каждой записи в таблице. Этот процесс может занять одну или две минуты и повторяться каждые n минут (настраивается, как правило, 15 минут).

Имея таблицу из 1000 записей, которая нуждается в этой обработке, и две службы, работающие с одним и тем же набором данных, я бы хотел, чтобы каждая служба «извлекала» запись для обработки. Мне нужно убедиться, что только одна служба / поток обрабатывает каждую запись одновременно.

У меня есть коллеги, которые использовали «таблицу блокировок» в прошлом. Где запись записывается в эту таблицу для логической блокировки записи в другой таблице (кстати, эта другая таблица довольно статична и с добавлением очень редкой новой записи), а затем удаляется, чтобы снять блокировку.

Мне интересно, не лучше ли для новой таблицы иметь столбец, который указывает, когда она была заблокирована, и что она в данный момент заблокирована, вместо того, чтобы вставлять удаление постоянно.

У кого-нибудь есть советы для такого рода вещей? Существует ли установленный шаблон для долгосрочной (ish) долгосрочной логической блокировки? Какие-нибудь советы, как гарантировать, что только одна служба захватывает блокировку одновременно? (Мой коллега использует TABLOCKX для блокировки всей таблицы.)

декан
источник

Ответы:

12

Я не большой поклонник дополнительной таблицы блокировки или идеи блокировки всей таблицы, чтобы получить следующую запись. Я понимаю, почему это делается, но это также вредит параллелизму для операций, которые обновляются для освобождения заблокированной записи (конечно, два процесса не могут бороться за это, когда два процесса не могут заблокировать одну и ту же запись в в то же время).

Я предпочел бы добавить столбец ProcessStatusID (обычно TINYINT) в таблицу с обрабатываемыми данными. И есть ли поле для LastModifiedDate? Если нет, то это следует добавить. Если да, то обновляются ли эти записи вне этой обработки? Если записи могут быть обновлены вне этого конкретного процесса, тогда нужно добавить другое поле для отслеживания StatusModifiedDate (или что-то в этом роде). В оставшейся части этого ответа я просто буду использовать «StatusModifiedDate», поскольку он понятен по своему значению (и фактически может использоваться в качестве имени поля, даже если в настоящее время нет поля «LastModifiedDate»).

Значения для ProcessStatusID (который должен быть помещен в новую справочную таблицу с именем «ProcessStatus» и внешним ключом этой таблицы) могут быть:

  1. Завершено (или даже «В ожидании» в этом случае, поскольку оба означают «готов к обработке»)
  2. В процессе (или «Обработка»)
  3. Ошибка (или "WTF?")

На данный момент кажется безопасным предположить, что из приложения оно просто хочет получить следующую запись для обработки и не будет передавать ничего, чтобы помочь принять это решение. Поэтому мы хотим получить самую старую (по крайней мере, с точки зрения StatusModifiedDate) запись, для которой установлено значение «Completed» / «Pending». Что-то вроде:

SELECT TOP 1 pt.RecordID
FROM   ProcessTable pt
WHERE  pt.StatusID = 1
ORDER BY pt.StatusModifiedDate ASC;

Мы также хотим обновить эту запись, чтобы "В процессе" в то же время, чтобы другой процесс не захватил ее. Мы могли бы использовать OUTPUTпредложение, чтобы позволить нам выполнить UPDATE и SELECT в одной транзакции:

UPDATE TOP (1) pt
SET    pt.StatusID = 2,
       pt.StatusModifiedDate = GETDATE() -- or GETUTCDATE()
OUTPUT INSERTED.RecordID
FROM   ProcessTable pt
WHERE  pt.StatusID = 1;

Основная проблема здесь заключается в том , что в то время как мы можем сделать TOP (1)в UPDATEэксплуатации, не существует способ сделать ORDER BY. Но мы можем обернуть его в CTE, чтобы объединить эти две концепции:

;WITH cte AS
(
   SELECT TOP 1 pt.RecordID
   FROM   ProcessTable pt (READPAST, ROWLOCK, UPDLOCK)
   WHERE  pt.StatusID = 1
   ORDER BY pt.StatusModifiedDate ASC;
)
UPDATE cte
SET    cte.StatusID = 2,
       cte.StatusModifiedDate = GETDATE() -- or GETUTCDATE()
OUTPUT INSERTED.RecordID;

Очевидный вопрос заключается в том, могут ли два процесса, выполняющие SELECT, одновременно захватить одну и ту же запись. Я уверен, что предложение UPDATE with OUTPUT, особенно в сочетании с подсказками READPAST и UPDLOCK (более подробно см. Ниже), будет в порядке. Однако я не проверял этот точный сценарий. Если по какой-либо причине вышеуказанный запрос не позаботится о состоянии гонки, то добавление следующего будет: блокировки приложения.

Вышеупомянутый запрос CTE может быть заключен в sp_getapplock и sp_releaseapplock, чтобы создать «привратник» для процесса. При этом только один процесс за раз сможет войти для выполнения запроса выше. Другие процессы будут заблокированы, пока процесс с блокировкой приложения не освободит его. А поскольку этот шаг всего процесса заключается в простом захвате RecordID, он довольно быстрый и не будет блокировать другие процессы очень долго. И, как и в случае запроса CTE, мы не блокируем всю таблицу, тем самым позволяя другим обновлениям других строк (устанавливать их статус либо «Завершено», либо «Ошибка»). По существу:

BEGIN TRANSACTION;
EXEC sp_getapplock @Resource = 'GetNextRecordToProcess', @LockMode = 'Exclusive';

   {CTE UPDATE query shown above}

EXEC sp_releaseapplock @Resource = 'GetNextRecordToProcess';
COMMIT TRANSACTION;

Блокировки приложений очень хороши, но их следует использовать с осторожностью.

Наконец, вам просто нужна хранимая процедура для обработки установки статуса «Завершено» или «Ошибка». И это может быть просто:

CREATE PROCEDURE ProcessTable_SetProcessStatusID
(
   @RecordID INT,
   @ProcessStatusID TINYINT
)
AS
SET NOCOUNT ON;

UPDATE pt
SET    pt.ProcessStatusID = @ProcessStatusID,
       pt.StatusModifiedDate = GETDATE() -- or GETUTCDATE()
FROM   ProcessTable pt
WHERE  pt.RecordID = @RecordID;

Подсказки к таблицам (см. Подсказки (Transact-SQL) - Таблица ):

  • READPAST (кажется, соответствует этому точному сценарию)

    Указывает, что компонент Database Engine не читает строки, заблокированные другими транзакциями. Когда указано READPAST, блокировки на уровне строк пропускаются. То есть компонент Database Engine пропускает строки, а не блокирует текущую транзакцию, пока не будут сняты блокировки ... READPAST в основном используется для уменьшения конкуренции за блокировку при реализации рабочей очереди, использующей таблицу SQL Server. Средство чтения очереди, использующее READPAST, пропускает прошлые записи очереди, заблокированные другими транзакциями, к следующей доступной записи очереди, не дожидаясь, пока другие транзакции снимают свои блокировки.

  • ROWLOCK (просто чтобы быть в безопасности)

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

  • UPDLOCK

    Указывает, что блокировки обновления следует снимать и удерживать до завершения транзакции. UPDLOCK принимает блокировки обновления для операций чтения только на уровне строк или страниц.

Соломон Руцкий
источник
1

Делал аналогичные вещи (без приложений, чисто внутри БД), используя очереди Service Broker. Легкий, полностью совместимый с ACID, масштабируется практически бесконечно. Прозрачная блокировка строк (или, скорее, «скрытие») встроена. Доступно с версии 2005 года и далее.

В вашем случае общая архитектура может выглядеть следующим образом: какой-то процесс отправляет сообщения в диалоговые окна Service Broker в соответствии с их расписаниями, а прослушиватели выбирают их из очереди на целевой стороне. Помимо создания отдельных типов сообщений, вы можете включать в текст сообщения практически все, что угодно - например, время ожидания и любые параметры, которые может иметь задача.

Не самая простая вещь для понимания, это точно, но как только вы ее получите, ее преимущества станут очевидными.

Роджер Вольф
источник