2015-10-23 24 views
14

Ho una tabella elaborata contemporaneamente da N thread.Seleziona query salta i record durante gli aggiornamenti simultanei

CREATE TABLE [dbo].[Jobs] 
(
    [Id]     BIGINT   NOT NULL CONSTRAINT [PK_Jobs] PRIMARY KEY IDENTITY, 
    [Data]     VARBINARY(MAX) NOT NULL, 
    [CreationTimestamp]  DATETIME2(7) NOT NULL, 
    [Type]     INT    NOT NULL, 
    [ModificationTimestamp] DATETIME2(7) NOT NULL, 
    [State]     INT    NOT NULL, 
    [RowVersion]   ROWVERSION  NOT NULL, 
    [Activity]    INT     NULL, 
    [Parent_Id]    BIGINT    NULL 
) 
GO 

CREATE NONCLUSTERED INDEX [IX_Jobs_Type_State_RowVersion] ON [dbo].[Jobs]([Type], [State], [RowVersion] ASC) WHERE ([State] <> 100) 
GO 

CREATE NONCLUSTERED INDEX [IX_Jobs_Parent_Id_State] ON [dbo].[Jobs]([Parent_Id], [State] ASC) 
GO 

lavoro sta aggiungendo a tavola con State=0 (New) - può essere consumato da tutti i lavoratori in questo stato. Quando il lavoratore ottiene questo articolo in coda, State è cambiato in 50 (Processing) e il lavoro non è più disponibile per altri utenti (gli operatori chiamano [dbo].[Jobs_GetFirstByType] con argomenti: Type=any, @CurrentState=0, @NewState=50).

CREATE PROCEDURE [dbo].[Jobs_GetFirstByType] 
    @Type   INT, 
    @CurrentState INT, 
    @NewState  INT 
AS 
BEGIN 

    SET TRANSACTION ISOLATION LEVEL READ COMMITTED; 

    DECLARE @JobId BIGINT; 

    BEGIN TRAN 

     SELECT  TOP(1) 
        @JobId = Id 
     FROM  [dbo].[Jobs] WITH (UPDLOCK, READPAST) 
     WHERE  [Type] = @Type AND [State] = @CurrentState 
     ORDER BY [RowVersion]; 


     UPDATE [dbo].[Jobs] 

     SET  [State] = @NewState, 
       [ModificationTimestamp] = SYSUTCDATETIME() 

     OUTPUT INSERTED.[Id] 
       ,INSERTED.[RowVersion] 
       ,INSERTED.[Data] 
       ,INSERTED.[Type] 
       ,INSERTED.[State] 
       ,INSERTED.[Activity] 

     WHERE [Id] = @JobId; 

    COMMIT TRAN 

END 

Dopo l'elaborazione, lavoro State può essere modificato in 0 (New) nuovamente o può essere una volta impostato 100 (Completed).

CREATE PROCEDURE [dbo].[Jobs_UpdateStatus] 
    @Id   BIGINT, 
    @State  INT, 
    @Activity INT 
AS 
BEGIN 

    UPDATE j 

    SET  j.[State] = @State, 
      j.[Activity] = @Activity, 
      j.[ModificationTimestamp] = SYSUTCDATETIME() 

    OUTPUT INSERTED.[Id], INSERTED.[RowVersion] 

    FROM [dbo].[Jobs] j 

    WHERE j.[Id] = @Id; 

END 

Jobs ha struttura gerarchica, lavoro genitore ottiene State=100 (Completed) solo quando tutti bambino sono stati completati. Alcuni operatori chiamano stored procedure ([dbo].[Jobs_GetCountWithExcludedState] con @ExcludedState=100) che restituiscono il numero di lavori incompleti, quando restituisce 0, il processo padre State può essere impostato su 100 (Completed).

CREATE PROCEDURE [dbo].[Jobs_GetCountWithExcludedState] 
    @ParentId  INT, 
    @ExcludedState INT 
AS 
BEGIN 

    SET TRANSACTION ISOLATION LEVEL READ COMMITTED; 

    SELECT COUNT(1) 

    FROM [dbo].[Jobs] 

    WHERE [Parent_Id] = @ParentId 
    AND  [State] <> @ExcludedState 

END 

Il problema principale è uno strano comportamento di questa stored procedure. A volte restituisce 0 per lavoro principale, ma è esattamente ha lavori incompleti. Ho provato ad attivare la modifica del tracciamento dei dati e alcune informazioni di debug (inclusa la creazione di profili) - lavori secondari 100% non ha State=100 quando SP restituisce 0. Sembra che l'SP salti i record, che non sono nello stato 100 (Completed), ma perché succede e come possiamo impedirlo?

UPD: Calling [dbo].[Jobs_GetCountWithExcludedState] inizia quando lavoro genitore ha bambino. Non ci сan sarà alcuna situazione in cui lavoratore inizia a controllare i processi secondari senza la loro esistenza, perché la creazione di Childs e l'impostazione di lavoro genitore controllare l'attività avvolto in transazione:

using (var ts = new TransactionScope()) 
{ 
    _jobManager.AddChilds(parentJob); 

    parentJob.State = 0; 
    parentJob.Activity = 30; // in this activity worker starts checking child jobs 

    ts.Complete(); 
} 
+1

questo acaro aiuta il tuo problema http://stackoverflow.com/questions/12608780/understanding-sql -server-lock-on-select-query –

+1

e questo http://vladmihalcea.com/2014/09/14/a-beginners-guide-to-database-locking-and-the-lost-update-phenomena/ –

+0

Ho trovato questo BLOCCO SERVER SQL: http://stackoverflow.com/questions/12608780/understanding-sql-server-locks-on-select-queries http: // vladmihalcea.com/2014/09/14/a-principianti-guida-al-database-blocco-e-lost-update-phenomena/ –

risposta

2

Sarebbe molto preoccupante se in realtà la vostra procedura Jobs_GetCountWithExcludedState stava tornando un conteggio di 0 record quando c'erano effettivamente commessi record corrispondenti ai tuoi criteri. È una procedura piuttosto semplice. Quindi ci sono due possibilità:

  • La query non riesce a causa di un problema con SQL Server o danneggiamento dei dati .
  • In realtà non ci sono commessi record corrispondenti ai criteri al momento dell'esecuzione della procedura .

La corruzione è una causa improbabile, ma possibile. È possibile controllare la corruzione con DBCC CHECKDB.

Molto probabilmente non ci sono davvero commessi record di lavoro che hanno un Parent_ID pari al parametro @ParentId e non si trovano in uno stato di 100 nel momento in cui viene eseguito.

Sottolineo commit perché è quello che vedrà la transazione.

Nella tua domanda non viene mai spiegato in che modo lo Parent_ID viene impostato sui lavori. Il mio primo pensiero è che forse stai controllando i lavori secondari non elaborati e non trova nessuno, ma poi un altro processo lo aggiunge come Parent_ID di un altro lavoro incompleto. È questa una possibilità?

Vedo che è stato aggiunto un aggiornamento per mostrare che quando si aggiunge un record di processo figlio che l'aggiornamento dei record padre e figlio viene inserito in una transazione. Questo è buono, ma non la domanda che stavo chiedendo. Questo è lo scenario che sto prendendo in considerazione come possibilità:

  • Un record di lavoro viene inserito e impegnato per il genitore.
  • Jobs_GetFirstByType acquisisce il lavoro parent.
  • Un thread di lavoro elabora e chiama Jobs_UpdateStatus e gli aggiornamenti è stato a 100.
  • Qualcosa chiamate Jobs_GetCountWithExcludedState con il lavoro e restituisce 0.
  • Viene creato un processo secondario e collegato al record processo padre completato ... che lo rende ora nuovamente incompleto.

Non sto dicendo che questo è ciò che sta accadendo ... Sto solo chiedendo se è possibile e quali misure stai prendendo per impedirlo? Ad esempio, nel tuo codice sopra nell'aggiornamento alla tua domanda stai selezionando un ParentJob per collegare il bambino all'esterno della transazione. Potrebbe essere che si sta selezionando un lavoro genitore e poi si completa prima di eseguire la transazione che aggiunge il figlio al genitore? O forse l'ultimo processo figlio di un processo genitore è completo, quindi il thread di lavoro controlla e contrassegna il genitore completo, ma qualche altro thread di lavoro ha già selezionato il lavoro come genitore di un nuovo lavoro secondario?

Ci sono molti diversi scenari che potrebbero causare il sintomo che si sta descrivendo. Credo che il problema si trovi in ​​un codice che non hai condiviso con noi, ma in particolare su come vengono creati i lavori e il codice che circonda le chiamate su Jobs_GetCountWithExcludedState. Se riesci a fornire maggiori informazioni, penso che sarà più probabile trovare una risposta utilizzabile, altrimenti il ​​meglio che possiamo fare è indovinare tutte le cose che potrebbero accadere nel codice che non possiamo vedere.

+0

Chiamando ** Jobs_GetCountWithExcludedState ** per i lavori parent inizia solo quando vengono creati lavori secondari. – Boo

+0

Forse sarebbe d'aiuto se si includesse il codice attorno alla chiamata a Jobs_GetCountWithExcludedState. Indica anche se si trova in una transazione ea quale livello di isolamento. Non penso che ci siano abbastanza informazioni nella tua domanda per determinare quale sia il problema. –

-1

Ho il lato cliente di revisione dei suggerimenti e come si gestisce la durata della transazione e della connessione per ogni thread. Perché tutti i comandi sono in esecuzione sulla transazione del client.

0

Il tuo problema è quasi certamente causato dalla tua scelta del livello di isolamento "READ COMMITTED". Il comportamento di questo dipende dalle impostazioni di configurazione per READ_COMMITTED_SNAPSHOT, ma in entrambi i casi consente a un altro thread di transazione di modificare i record che sarebbero stati visti da SELECT, tra SELECT e UPDATE - in modo da avere una race condition.

Provalo di nuovo con il livello di isolamento "SERIALIZABLE" e verifica se il problema è stato risolto. Per ulteriori informazioni sui livelli di isolamento, la documentazione è molto utile:

https://msdn.microsoft.com/en-AU/library/ms173763.aspx

+0

'READ_COMMITTED_SNAPSHOT' è disattivato. Non capisco, perché si verifica una condizione di competizione - "UPDATE" può essere chiamato prima/durante/dopo "SELECT", ma comunque, "SELECT" dovrebbe restituire dati reali o sbaglio? Quando il lavoro ottiene ** Stato = 100 **, non si verificano più altri aggiornamenti. Quindi, situazione, quando SP legge i record in 100 stati e dopo averli letti è impossibile. Hai ragione, ho già provato il livello di isolamento 'SERIALIZABLE', aiuta, ma in questo caso vediamo un sacco di deadlock e le prestazioni complessive sono considerevolmente ridotte. – Boo

+0

Ho visto anche questo ... ma non è un problema a causa dei suggerimenti di blocco sulla selezione ('UPDLOCK' e' READPAST'). 'UPDLOCK' impedisce ad un'altra transazione di modificare i dati selezionati fino al completamento della transazione. Il 'READPAST' impedisce la selezione dal blocco quando un altro processo sta elaborando un altro record. Penso che questo va bene e fa ciò che è inteso. –

+0

Sono d'accordo che Jobs_GetFirstByType dovrebbe essere privo di corse, anche se a volte potrebbe leggere READPAST e non ottenere alcun lavoro anche quando ne esiste uno. Tuttavia, penso che Jobs_GetCountWithExcludedState abbia bisogno di essere serializzabile. È difficile dire senza vedere una sequenza specifica che porta al fallimento. – cliffordheath

0

Il codice SQL osserva benissimo. Pertanto, il problema sta nel modo in cui viene utilizzato.

Ipotesi # 0
Procedura "Jobs_GetCountWithExcludedState" è chiamato con un ID di totalmente sbagliato. Perché sì qualche problema è in realtà solo un piccolo errore. Dubito che questo sia il tuo caso comunque.


Ipotesi # 1
Il codice di verifica sul campo "attività = 30" sta facendo in "READ UNCOMMITED" livello di isolamento. Chiamerebbe quindi "Jobs_GetCountWithExcludedState" con un parentID che potrebbe non essere realmente pronto per questo perché la transazione di inserimento potrebbe non essere ancora terminata o essere stata sottoposta a rollback.


Ipotesi # 2
Procedura "Jobs_GetCountWithExcludedState" viene chiamata con un ID che non ha figli più. Ci potrebbero essere molte ragioni per cui questo accada.
Per esempio,

  • transazione che inserito processo secondario non riuscito per qualsiasi motivo, ma questa procedura è stato chiamato in ogni caso.
  • Un lavoro figlio singolo è stato eliminato e stava per essere sostituito.
  • ecc

Ipotesi # 3
Procedura "Jobs_GetCountWithExcludedState" viene chiamato prima il childJob ottenere il loro parentId assegnato.


Conclusione
Come potete vedere, abbiamo bisogno di maggiori informazioni su due cose:
1. Come "Jobs_GetCountWithExcludedState" si chiama.
2. Come vengono inseriti i lavori. Il parentId è assegnato al momento dell'inserimento o viene aggiornato un po 'più tardi? Sono inseriti in batch? C'è un codice allegato ad esso che fa altre cose?

Questo è anche il punto in cui ti consiglio di verificare se l'ipotesi sopra è il problema più probabile nel programma.


possibile refactoring per invalidare tutte quelle ipotesi
Avere il database dire al programma che le attività genitori sono stati completati direttamente invece.

  • Proprio come "Jobs_GetFirstByType", ci potrebbe essere Jobs_GetFirstParentJobToComplete "che potrebbe restituire il prossimo lavoro genitore incompiuto con bambino completate eventuali. Potrebbe anche essere una visione che restituiscono tutti loro. Modi entrambi i casi, l'utilizzo di" Jobs_GetCountWithExcludedState "non sarebbe più usato quindi invalida tutte le mie ipotesi." La nuova procedura o visualizzazione dovrebbe essere LETTURA COMMERCIALE o superiore