Domanda

Ho una tabella [File] che ha il seguente schema

CREATE TABLE [dbo].[File]
(
    [FileID] [int] IDENTITY(1,1) NOT NULL,
    [Name] [varchar](256) NOT NULL,
 CONSTRAINT [PK_File] PRIMARY KEY CLUSTERED 
(
    [FileID] ASC
)WITH (PAD_INDEX  = OFF, STATISTICS_NORECOMPUTE  = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS  = ON, ALLOW_PAGE_LOCKS  = ON) ON [PRIMARY]
) ON [PRIMARY]

L'idea è che il FileID sia usato come chiave per la tabella e il Nome sia il percorso completo che rappresenta un file.

Quello che ho cercato di fare è creare una Stored Procedure che verificherà se il Nome è già in uso, in tal caso quindi utilizzare quel record altrimenti creare un nuovo record.

Ma quando provo lo stress test del codice con molti thread che eseguono contemporaneamente la procedura memorizzata ottengo diversi errori.

Questa versione del codice creerà un deadlock e genererà un'eccezione di deadlock sul client.

CREATE PROCEDURE [dbo].[File_Create]
    @Name varchar(256)
AS
    SET TRANSACTION ISOLATION LEVEL SERIALIZABLE
    BEGIN TRANSACTION xact_File_Create
    SET XACT_ABORT ON

    SET NOCOUNT ON 
    DECLARE @FileID int
    SELECT @FileID = [FileID] FROM [dbo].[File] WHERE [Name] = @Name
    IF @@ROWCOUNT=0
    BEGIN
        INSERT INTO [dbo].[File]([Name])
        VALUES (@Name)
        SELECT @FileID = [FileID] FROM [dbo].[File] WHERE [Name] = @Name
    END

    SELECT * FROM [dbo].[File]
    WHERE [FileID] = @FileID

    COMMIT TRANSACTION xact_File_Create
GO

Questa versione del codice finisce per ottenere righe con gli stessi dati nella colonna Nome.

CREATE PROCEDURE [dbo].[File_Create]
    @Name varchar(256)
AS
    BEGIN TRANSACTION xact_File_Create

    SET NOCOUNT ON 
    DECLARE @FileID int
    SELECT @FileID = [FileID] FROM [dbo].[File] WHERE [Name] = @Name
    IF @@ROWCOUNT=0
    BEGIN
        INSERT INTO [dbo].[File]([Name])
        VALUES (@Name)
        SELECT @FileID = [FileID] FROM [dbo].[File] WHERE [Name] = @Name
    END

    SELECT * FROM [dbo].[File]
    WHERE [FileID] = @FileID

    COMMIT TRANSACTION xact_File_Create
GO

Mi chiedo quale sia il modo giusto di fare questo tipo di azione? In generale, questo è un modello che vorrei utilizzare in cui i dati della colonna sono univoci in una singola colonna o in più colonne e un'altra colonna viene utilizzata come chiave.

Grazie

È stato utile?

Soluzione

Se stai effettuando una ricerca approfondita nel campo Nome, probabilmente lo vorrai indicizzato (come univoco e forse anche raggruppato se questo è il campo di ricerca primario ). Dato che non usi @FileID dalla prima selezione, selezionerei semplicemente count (*) dal file dove Name = @Name e vedo se è maggiore di zero (questo impedirà a SQL di conservare i blocchi sulla tabella da la fase di ricerca, poiché nessuna colonna è selezionata).

Sei sulla strada giusta con il livello SERIALIZZABILE, poiché la tua azione influirà sul successo o il fallimento delle query successive con la presenza del Nome. Il motivo per cui la versione senza quel set causa duplicati è che due selezioni sono state eseguite contemporaneamente e hanno scoperto che non c'erano record, quindi entrambi sono andati avanti con gli inserti (che creano il duplicato).

Il deadlock con la versione precedente è molto probabilmente dovuto alla mancanza di un indice che richiede molto tempo per il processo di ricerca. Quando si carica il server in una transazione SERIALIZZABILE, tutto il resto dovrà attendere il completamento dell'operazione. L'indice dovrebbe velocizzare l'operazione, ma solo i test indicheranno se è abbastanza veloce. Si noti che è possibile rispondere alla transazione non riuscita reinviando: nelle situazioni del mondo reale si spera che il carico sia transitorio.

MODIFICA: Rendendo la tabella indicizzata, ma non usando SERIALIZABLE, si ottengono tre casi:

  • Nome trovato, ID acquisito e utilizzato. Comune
  • Il nome non è stato trovato, si inserisce come previsto. Comune
  • Il nome non viene trovato, l'inserimento non riesce perché un'altra corrispondenza esatta è stata pubblicata entro millisecondi dal primo. Molto raro

Mi aspetterei che quest'ultimo caso fosse davvero eccezionale, quindi usare un'eccezione per catturare questo caso molto raro sarebbe preferibile impegnarsi in SERIALIZZABILE, che ha gravi conseguenze sulle prestazioni.

Se hai davvero l'aspettativa che sarà comune avere post tra millisecondi l'uno dell'altro con lo stesso nuovo nome, usa una transazione SERIALIZZABILE insieme all'indice. Sarà più lento nel caso generale, ma più veloce quando vengono trovati questi post.

Altri suggerimenti

Innanzitutto, crea un indice univoco nella colonna Nome. Quindi dal tuo codice client controlla prima se esiste il Nome selezionando il FileID e inserendo il Nome nella clausola where - in caso affermativo, usa il FileID. In caso contrario, inserirne uno nuovo.

L'uso della funzione Exists potrebbe ripulire un po 'le cose.

if (Exists(select * from table_name where column_name = @param)
begin
  //use existing file name
end
else
  //use new file name
Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top