Я не большой поклонник дополнительной таблицы блокировки или идеи блокировки всей таблицы, чтобы получить следующую запись. Я понимаю, почему это делается, но это также вредит параллелизму для операций, которые обновляются для освобождения заблокированной записи (конечно, два процесса не могут бороться за это, когда два процесса не могут заблокировать одну и ту же запись в в то же время).
Я предпочел бы добавить столбец ProcessStatusID (обычно TINYINT) в таблицу с обрабатываемыми данными. И есть ли поле для LastModifiedDate? Если нет, то это следует добавить. Если да, то обновляются ли эти записи вне этой обработки? Если записи могут быть обновлены вне этого конкретного процесса, тогда нужно добавить другое поле для отслеживания StatusModifiedDate (или что-то в этом роде). В оставшейся части этого ответа я просто буду использовать «StatusModifiedDate», поскольку он понятен по своему значению (и фактически может использоваться в качестве имени поля, даже если в настоящее время нет поля «LastModifiedDate»).
Значения для ProcessStatusID (который должен быть помещен в новую справочную таблицу с именем «ProcessStatus» и внешним ключом этой таблицы) могут быть:
- Завершено (или даже «В ожидании» в этом случае, поскольку оба означают «готов к обработке»)
- В процессе (или «Обработка»)
- Ошибка (или "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) - Таблица ):