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 / 58042
Accepted
Ian Boyd
Ian Boyd
Asked: 2014-02-01 18:50:39 +0800 CST2014-02-01 18:50:39 +0800 CST 2014-02-01 18:50:39 +0800 CST

Como o SQL Server está retornando um valor novo e um valor antigo durante um UPDATE?

  • 772

Tivemos problemas, durante alta simultaneidade, de consultas retornando resultados sem sentido - resultados que violam a lógica das consultas sendo emitidas. Demorou um pouco para reproduzir o problema. Consegui reduzir o problema reproduzível a alguns punhados de T-SQL.

Observação : a parte do sistema ativo com o problema é composta por 5 tabelas, 4 gatilhos, 2 procedimentos armazenados e 2 exibições. Simplifiquei o sistema real em algo muito mais gerenciável para uma pergunta postada. Coisas foram reduzidas, colunas removidas, procedimentos armazenados em linha, exibições transformadas em expressões de tabela comuns, valores de colunas alterados. Isso tudo é um longo caminho para dizer que, embora o que se segue reproduza um erro, pode ser mais difícil de entender. Você terá que se abster de se perguntar por que algo está estruturado do jeito que está. Estou aqui tentando descobrir por que a condição de erro ocorre de forma reproduzível neste modelo de brinquedo.

/*
The idea in this system is that people are able to take days off. 
We create a table to hold these *"allocations"*, 
and declare sample data that only **1** production operator 
is allowed to take time off:
*/
IF OBJECT_ID('Allocations') IS NOT NULL DROP TABLE Allocations
CREATE TABLE [dbo].[Allocations](
    JobName varchar(50) PRIMARY KEY NOT NULL,
    Available int NOT NULL
)
--Sample allocation; there is 1 avaialable slot for this job
INSERT INTO Allocations(JobName, Available)
VALUES ('Production Operator', 1);

/*
Then we open up the system to the world, and everyone puts in for time. 
We store these requests for time off as *"transactions"*. 
Two production operators requested time off. 
We create sample data, and note that one of the users 
created their transaction first (by earlier CreatedDate):
*/
IF OBJECT_ID('Transactions') IS NOT NULL DROP TABLE Transactions;
CREATE TABLE [dbo].[Transactions](
    TransactionID int NOT NULL PRIMARY KEY CLUSTERED,
    JobName varchar(50) NOT NULL,
    ApprovalStatus varchar(50) NOT NULL,
    CreatedDate datetime NOT NULL
)
--Two sample transactions
INSERT INTO Transactions (TransactionID, JobName, ApprovalStatus, CreatedDate)
VALUES (52625, 'Production Operator', 'Booked', '20140125 12:00:40.820');
INSERT INTO Transactions (TransactionID, JobName, ApprovalStatus, CreatedDate)
VALUES (60981, 'Production Operator', 'WaitingList', '20150125 12:19:44.717');

/*
The allocation, and two sample transactions are now in the database:
*/
--Show the sample data
SELECT * FROM Allocations
SELECT * FROM Transactions

As transações são inseridas como arquivos WaitingList. Em seguida, temos uma tarefa periódica que é executada, procurando slots vazios e coloca qualquer pessoa na WaitingList em um status Booked.

Em uma janela SSMS separada, temos o procedimento armazenado recorrente simulado:

/*
    Simulate recurring task that looks for empty slots, 
    and bumps someone on the waiting list into that slot.
*/
SET NOCOUNT ON;

--Reset the faulty row so we can continue testing
UPDATE Transactions SET ApprovalStatus = 'WaitingList'
WHERE TransactionID = 60981

--DBCC TRACEON(3604,1200,3916,-1) WITH NO_INFOMSGS

DECLARE @attempts int
SET @attempts = 0;

WHILE (@attempts < 1000000)
BEGIN
    SET @attempts = @attempts+1;

    /*
        The concept is that if someone is already "Booked", then they occupy an available slot.
        We compare the configured amount of allocations (e.g. 1) to how many slots are used.
        If there are any slots leftover, then find the **earliest** created transaction that 
        is currently on the WaitingList, and set them to Booked.
    */

    PRINT '=== Looking for someone to bump ==='
    WITH AvailableAllocations AS (
        SELECT 
            a.JobName,
            a.Available AS Allocations, 
            ISNULL(Booked.BookedCount, 0) AS BookedCount, 
            a.Available-ISNULL(Booked.BookedCount, 0) AS Available
        FROM Allocations a
            FULL OUTER JOIN (
                SELECT t.JobName, COUNT(*) AS BookedCount
                FROM Transactions t
                WHERE t.ApprovalStatus IN ('Booked') 
                GROUP BY t.JobName
            ) Booked
            ON a.JobName = Booked.JobName
        WHERE a.Available > 0
    )
    UPDATE Transactions SET ApprovalStatus = 'Booked'
    WHERE TransactionID = (
        SELECT TOP 1 t.TransactionID
        FROM AvailableAllocations aa
            INNER JOIN Transactions t
            ON aa.JobName = t.JobName
            AND t.ApprovalStatus = 'WaitingList'
        WHERE aa.Available > 0
        ORDER BY t.CreatedDate 
    )


    IF EXISTS(SELECT * FROM Transactions WHERE TransactionID = 60981 AND ApprovalStatus = 'Booked')
    begin
        --DBCC TRACEOFF(3604,1200,3916,-1) WITH NO_INFOMSGS
        RAISERROR('The later tranasction, that should never be booked, managed to get booked!', 16, 1)
        BREAK;
    END
END

E, finalmente, execute isso em uma terceira janela de conexão do SSMS. Isso simula um problema de simultaneidade em que a transação anterior deixa de ocupar um slot e fica na lista de espera:

/*
    Toggle the earlier transaction back to "WaitingList".
    This means there are two possibilies:
       a) the transaction is "Booked", meaning no slots are available. 
          Therefore nobody should get bumped into "Booked"
       b) the transaction is "WaitingList", 
          meaning 1 slot is open and both tranasctions are "WaitingList"
          The earliest transaction should then get "Booked" into the slot.

    There is no time when there is an open slot where the 
    first transaction shouldn't be the one to get it - he got there first.
*/
SET NOCOUNT ON;

--Reset the faulty row so we can continue testing
UPDATE Transactions SET ApprovalStatus = 'WaitingList'
WHERE TransactionID = 60981

DECLARE @attempts int
SET @attempts = 0;

WHILE (@attempts < 100000)
BEGIN
    SET @attempts = @attempts+1

    /*Flip the earlier transaction from Booked back to WaitingList
        Because it's now on the waiting list -> there is a free slot.
        Because there is a free slot -> a transaction can be booked.
        Because this is the earlier transaction -> it should always be chosen to be booked
    */
    --DBCC TRACEON(3604,1200,3916,-1) WITH NO_INFOMSGS

    PRINT '=== Putting the earlier created transaction on the waiting list ==='

    UPDATE Transactions
    SET ApprovalStatus = 'WaitingList'
    WHERE TransactionID = 52625

    --DBCC TRACEOFF(3604,1200,3916,-1) WITH NO_INFOMSGS

    IF EXISTS(SELECT * FROM Transactions WHERE TransactionID = 60981 AND ApprovalStatus = 'Booked')
    begin
        RAISERROR('The later tranasction, that should never be booked, managed to get booked!', 16, 1)
        BREAK;
    END
END

Conceitualmente, o procedimento de colisão continua procurando por slots vazios. Se encontrar uma, pega a primeira transação que está no WaitingListe a marca como Booked.

Quando testado sem simultaneidade, a lógica funciona. Temos duas transações:

  • 12h00: Lista de Espera
  • 12h20: Lista de Espera

Há 1 alocação e 0 transações reservadas, então marcamos a transação anterior como reservada:

  • 12:00: Reservado
  • 12h20: Lista de Espera

Na próxima vez que a tarefa for executada, haverá 1 slot sendo ocupado - portanto, não há nada para atualizar.

Se atualizarmos a primeira transação e colocá-la no WaitingList:

UPDATE Transactions SET ApprovalStatus='WaitingList'
WHERE TransactionID = 60981

Então estamos de volta onde começamos:

  • 12h00: Lista de Espera
  • 12h20: Lista de Espera

Observação : você deve estar se perguntando por que estou colocando uma transação de volta na lista de espera. Isso é uma vítima do modelo de brinquedo simplificado. No sistema real, as transações podem ser PendingApproval, que também ocupam um slot. Uma transação PendingApproval é colocada na lista de espera quando é aprovada. Não importa. Não se preocupe com isso.

Mas quando eu introduzo a simultaneidade, por ter uma segunda janela constantemente colocando a primeira transação de volta na lista de espera após ser reservada, a transação posterior conseguiu obter a reserva:

  • 12h00: Lista de Espera
  • 12h20: Reservado

Os scripts de teste de brinquedo detectam isso e param de iterar:

Msg 50000, Level 16, State 1, Line 41
The later tranasction, that should never be booked, managed to get booked!

Por quê?

A questão é: por que nesse modelo de brinquedo essa condição de salvamento está sendo acionada?

Existem dois estados possíveis para o status de aprovação da primeira transação:

  • Reservado : caso em que o slot está ocupado e a transação posterior não pode tê-lo
  • WaitingList : nesse caso, há um slot vazio e duas transações que o desejam. Mas como sempre temos selecta transação mais antigaORDER BY CreatedDate (ou seja, ) a primeira transação deve obtê-la.

Eu pensei que talvez por causa de outros índices

Aprendi que após iniciar um UPDATE e alterar os dados , é possível ler os valores antigos. Nas condições iniciais:

  • Índice agrupado :Booked
  • Índice não agrupado :Booked

Em seguida, faço uma atualização e, embora o nó folha do índice clusterizado tenha sido modificado, todos os índices não clusterizados ainda contêm o valor original e ainda estão disponíveis para leitura:

  • Índice agrupado (Bloqueio exclusivo):Booked WaitingList
  • Índice não clusterizado : (desbloqueado)Booked

Mas isso não explica o problema observado. Sim, a transação não está mais reservada , o que significa que agora há um slot vazio. Mas essa mudança ainda não foi confirmada, ainda é realizada exclusivamente. Se o procedimento de colisão fosse executado, seria:

  • bloco: se a opção de banco de dados de isolamento de instantâneo estiver desativada
  • leia o valor antigo (por exemplo Booked): se o isolamento de instantâneo estiver ativado

De qualquer maneira, o trabalho de colisão não saberia que há um slot vazio.

Então eu não tenho ideia

Estamos lutando há dias para descobrir como esses resultados absurdos podem acontecer.

Você pode não entender o sistema original, mas há um conjunto de scripts reprodutíveis de brinquedo. Eles salvam quando o caso inválido é detectado. Por que está sendo detectado? Por que isso está acontecendo?

Pergunta bônus

Como a NASDAQ resolve isso? Como funciona o cavirtex? Como funciona o mtgox?

tl;dr

Há três blocos de script. Coloque-os em 3 guias separadas do SSMS e execute-os. O 2º e 3º scripts gerarão um erro. Ajude-me a descobrir por que o erro aparece.

sql-server-2008-r2 locking
  • 1 1 respostas
  • 5004 Views

1 respostas

  • Voted
  1. Best Answer
    Paul White
    2014-02-02T00:01:02+08:002014-02-02T00:01:02+08:00

    O nível de isolamento de transação padrão READ COMMITTEDgarante que sua transação não lerá dados não confirmados. Não garante que os dados que você leu permanecerão os mesmos se você os ler novamente (leituras repetíveis) ou que novos dados não aparecerão (fantasmas).

    Essas mesmas considerações se aplicam a vários acessos a dados na mesma instrução .

    Sua UPDATEinstrução produz um plano que acessa a Transactionstabela mais de uma vez, portanto, é suscetível a efeitos causados ​​por leituras não repetíveis e fantasmas.

    acesso múltiplo

    Existem várias maneiras de esse plano produzir resultados que você não espera sob READ COMMITTEDisolamento.

    Um exemplo

    O primeiro Transactionsacesso à tabela encontra linhas com status WaitingList. O segundo acesso conta o número de cadastros (para o mesmo trabalho) que possuem o status Booked. O primeiro acesso pode retornar apenas a transação posterior (a anterior está Bookedneste ponto). Quando ocorre o segundo acesso (de contagem), a transação anterior foi alterada para WaitingList. A linha posterior, portanto, se qualifica para a atualização de Bookedstatus.

    Soluções

    Existem várias maneiras de definir a semântica de isolamento para obter os resultados desejados, principalmente usando o nível de isolamento correto.

    Usar nãoREAD_COMMITTED_SNAPSHOT seria uma solução. Essa implementação de isolamento de confirmação de leitura fornece consistência de leitura em nível de instrução (em que leituras não repetíveis e fantasmas não são possíveis), mas o SQL Server usa bloqueios de atualização ao localizar linhas a serem atualizadas. Isso significa que o caminho de acesso usado para localizar as linhas a serem atualizadas sempre lê os dados confirmados mais atuais. Uma segunda leitura da mesma estrutura de dados na mesma instrução leria as linhas com versão.

    Para obter mais detalhes, consulte meu artigo Data Modifications under Read Committed Snapshot Isolation .

    O SNAPSHOTnível de isolamento forneceria um instantâneo consistente desde o início da transação para todas as leituras, mas você pode encontrar erros de conflito de gravação.

    Outras observações

    Devo dizer, porém, que não teria projetado o esquema ou a consulta dessa maneira. Há muito mais trabalho envolvido do que o necessário para atender aos requisitos de negócios declarados. Talvez isso seja em parte o resultado das simplificações da questão, em todo caso, essa é uma questão separada.

    O comportamento que você está vendo não representa um bug de nenhum tipo. Os scripts produzem resultados corretos de acordo com a semântica de isolamento solicitada. Efeitos de simultaneidade como esse também não se limitam a planos que acessam dados várias vezes.

    O nível de isolamento de confirmação de leitura fornece muito menos garantias do que normalmente se supõe. Por exemplo, pular linhas e/ou ler a mesma linha mais de uma vez é perfeitamente possível.

    • 12

relate perguntas

  • A instalação autônoma do cluster do SQL Server 2008 R2 falha com o erro - "Caracteres ilegais no caminho".

  • Migração de banco de dados grande

  • Justifique NÃO usando a dica (nolock) em todas as consultas

  • plano de manutenção executado pelo agente

  • Randomizando o conteúdo da tabela e armazenando-o de volta na tabela

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