Olá pessoas mais espertas que eu! Eu criei uma espécie de sistema de tabela de fila, mas parece simples demais para estar protegido contra condições de corrida. Estou faltando alguma coisa ou a seguinte condição de corrida é segura?
O Esquema
Eu tenho uma mesa, vamos chamá-la ProductQueue
:
CREATE TABLE dbo.ProductQueue
(
SerialId BIGINT PRIMARY KEY,
QueuedDateTime DATETIME NOT NULL -- Only using this for reference, no functionality is tied to it
);
Eu tenho um procedimento para adicionar à fila chamado AddToProductQueue
:
CREATE PROCEDURE dbo.AddToProductQueue (@SerialId BIGINT)
AS
BEGIN
INSERT INTO dbo.ProductQueue (SerialId, QueuedDateTime)
OUTPUT Inserted.SerialId
SELECT @SerialId, GETDATE();
END
Também tenho um procedimento para remover da fila chamado RemoveFromProductQueue
:
CREATE PROCEDURE dbo.RemoveFromProductQueue (@SerialId BIGINT)
AS
BEGIN
DELETE FROM dbo.ProductQueue
OUTPUT Deleted.SerialId
WHERE SerialId = @SerialId;
END
Observe SerialId
que é globalmente exclusivo para um Product
banco de dados/sistema de origem. Ou seja, duas instâncias de a Product
nunca podem ter o mesmo SerialId
. Essa é a extensão do lado do banco de dados.
O fluxo de trabalho
- Eu tenho um processo de inscrição que é executado de hora em hora.
- Esse processo obtém uma lista de variáveis
SerialIds
do sistema de origem. - Ele chama iterativamente o
AddToProductQueue
procedimento em cada umSerialId
de sua lista. - Se o procedimento tentar inserir um
SerialId
que já existe naProductQueue
tabela, ele gerará um erro de violação de chave primária e o processo do aplicativo capturará esse erro e o ignoraráSerialId
. - Caso contrário, o procedimento adiciona isso
SerialId
àProductQueue
tabela com êxito e o retorna ao processo de aplicativo. - O processo de inscrição então adiciona o que foi enfileirado com sucesso
SerialId
em uma lista separada. - Depois que o processo de aplicativo termina de iterar sua lista de todos os candidatos
SerialIds
a serem enfileirados, ele itera sua nova lista de enfileirados com sucessoSerialIds
e realiza trabalho externo neles, em um thread separado porSerialId
. (Este trabalho não está relacionado ao banco de dados.) - Finalmente, à medida que cada thread termina seu trabalho externo, a última etapa desse thread assíncrono é removê-lo
SerialId
daProductQueue
tabela chamando oRemoveFromProductQueue
procedimento. (Observe que um novo objeto de contexto de banco de dados é instanciado e uma nova conexão é criada para cada chamada assíncrona para esse procedimento, para que ele seja thread-safe no lado do aplicativo.)
Informações adicionais
- Não há índices na
ProductQueue
tabela e ela nunca terá mais de 1.000 linhas ao mesmo tempo. (Na verdade, na maioria das vezes terá literalmente apenas algumas linhas.) - O mesmo
SerialId
pode se tornar candidato novamente para ser adicionado novamente à tabela de filas em uma futura execução do processo de aplicação. - Não há proteções que impeçam a execução simultânea de uma segunda instância do processo de aplicativo, seja por acidente ou se a primeira instância demorou mais de 1 hora para ser executada, etc. (Esta é a parte simultânea com a qual estou mais preocupado.)
- O nível de isolamento da transação do banco de dados (e da conexão que está sendo feita) onde residem a tabela de filas e os procedimentos é o nível de isolamento padrão de
Read Committed
.
Problemas potenciais
- A instância em execução do processo do aplicativo trava de forma não tratada, deixando-
SerialIds
a presa na tabela de filas. Isso é aceitável para as necessidades do negócio e planejamos ter relatórios de exceção para nos ajudar a remediar manualmente esse caso. - O processo do aplicativo é executado várias vezes simultaneamente e captura algumas das mesmas
SerialIds
entre as instâncias em suas listas de origem iniciais. Ainda não consigo pensar em nenhuma ramificação negativa deste caso, uma vez que o procedimento de enfileiramento é atômico, e a lista real emSerialIds
que o processo de aplicação funcionará deve ser independente devido a esse procedimento de enfileiramento atômico. Não nos importamos qual instância do processo de aplicativo realmente processa cada umaSerialId
, desde que a mesmaSerialId
não seja processada simultaneamente por ambas as instâncias do processo.
A única coisa que você está pedindo ao mecanismo de banco de dados neste cenário é impor o
PRIMARY KEY
. Fá-lo-á em todas as condições e níveis de isolamento, claro.As possíveis condições de corrida são todas externas ao banco de dados, considerações que não são abordadas aqui.
Dito isso, a única maneira de pensar no banco de dados envolvido em uma condição de corrida é o processo de adição de IDs seriais candidatos a serem agrupados em uma transação de banco de dados , mas você não mencionou nada sobre isso.
Talvez seja possível que os processos usem uma transação de banco de dados de maneira não intencional, por exemplo, se você estiver usando um ORM que faz coisas úteis por mágica, sem ser solicitado explicitamente. Ou talvez você esteja usando transações implícitas.
Nesse cenário, a instância do aplicativo A começaria a adicionar seus (longa lista de) IDs seriais, todos em uma única transação de banco de dados. Enquanto isso, a instância do aplicativo B (com uma lista igualmente longa, incluindo pelo menos uma presente na lista de A) seria bloqueada na inserção de um ID serial sobreposto (porque a instância A ainda não confirmou sua transação).
Em uma sequência infeliz de eventos, a instância A terminaria o processamento assíncrono de um ID serial duplicado (no início de sua lista) antes que a instância B bloqueada pudesse executar sua inserção (no final de sua lista).
Nesse caso, A e B processariam com êxito o mesmo ID serial.
Isto parece improvável, dado o esboço do processo que você descreveu, mas não impossível.
Não sei por que o nome da sua tabela contém a palavra "fila", mas parece-me que o que você realmente deseja é um mecanismo de bloqueio.
Para responder diretamente à sua pergunta: se você não se importa com bloqueios obsoletos e desde que a
INSERT
transação, o processamento e aDELETE
transação estejam em uma relação causal (ou seja, aconteça nesta ordem após a etapa anterior relatar sucesso), eu não não vejo nenhum potencial para processamento duplo.Dito isso, uma maneira de resolver os dois problemas listados em "problemas potenciais" de maneira automatizada seria implementar a variante de instância única do algoritmo de bloqueio Redlock .
Cada instância tem seu próprio ID exclusivo (digamos, um guia ou algo que você gera toda vez que executa seu aplicativo de processamento)
Sempre que a instância captura um
SerialId
, ela tenta obter um bloqueio sobre ele:A saída da consulta, se houver, será o ID de série no qual você está bloqueado. Se não houver saída, você não tem o bloqueio.
Passe o timestamp atual na variável
@now
e o timestamp de cinco minutos no futuro na variável@expires
Execute um thread keep-alive que envie essa consulta a cada minuto ou com a freqüência necessária, estendendo o valor na variável
@expires
, digamos, cinco minutos depois de cada vez. Você pode alterar a consulta para estender bloqueios que ainda não foram processados em um único lote. Certifique-se de se comprometer imediatamente.Quando terminar o processamento, exclua o cadeado apenas se for seu :
Por aqui:
Claro, você pode simplesmente instalar uma instância real do Redis e usar uma implementação pronta do Redlock em seu aplicativo, que é abundante.
Achei o Service Broker completamente insatisfatório, impondo muita sobrecarga. As conversas exigem limpeza. Você está melhor com sua mesa.
Eu também recomendaria permitir bloqueios de linha no índice atrás de sua chave primária.
Não há nada de errado com o que você está fazendo, mas o SQL Server já possui um serviço integrado, Broker Services, que trata e gerencia filas de dados. Eu humildemente sugeriria que você investisse seu tempo nisso. Obrigado 😀