Como limite um procedimento armazenado SQL para ser executado por uma pessoa de cada vez?

12

Eu tenho um procedimento armazenado que basicamente seleciona valores de uma tabela e os insere em outra, uma espécie de arquivamento. Quero evitar que várias pessoas façam isso ao mesmo tempo.

Enquanto esse procedimento estiver em execução, não quero que mais ninguém possa iniciá-lo; no entanto, não quero a serialização, a outra pessoa executará o procedimento após o término.

O que eu quero é que a outra pessoa que esteja tentando iniciá-lo receba um erro enquanto estou executando o procedimento.

Eu tentei usar sp_getapplock, no entanto, não consigo parar completamente a pessoa de executar o procedimento.

Eu também tentei encontrar o procedimento com sys.dm_exec_requests e bloquear o procedimento, enquanto isso funciona, acho que não é o ideal, porque em alguns servidores não tenho permissões para executar o sys.dm_exec_sql_text (sql_handle).

Qual é a melhor maneira de fazer isso?

twoheadedmona
fonte
3
Você pode dar um passo atrás e fornecer mais algumas informações sobre o que o procedimento está fazendo e por que você deseja evitar que várias pessoas o executem ao mesmo tempo? Pode haver uma técnica de codificação que elimine esse requisito ou algum tipo de fila que você pode implementar para lidar com as coisas.
AMtwo

Respostas:

15

Para adicionar à resposta de @ Tibor-Karaszi, definir um tempo limite de bloqueio não produz um erro (enviei um PR contra os documentos). sp_getapplock apenas retorna -1, então você deve verificar o valor de retorno. Então assim:

create or alter procedure there_can_be_only_one 
as
begin
begin transaction

  declare @rv int
  exec @rv = sp_getapplock 'only_one','exclusive','Transaction',0
  if @rv < 0
   begin
      throw 50001, 'There is already an instance of this procedure running.', 10
   end

  --do stuff
  waitfor delay '00:00:20'


commit transaction
end
David Browne - Microsoft
fonte
8

Use sp_getapplock no início do processo e defina um tempo limite de bloqueio para um valor muito baixo. Dessa forma, você recebe um erro quando está bloqueado.

Tibor Karaszi
fonte
7

Outra opção é criar uma tabela para controlar o acesso ao procedimento. o exemplo abaixo mostra uma tabela possível e um procedimento que pode ser usado.

CREATE TABLE dbo.ProcedureLock
    (
    ProcedureLockID INT NOT NULL IDENTITY(1,1)
    , ProcedureName SYSNAME NOT NULL
    , IsLocked BIT NOT NULL CONSTRAINT DF_ProcedureLock_IsLocked DEFAULT (0)
    , UserSPID INT NULL
    , DateLockTaken DATETIME2(7) NULL
    , DateLockExpires DATETIME2(7) NULL
    , CONSTRAINT PK_ProcedureLock PRIMARY KEY CLUSTERED (ProcedureLockID)
    )

CREATE UNIQUE NONCLUSTERED INDEX IDXUQ_ProcedureLock_ProcedureName
    ON dbo.ProcedureLock (ProcedureName)

INSERT INTO dbo.ProcedureLock
    (ProcedureName, IsLocked)
VALUES ('dbo.DoSomeWork', 0)

GO

CREATE PROCEDURE dbo.DoSomeWork
AS
BEGIN

    /** Take Lock */
    UPDATE dbo.ProcedureLock
    SET IsLocked = 1
        , UserSPID = @@SPID
        , DateLockTaken = SYSDATETIME()
        , DateLockExpires = DATEADD(MINUTE, 10, SYSDATETIME())
    WHERE ProcedureName = 'dbo.DoSomeWork'
        AND (IsLocked = 0
            OR (IsLocked = 1 AND DateLockExpires < SYSDATETIME())
            )

    IF COALESCE(@@ROWCOUNT, 0) = 0
    BEGIN
        ;THROW 50000, 'This procedure can only be run one at a time, please wait', 1;
    END

    /** DO WHATEVER NEEDS TO BE DONE */

    /** Release the lock */
    UPDATE dbo.ProcedureLock
    SET IsLocked = 0
        , UserSPID = NULL
        , DateLockTaken = NULL
        , DateLockExpires = NULL
    WHERE ProcedureName = 'dbo.DoSomeWork'

END
Jonathan Fite
fonte
1
Isso é muito semelhante ao (ou talvez o mesmo que) o que pensei imediatamente depois de ler a pergunta, mas tive um problema com a ideia de que não tinha muita certeza de como abordar e não consigo vê-la na sua resposta. ou. Minha preocupação é: e se algo acontecer durante a parte "faça o que for necessário"? Como você redefiniria o IsLockedestado para 0 nesse caso? Também estou curioso sobre o seu uso COALESCEaqui. Pode @@ROWCOUNTser nulo após declarações como UPDATE? Por fim, apenas um pequeno detalhe, por que colocar um ponto-e-vírgula na frente da THROWdeclaração nesse caso específico?
187 Andriy M
A expiração do bloqueio é uma maneira de lidar com isso. Ele precisaria ser definido como um período de tempo razoável; defini-o para 10 minutos no meu exemplo. Você pode encapsular sua lógica de trabalho em um bloco try / catch e desbloquear o catch, se assim o desejar. Uso COALESCE por hábito, mas nenhum @@ ROWCOUNT não pode ser NULL. o ponto-e-vírgula principal vem do trabalho com projetos de banco de dados do Visual Studio, reclama se não estiver lá. nenhum dano de qualquer maneira.
Jonathan Fite
-1

Eu acho que você está tentando resolver o problema de maneira incorreta. O que você deseja é a máxima proteção da consistência do banco de dados. Se duas pessoas executam um procedimento armazenado ao mesmo tempo, a consistência do banco de dados pode ser violada.

Para proteger contra vários tipos de inconsistências do banco de dados, o padrão SQL possui quatro níveis de isolamento de transação:

  • LEIA NÃO COMPROMETIDO onde basicamente as transações perdem seu valor, outras transações veem dados sujos. Não use isso!
  • LEIA COMPROMETIDO onde as transações veem apenas dados confirmados, mas pode haver inconsistências nas quais duas transações podem passar por cima dos dedos uma da outra
  • LEITURA REPETIDA, onde é resolvido um tipo de inconsistência, leitura não repetível
  • SERIALIZABLE, que garante que exista alguma ordem virtual na qual a execução das transações levaria aos resultados que sua execução resultou em

No entanto, o padrão SQL possui uma abordagem baseada em bloqueio para essas inconsistências do banco de dados e, por razões de desempenho, muitos bancos de dados adotam uma abordagem baseada em isolamento de instantâneo que basicamente possui esses níveis:

  • LEIA COMPROMISSO, que é o mesmo em bancos de dados com bloqueio
  • SNAPSHOT ISOLATION em que o banco de dados vê uma captura instantânea de todos os dados e se tenta atualizar uma linha que foi atualizada por alguma outra transação, é cancelada, mas existem algumas anomalias conhecidas que podem ocorrer
  • SERIALIZABLE, que é o mesmo dos bancos de dados baseados em bloqueio, mas desta vez implementado de maneira diferente, não bloqueando, mas garantindo que não haja violações de serialização e, se essa violação for detectada, cancelando uma transação

Os cancelamentos de transação nesses bancos de dados baseados em isolamento de captura instantânea podem parecer preocupantes, mas, novamente, todos os bancos de dados cancelam uma transação devido a um conflito, portanto, qualquer aplicativo razoável precisa, de qualquer maneira, poder tentar novamente uma transação.

O que você deseja é o nível de isolamento SERIALIZABLE : garante que se as transações executadas independentemente uma após a outra resultem em um bom estado, qualquer execução paralela das transações também resultará em um bom estado. Felizmente, Michael Cahill, em sua tese de doutorado, descobriu como o nível de isolamento SERIALIZABLE pode ser suportado por bancos de dados isolados de captura instantânea com pouco esforço.

Se estiver usando um nível de isolamento SERIALIZABLE em um banco de dados isolado de captura instantânea, se duas pessoas tentarem executar o procedimento armazenado simultaneamente e pisarem nos dedos uma da outra, uma das transações será cancelada.

Agora, o SQL Server oferece suporte genuíno ao nível de isolamento SERIALIZABLE (em vez de mascarar o isolamento de instantâneo atrás da palavra-chave SERIALIZABLE )? Francamente, não sei: o único banco de dados que conheço que o suporta é o PostgreSQL.

Mesmo que eu não tenha dado conselhos específicos ao SQL Server, continuo postando essa resposta, pois usuários do PostgreSQL e usuários de outros bancos de dados que podem considerar a possibilidade de mudar para o PostgreSQL podem se beneficiar da minha resposta. Além disso, os usuários de bancos de dados não PostgreSQL que não podem mudar para o PostgreSQL podem pressionar seu fornecedor de banco de dados favorito para oferecer um nível de isolamento SERIALIZABLE genuíno .

juhist
fonte
Entendo que o voto negativo significa que alguém investigou se o SQL Server tem o nível de isolamento SERIALIZABLE e descobriu que não.
21718 juhist
-2

Sei que o problema "real" pode ser mais complexo.

Caso contrário, se você arquivar com gatilhos de inserção e / ou atualização, poderá evitar o problema que está tentando resolver.

Espero que ajude,
-Chris C.

J. Chris Compton
fonte
1
O que você quer dizer com "imediatamente"? Imediatamente depois do que? Após inserir? Assim que uma nova linha chega, ela é imediatamente enviada para arquivamento? Ou você quis dizer após a atualização? Então, qualquer alteração de dados aciona o arquivamento? Talvez você deva ser mais específico sobre o cenário que deseja sugerir.
187 Andriy M
O arquivamento pode ser muito caro e / ou muito raramente desejado em todas as inserções, especialmente se a tabela de origem for frequentemente inserida e / ou a segurança transacional entre ela e o arquivo exigiria bloqueios caros.
Underscore_d
@underscore_d Sim, pode ser muito caro ou nem sempre é necessário. É por isso que comecei minha resposta com a afirmação de que the 'real' problem may be more complex. Caso isso não aconteça, os gatilhos são uma boa solução. Além disso, provavelmente será mais fácil testar e manter, porque é um recurso do banco de dados, e não uma solução personalizada.
J. Chris Compton
@AndriyM Eu removi a palavra imediatamente, substituindo por uma ref para inserir / atualizar gatilhos. Desculpe pela confusão.
31418 James J. Compton
1
Reli a pergunta e acho que consigo ver a fonte da minha confusão. O que você está sugerindo aqui é mais parecido com auditoria do que arquivamento. Pelo que entendi, arquivar dados implica mover os dados (por exemplo, de uma tabela para outra). No entanto, embora o OP resumisse a função de seu procedimento como "uma espécie de arquivamento", eles nunca disseram que os dados seriam removidos da fonte, apenas que seriam selecionados e inseridos no destino. Então, suponho que você assumiu que o OP precisa copiar , em vez de mover , seus dados; nesse caso, usar gatilhos provavelmente faz sentido.
Andriy M