Eu não sou um grande fã da mesa extra "lock" ou da idéia de bloquear a mesa inteira para pegar o próximo disco. Entendo por que isso está sendo feito, mas isso também prejudica a simultaneidade das operações que estão sendo atualizadas para liberar um registro bloqueado (certamente dois processos não podem estar lutando contra isso quando não é possível que dois processos tenham bloqueado o mesmo registro no mesmo tempo).
Minha preferência seria adicionar uma coluna ProcessStatusID (normalmente TINYINT) à tabela com os dados sendo processados. E existe um campo para LastModifiedDate? Caso contrário, deve ser adicionado. Se sim, esses registros são atualizados fora desse processamento? Se os registros puderem ser atualizados fora desse processo específico, outro campo deverá ser adicionado para rastrear StatusModifiedDate (ou algo assim). No restante desta resposta, usarei apenas "StatusModifiedDate", pois é claro em seu significado (e, de fato, poderia ser usado como o nome do campo, mesmo que atualmente não exista campo "LastModifiedDate").
Os valores para ProcessStatusID (que devem ser colocados em uma nova tabela de pesquisa chamada "ProcessStatus" e Foreign Keyed para esta tabela) podem ser:
- Concluído (ou mesmo "Pendente", neste caso, pois ambos significam "pronto para ser processado")
- Em processo (ou "Processando")
- Erro (ou "WTF?")
Neste ponto, parece seguro supor que, a partir do aplicativo, ele só queira pegar o próximo registro para processar e não passará nada para ajudar a tomar essa decisão. Portanto, queremos pegar o registro mais antigo (pelo menos em termos de StatusModifiedDate) definido como "Concluído" / "Pendente". Algo ao longo das linhas de:
SELECT TOP 1 pt.RecordID
FROM ProcessTable pt
WHERE pt.StatusID = 1
ORDER BY pt.StatusModifiedDate ASC;
Também queremos atualizar esse registro para "Em processo" ao mesmo tempo para impedir que o outro processo o pegue. Poderíamos usar a OUTPUT
cláusula para nos permitir fazer o UPDATE e SELECT na mesma transação:
UPDATE TOP (1) pt
SET pt.StatusID = 2,
pt.StatusModifiedDate = GETDATE() -- or GETUTCDATE()
OUTPUT INSERTED.RecordID
FROM ProcessTable pt
WHERE pt.StatusID = 1;
O principal problema aqui é que, embora possamos fazer um TOP (1)
em uma UPDATE
operação, não há como fazê-lo ORDER BY
. Mas, podemos envolvê-lo em um CTE para combinar esses dois conceitos:
;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;
A questão óbvia é se dois processos que executam o SELECT ao mesmo tempo podem pegar o mesmo registro. Tenho certeza de que a cláusula UPDATE with OUTPUT, especialmente combinada com as dicas READPAST e UPDLOCK (veja abaixo para obter mais detalhes), ficará bem. No entanto, eu não testei esse cenário exato. Se, por algum motivo, a consulta acima não atender à condição de corrida, adicione o seguinte: bloqueios de aplicativo.
A consulta CTE acima pode ser agrupada em sp_getapplock e sp_releaseapplock para criar um " gatekeeper " para o processo. Ao fazer isso, apenas um processo de cada vez poderá entrar para executar a consulta acima. Os outros processos serão bloqueados até que o processo com o applock o libere. E como essa etapa do processo geral é apenas para capturar o RecordID, é bastante rápida e não bloqueará os outros processos por muito tempo. E, assim como com a consulta CTE, estamos não bloquear a tabela inteira, permitindo assim que outras atualizações para outras linhas (para definir seu status a qualquer "Concluído" ou "Erro"). Essencialmente:
BEGIN TRANSACTION;
EXEC sp_getapplock @Resource = 'GetNextRecordToProcess', @LockMode = 'Exclusive';
{CTE UPDATE query shown above}
EXEC sp_releaseapplock @Resource = 'GetNextRecordToProcess';
COMMIT TRANSACTION;
Os bloqueios de aplicativos são muito bons, mas devem ser usados com moderação.
Por fim, você só precisa de um procedimento armazenado para lidar com a configuração do status como "Concluído" ou "Erro". E isso pode ser simples:
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;
Dicas de tabela (encontradas em Dicas (Transact-SQL) - Tabela ):