AskOverflow.Dev

AskOverflow.Dev Logo AskOverflow.Dev Logo

AskOverflow.Dev Navigation

  • Início
  • system&network
  • Ubuntu
  • Unix
  • DBA
  • Computer
  • Coding
  • LangChain

Mobile menu

Close
  • Início
  • system&network
    • Recentes
    • Highest score
    • tags
  • Ubuntu
    • Recentes
    • Highest score
    • tags
  • Unix
    • Recentes
    • tags
  • DBA
    • Recentes
    • tags
  • Computer
    • Recentes
    • tags
  • Coding
    • Recentes
    • tags
Início / dba / Perguntas / 112818
Accepted
Vladimir Baranov
Vladimir Baranov
Asked: 2015-09-02 03:45:24 +0800 CST2015-09-02 03:45:24 +0800 CST 2015-09-02 03:45:24 +0800 CST

Usando sp_getapplock para implementar uma fila. Está correto? Existe uma maneira melhor?

  • 772

Estive lendo uma série de posts de Paul White sobre SQL Server Isolation Levels e me deparei com uma frase :

Para enfatizar o ponto, as pseudo-restrições escritas em T-SQL devem ser executadas corretamente, independentemente das modificações simultâneas que possam estar ocorrendo. Um desenvolvedor de aplicativo pode proteger uma operação confidencial como essa com uma instrução de bloqueio. A coisa mais próxima que os programadores T-SQL têm dessa facilidade para procedimento armazenado em risco e código de gatilho é o sp_getapplockprocedimento armazenado do sistema comparativamente raramente usado. Isso não quer dizer que seja a única opção, ou mesmo preferida, apenas que existe e pode ser a escolha certa em algumas circunstâncias.

Estou usando sp_getapplocke isso me fez pensar se estou usando corretamente ou se existe uma maneira melhor de obter o efeito desejado.

Eu tenho um aplicativo C++ que processa os chamados "servidores de construção" em um loop 24 horas por dia, 7 dias por semana. Existe uma tabela com a lista desses Servidores do Edifício (cerca de 200 linhas). Novas linhas podem ser adicionadas a qualquer momento, mas isso não acontece com frequência. As linhas nunca são excluídas, mas podem ser marcadas como inativas. O processamento de um servidor pode levar de alguns segundos a dezenas de minutos, cada servidor é diferente, alguns são "pequenos", alguns são "grandes". Depois que um servidor é processado, o aplicativo precisa esperar pelo menos 20 minutos antes de processá-lo novamente (os servidores não devem ser pesquisados ​​com muita frequência). O aplicativo inicia 10 threads que executam o processamento em paralelo, mas devo garantir que dois threads não tentem processar o mesmo servidor ao mesmo tempo. Dois servidores diferentes podem e devem ser processados ​​simultaneamente, mas cada servidor pode ser processado não mais do que uma vez a cada 20 minutos.

Aqui está a definição de uma tabela:

CREATE TABLE [dbo].[PortalBuildingServers](
    [InternalIP] [varchar](64) NOT NULL,
    [LastCheckStarted] [datetime] NOT NULL,
    [LastCheckCompleted] [datetime] NOT NULL,
    [IsActiveAndNotDisabled] [bit] NOT NULL,
    [MaxBSMonitoringEventLogItemID] [bigint] NOT NULL,
CONSTRAINT [PK_PortalBuildingServers] PRIMARY KEY CLUSTERED 
(
    [InternalIP] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]

CREATE NONCLUSTERED INDEX [IX_LastCheckCompleted] ON [dbo].[PortalBuildingServers]
(
    [LastCheckCompleted] ASC
)
INCLUDE 
(
    [LastCheckStarted],
    [IsActiveAndNotDisabled],
    [MaxBSMonitoringEventLogItemID]
) WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, SORT_IN_TEMPDB = OFF, DROP_EXISTING = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]

O loop principal de um thread de trabalho em um aplicativo se parece com isso:

for(;;)
{
    // Choose building server for checking
    std::vector<SBuildingServer> vecBS = GetNextBSToCheck();
    if (vecBS.size() == 1)
    {
        // do the check and don't go to sleep afterwards
        SBuildingServer & bs = vecBS[0];
        DoCheck(bs);
        SetCheckComplete(bs);
    }
    else
    {
        // Sleep for a while
        ...
    }
}

Duas funções aqui GetNextBSToChecke SetCheckCompleteestão chamando procedimentos armazenados correspondentes.

GetNextBSToCheckretorna 0 ou 1 linha com detalhes do servidor que deve ser processado a seguir. É um servidor que não é processado há muito tempo. Se este servidor "mais antigo" tiver sido processado há menos de 20 minutos, nenhuma linha será retornada e o encadeamento aguardará um minuto.

SetCheckCompletedefine a hora em que o processamento foi concluído, tornando possível escolher este servidor para processamento novamente após 20 minutos.

Finalmente, o código de stored procedures:

GetNextToCheck:

CREATE PROCEDURE [dbo].[GetNextToCheck]
AS
BEGIN
    SET NOCOUNT ON;

    BEGIN TRANSACTION;
    BEGIN TRY
        DECLARE @VarInternalIP varchar(64) = NULL;
        DECLARE @VarMaxBSMonitoringEventLogItemID bigint = NULL;

        DECLARE @VarLockResult int;
        EXEC @VarLockResult = sp_getapplock
            @Resource = 'PortalBSChecking_app_lock',
            @LockMode = 'Exclusive',
            @LockOwner = 'Transaction',
            @LockTimeout = 60000,
            @DbPrincipal = 'public';

        IF @VarLockResult >= 0
        BEGIN
            -- Acquired the lock
            -- Find BS that wasn't checked for the longest period
            SELECT TOP 1
                @VarInternalIP = InternalIP
                ,@VarMaxBSMonitoringEventLogItemID = MaxBSMonitoringEventLogItemID
            FROM
                dbo.PortalBuildingServers
            WHERE
                LastCheckStarted <= LastCheckCompleted
                -- this BS is not being checked right now
                AND LastCheckCompleted < DATEADD(minute, -20, GETDATE())
                -- last check was done more than 20 minutes ago
                AND IsActiveAndNotDisabled = 1
            ORDER BY LastCheckCompleted
            ;

            -- Start checking the found BS
            UPDATE dbo.PortalBuildingServers
            SET LastCheckStarted = GETDATE()
            WHERE InternalIP = @VarInternalIP;
            -- There is no need to explicitly verify if we found anything.
            -- If @VarInternalIP is null, no rows will be updated
        END;

        -- Return found BS, 
        -- or no rows if nothing was found, or failed to acquire the lock
        SELECT
            @VarInternalIP AS InternalIP
            ,@VarMaxBSMonitoringEventLogItemID AS MaxBSMonitoringEventLogItemID
        WHERE
            @VarInternalIP IS NOT NULL
            AND @VarMaxBSMonitoringEventLogItemID IS NOT NULL
        ;

        COMMIT TRANSACTION;
    END TRY
    BEGIN CATCH
        ROLLBACK TRANSACTION;
    END CATCH;

END

SetCheckComplete:

CREATE PROCEDURE [dbo].[SetCheckComplete]
    @ParamInternalIP varchar(64)
AS
BEGIN
    SET NOCOUNT ON;

    BEGIN TRANSACTION;
    BEGIN TRY

        DECLARE @VarLockResult int;
        EXEC @VarLockResult = sp_getapplock
            @Resource = 'PortalBSChecking_app_lock',
            @LockMode = 'Exclusive',
            @LockOwner = 'Transaction',
            @LockTimeout = 60000,
            @DbPrincipal = 'public';

        IF @VarLockResult >= 0
        BEGIN
            -- Acquired the lock
            -- Completed checking the given BS
            UPDATE dbo.PortalBuildingServers
            SET LastCheckCompleted = GETDATE()
            WHERE InternalIP = @ParamInternalIP;
        END;

        COMMIT TRANSACTION;
    END TRY
    BEGIN CATCH
        ROLLBACK TRANSACTION;
    END CATCH;

END

Como você pode ver, eu uso sp_getapplockpara garantir que apenas uma instância de ambos os procedimentos armazenados esteja em execução a qualquer momento. Acho que preciso usar sp_getapplocknas duas procedures, pois a consulta que escolhe o servidor "mais antigo" usa o LastCheckCompletedhorário, que é atualizado por SetCheckComplete.

Acho que esse código garante que dois threads não tentem processar o mesmo servidor ao mesmo tempo, mas ficaria grato se você pudesse apontar algum problema com esse código e a abordagem geral. Então, a primeira pergunta: esta abordagem está correta?

Além disso, gostaria de saber se o mesmo efeito poderia ser alcançado sem usar sp_getapplock. A segunda pergunta: Existe uma maneira melhor?

sql-server sql-server-2008
  • 2 2 respostas
  • 7540 Views

2 respostas

  • Voted
  1. Best Answer
    Paul White
    2015-09-02T21:30:00+08:002015-09-02T21:30:00+08:00

    Esta abordagem está correta?

    Sim. Atende a todos os objetivos declarados na pergunta.

    Um comentário nos procedimentos para explicar a estratégia e observar o nome do procedimento relacionado pode ser útil para futuras manutenções por outros.

    Existe uma maneira melhor?

    Na minha opinião, não.

    Tirar um único bloqueio é uma operação extremamente rápida e resulta em uma lógica muito clara. Não está claro para mim que pegar o bloqueio no segundo procedimento seja redundante, mas mesmo que seja, o que você realmente ganha ao omiti-lo? A simplicidade e segurança de sua implementação me atraem.

    As alternativas são muito mais complexas e podem deixar você se perguntando se realmente cobriu todos os casos ou se pode haver uma alteração nos detalhes internos do mecanismo no futuro que quebraria suposições (talvez sutis e não declaradas).


    Se você precisar de uma implementação de enfileiramento mais tradicional, a seguinte referência é muito útil:

    Usando tabelas como filas por Remus Rusanu

    • 5
  2. Solomon Rutzky
    2015-09-02T18:08:14+08:002015-09-02T18:08:14+08:00

    Este cenário parece muito semelhante à seguinte pergunta:

    Estratégias para “check-out” de registros para processamento

    Na minha resposta, defendi um modelo semelhante ao que você tem aqui, mas com a noção de incluir sp_applockcomo um fail-safe apenas se o conceito inicial não fosse à prova de balas.

    A principal diferença no processo de "check-out" foi que combinei as consultas SELECTe UPDATEusando um CTE e a OUTPUTcláusula. Isso, com as dicas de consulta apropriadas (READPAST, ROWLOCK, UPDLOCK)no SELECT, permite atualizar o campo usado para determinar se uma linha é elegível para processamento e, ao mesmo tempo, retornar esse valor para que possa ser retornado ao processo de chamada. Com essas duas etapas combinadas, não há problema em fazer sem o bloqueio do aplicativo. E livrar-se do bloqueio do aplicativo deve, por sua vez, permitir maior rendimento, pois qualquer thread individual fazendo o processo de "check-out" no modelo atual (conforme postado na pergunta) faz com que os 9 threads restantes esperem, mesmo que eles possam estar agarrando o(s) próximo(s) na fila praticamente ao mesmo tempo.

    Sobre a seguinte declaração no final da pergunta:

    Eu uso sp_getapplockpara garantir que apenas uma instância de ambos os procedimentos armazenados esteja em execução a qualquer momento. Acho que preciso usar sp_getapplocknas duas procedures, pois a consulta que escolhe o servidor "mais antigo" usa o LastCheckCompletedhorário, que é atualizado por SetCheckComplete.

    Eu diria que se você mantiver a abordagem atual ou alternar para a abordagem "SELECT + UPDATE combinada via cláusula CTE + OUTPUT" (tm), o uso de sp_getapplockno SetCheckCompleteprocedimento armazenado é logicamente desnecessário. A razão pela qual não é necessário é:

    • apenas um thread pode ter um registro verificado a qualquer momento
    • usar o app lock in SetCheckCompleteimplica que o valor de LastCheckCompletedpode ter um efeito determinante no GetNextToCheck, ainda:
      • até que um registro seja verificado, o LastCheckStartedcampo será > the LastCheckCompleted, e esse estado faz com que o registro seja filtrado da consulta "GetNext" devido à LastCheckStarted <= LastCheckCompletedcondição
      • ao fazer o check-in, o LastCheckStarted <= LastCheckCompletednão filtrará mais o registro, mas a LastCheckCompleted < DATEADD(minute, -20, GETDATE())condição o filtrará, pois, por definição, foi concluído meros milissegundos antes da execução dessa consulta.

    Então, o SetCheckCompleteé realmente completamente independente do GetNextToCheckprocesso. É apenas o GetNextToCheckprocesso que precisa de qualquer quantidade de salvaguardas adicionadas a ele.

    A remoção do bloqueio do aplicativo SetCheckCompletenão deve ser apenas completamente segura, mas também aumentaria a taxa de transferência, pois haveria menos contenção nesse bloqueio arbitrário @Resource(novamente, mantendo ou não o modelo atual ou mudando para o que sugeri).


    ATUALIZAR

    Pergunta do comentário sobre esta resposta:

    A GetNextconsulta tem duas condições em WHERE. É possível que o servidor verifique primeiro uma condição: LastCheckCompleted < DATEADD(minute, -20, GETDATE())- esta condição é verdadeira para o item que está sendo retirado no momento. Em seguida, SetCheckCompletealtera o valor de LastCheckCompleted. Em seguida, o servidor verifica a segunda condição LastCheckStarted <= LastCheckCompletede também parece ser verdadeira. Resultado final: verificamos uma linha que acabamos de verificar sem esperar 20 minutos.

    Meu entendimento é que dentro de um único objeto (heap ou índice) isso não seria possível, mas entre vários objetos isso não é impossível . E olhando para o esquema, você tem um índice em LastCheckCompleted. Então duas coisas:

    1. Acho que esse cenário é altamente improvável, pois precisa LastCheckCompletedser atualizado após a verificação da primeira condição, mas acho que a tabela (Índice Clusterizado) seria atualizada primeiro, antes do Índice NonClustered, mas ainda para esse cenário por vir true teria que pegar o valor do LastCheckCompletedNonClustered Index, certo?
    2. De qualquer forma, uma solução fácil para impedir esse potencial (além de usar um nível de isolamento de transação de nível superior) seria criar um único campo de status. Para complicar as coisas, pelo menos um pouco, é que você precisa de uma informação extra (ou seja, a elegibilidade é composta por "não fez check-out" e "check-in há mais de 19 minutos"). Talvez você possa manter os dois campos de tempo atuais como informativos e adicionar um novo DATETIMEcampo que seja NULLpara "check-out" ou check-in. Em seguida, basta verificar new_field <= 20 minutos atrás e as NULLlinhas (ou seja, com check-out) não corresponderão de qualquer maneira (a menos que alguém seja bobo o suficiente para virar ANSI_NULLS OFF;-).
    • 1

relate perguntas

  • Quais são as principais causas de deadlocks e podem ser evitadas?

  • Quanto "Padding" coloco em meus índices?

  • Existe um processo do tipo "práticas recomendadas" para os desenvolvedores seguirem para alterações no banco de dados?

  • Como determinar se um Índice é necessário ou necessário

  • Downgrade do SQL Server 2008 para 2005

Sidebar

Stats

  • Perguntas 205573
  • respostas 270741
  • best respostas 135370
  • utilizador 68524
  • Highest score
  • respostas
  • Marko Smith

    conectar ao servidor PostgreSQL: FATAL: nenhuma entrada pg_hba.conf para o host

    • 12 respostas
  • Marko Smith

    Como fazer a saída do sqlplus aparecer em uma linha?

    • 3 respostas
  • Marko Smith

    Selecione qual tem data máxima ou data mais recente

    • 3 respostas
  • Marko Smith

    Como faço para listar todos os esquemas no PostgreSQL?

    • 4 respostas
  • Marko Smith

    Listar todas as colunas de uma tabela especificada

    • 5 respostas
  • Marko Smith

    Como usar o sqlplus para se conectar a um banco de dados Oracle localizado em outro host sem modificar meu próprio tnsnames.ora

    • 4 respostas
  • Marko Smith

    Como você mysqldump tabela (s) específica (s)?

    • 4 respostas
  • Marko Smith

    Listar os privilégios do banco de dados usando o psql

    • 10 respostas
  • Marko Smith

    Como inserir valores em uma tabela de uma consulta de seleção no PostgreSQL?

    • 4 respostas
  • Marko Smith

    Como faço para listar todos os bancos de dados e tabelas usando o psql?

    • 7 respostas
  • Martin Hope
    Jin conectar ao servidor PostgreSQL: FATAL: nenhuma entrada pg_hba.conf para o host 2014-12-02 02:54:58 +0800 CST
  • Martin Hope
    Stéphane Como faço para listar todos os esquemas no PostgreSQL? 2013-04-16 11:19:16 +0800 CST
  • Martin Hope
    Mike Walsh Por que o log de transações continua crescendo ou fica sem espaço? 2012-12-05 18:11:22 +0800 CST
  • Martin Hope
    Stephane Rolland Listar todas as colunas de uma tabela especificada 2012-08-14 04:44:44 +0800 CST
  • Martin Hope
    haxney O MySQL pode realizar consultas razoavelmente em bilhões de linhas? 2012-07-03 11:36:13 +0800 CST
  • Martin Hope
    qazwsx Como posso monitorar o andamento de uma importação de um arquivo .sql grande? 2012-05-03 08:54:41 +0800 CST
  • Martin Hope
    markdorison Como você mysqldump tabela (s) específica (s)? 2011-12-17 12:39:37 +0800 CST
  • Martin Hope
    Jonas Como posso cronometrar consultas SQL usando psql? 2011-06-04 02:22:54 +0800 CST
  • Martin Hope
    Jonas Como inserir valores em uma tabela de uma consulta de seleção no PostgreSQL? 2011-05-28 00:33:05 +0800 CST
  • Martin Hope
    Jonas Como faço para listar todos os bancos de dados e tabelas usando o psql? 2011-02-18 00:45:49 +0800 CST

Hot tag

sql-server mysql postgresql sql-server-2014 sql-server-2016 oracle sql-server-2008 database-design query-performance sql-server-2017

Explore

  • Início
  • Perguntas
    • Recentes
    • Highest score
  • tag
  • help

Footer

AskOverflow.Dev

About Us

  • About Us
  • Contact Us

Legal Stuff

  • Privacy Policy

Language

  • Pt
  • Server
  • Unix

© 2023 AskOverflow.DEV All Rights Reserve