12

Sto implementando un semplice lettore RSS basato sul web usando python (non proprio rilevante) e Postgresql (9.2 se rilevante). Lo schema del database è la seguente (basato sul formato RSS):Inserisci riga se non esiste conduce a condizioni di gara?

CREATE TABLE feed_channel 
(
    id SERIAL PRIMARY KEY, 
    name TEXT, 
    link TEXT NOT NULL, 
    title TEXT 
); 
CREATE TABLE feed_content 
(
    id SERIAL PRIMARY KEY, 
    channel INTEGER REFERENCES feed_channel(id) ON DELETE CASCADE ON UPDATE CASCADE, 
    guid TEXT UNIQUE NOT NULL, 
    title TEXT, 
    link TEXT, 
    description TEXT, 
    pubdate TIMESTAMP 
); 

Quando creo un nuovo canale (e anche interrogare per info feed aggiornato) Chiedo il feed, inserire i propri dati alla tabella feed_channel, seleziona l'ID appena inserito - o esistente per evitare duplicati - e quindi aggiungere i dati del feed alla tabella feed_content. Uno scenario tipico potrebbe essere:

  1. Query l'URL del feed, header feed afferrare e tutti i contenuti attuali
  2. Inserire le header feed in feed_channel se non esiste ... se esiste già, afferrare l'ID esistente
  3. Per ogni voce di feed, inserire nella tabella feed_content con un riferimento all'ID canale memorizzato

Questo è un problema standard "Inserisci se non esiste già, ma restituisce ID pertinente". Per risolvere questo ho implementato la stored procedure seguente:

CREATE OR REPLACE FUNCTION channel_insert(
    p_link feed_channel.link%TYPE, 
    p_title feed_channel.title%TYPE 
) RETURNS feed_channel.id%TYPE AS $$ 
    DECLARE 
    v_id feed_channel.id%TYPE; 
    BEGIN 
    SELECT id 
    INTO v_id 
    FROM feed_channel 
    WHERE link=p_link AND title=p_title 
    LIMIT 1; 

    IF v_id IS NULL THEN 
     INSERT INTO feed_channel(name,link,title) 
     VALUES (DEFAULT,p_link,p_title) 
     RETURNING id INTO v_id; 
    END IF; 

    RETURN v_id; 

    END; 
$$ LANGUAGE plpgsql; 

Questo viene poi chiamato come "selezionare channel_insert (link, titolo);" dalla mia applicazione per inserire se non esiste già e quindi restituire l'ID della riga pertinente indipendentemente dal fatto che sia stato inserito o appena trovato (passaggio 2 nella lista sopra).

Questo funziona benissimo!

Tuttavia, recentemente ho iniziato a chiedermi cosa succederebbe se questa procedura fosse eseguita due volte contemporaneamente con gli stessi argomenti. Consente di assumere la seguente:

  1. utente 1 tenta di aggiungere un nuovo canale e quindi eseguire channel_insert
  2. pochi ms dopo, User 2 tentativi per aggiungere sullo stesso canale e anche la tecnologia execute channel_insert
  3. utente 1 di controllo per le righe esistenti sono completate, ma prima che l'inserimento sia completo, il controllo dell'Utente 2 viene completato e dice che non ci sono righe esistenti.

Questa è una potenziale condizione di competizione in PostgreSQL? Qual è il modo migliore per risolvere questo problema per evitare tali scenari? È possibile rendere atomicamente l'intera procedura memorizzata, cioè che può essere eseguita una sola volta nello stesso momento?

Un'opzione che ho provato era quella di rendere i campi Unici e poi tentare di inserire prima, e se l'eccezione, invece, selezionare l'esistente ... Questo funzionava, tuttavia, il campo SERIAL avrebbe incrementato per ogni tentativo, lasciando un sacco di lacune nella sequenza. Non so se questo sarebbe un problema a lungo termine (probabilmente no), ma piuttosto fastidioso. Forse questa è la soluzione preferita?

Grazie per qualsiasi feedback. Questo livello di magia di PostgreSQL è al di fuori di me, quindi qualsiasi feedback sarebbe apprezzato.

+1

Non importa quello che fai, stai attento a normalizzare il formato link in modo che non avete problemi di casi ('' Www.Example.Com' e www.example .com'), l'ordine dei parametri issus ('? a = b & c = d' e'? c = d & a = b'), ecc. –

+0

Un loop di funzione plpgsql in caso di violazione di una chiave duplicata può gestire la condizione della competizione sul lato server e al livello di isolamento predefinito, che è * sicuro * tipicamente * più economico *: http://stackoverflow.com/questions/15939902/is-select-or-insert-in-a-function-prone-to- condizioni di gara/15950324 # 15950324 –

risposta

4

C'è una "gara" inevitabile qui, dal momento che due sessioni non possono "vedere" le righe non ammesse di ciascuno. In un conflitto, una sessione può solo eseguire il rollback (magari in un punto di salvataggio) e riprovare. Ciò significherebbe in genere: fare riferimento alla riga appena inserita dell'altro, invece di creare un duplicato privato.

C'è un problema di modellazione dei dati qui: feed_channel sembra avere molte chiavi candidate, e la regola a cascata da feed_content potrebbe orfanizzare un sacco di righe di feed_content (suppongo content-> channel è 1 :: M relation; più di una riga di contenuto potrebbe riferirsi allo stesso canale)

Infine, la tabella feed_channel almeno ha bisogno di la chiave naturale {link, title}. Ecco dove si trova l'inserto/non esiste. (e lo scopo di questa funzione)

Ho ripulito la funzione un po '. Il costrutto IF non è necessario, facendo un INSERT DOVE NON ESISTEprimo funziona altrettanto bene, e forse anche meglio.

DROP SCHEMA tmp CASCADE; 
CREATE SCHEMA tmp ; 
SET search_path=tmp; 

CREATE TABLE feed_channel 
    (id SERIAL PRIMARY KEY 
    , name TEXT 
    , link TEXT NOT NULL 
    , title TEXT NOT NULL -- part of PK :: must be not nullable 
    , CONSTRAINT feed_channel_nat UNIQUE (link,title) -- the natural key 
); 

CREATE TABLE feed_content 
    (id SERIAL PRIMARY KEY 
    , channel INTEGER REFERENCES feed_channel(id) ON DELETE CASCADE ON UPDATE CASCADE 
    , guid TEXT UNIQUE NOT NULL -- yet another primary key 
    , title TEXT -- 
    , link TEXT -- title && link appear to be yet another candidate key 
    , description TEXT 
    , pubdate TIMESTAMP 
    ); 

-- NOTE: omitted original function channel_insert() for brevity 
CREATE OR REPLACE FUNCTION channel_insert_wp(
    p_link feed_channel.link%TYPE, 
    p_title feed_channel.title%TYPE 
) RETURNS feed_channel.id%TYPE AS $body$ 
    DECLARE 
    v_id feed_channel.id%TYPE; 
    BEGIN 
     INSERT INTO feed_channel(link,title) 
     SELECT p_link,p_title 
     WHERE NOT EXISTS (SELECT * 
     FROM feed_channel nx 
     WHERE nx.link= p_link 
     AND nx.title= p_title 
     ) 
     ; 
    SELECT id INTO v_id 
    FROM feed_channel ex 
    WHERE ex.link= p_link 
    AND ex.title= p_title 
     ; 

    RETURN v_id; 

    END; 
$body$ LANGUAGE plpgsql; 

SELECT channel_insert('Bogus_link', 'Bogus_title'); 
SELECT channel_insert_wp('Bogus_link2', 'Bogus_title2'); 

SELECT * FROM feed_channel; 

Risultati:

DROP SCHEMA 
CREATE SCHEMA 
SET 
NOTICE: CREATE TABLE will create implicit sequence "feed_channel_id_seq" for serial column "feed_channel.id" 
NOTICE: CREATE TABLE/PRIMARY KEY will create implicit index "feed_channel_pkey" for table "feed_channel" 
NOTICE: CREATE TABLE/UNIQUE will create implicit index "feed_channel_nat" for table "feed_channel" 
CREATE TABLE 
NOTICE: CREATE TABLE will create implicit sequence "feed_content_id_seq" for serial column "feed_content.id" 
NOTICE: CREATE TABLE/PRIMARY KEY will create implicit index "feed_content_pkey" for table "feed_content" 
NOTICE: CREATE TABLE/UNIQUE will create implicit index "feed_content_guid_key" for table "feed_content" 
CREATE TABLE 
NOTICE: type reference feed_channel.link%TYPE converted to text 
NOTICE: type reference feed_channel.title%TYPE converted to text 
NOTICE: type reference feed_channel.id%TYPE converted to integer 
CREATE FUNCTION 
NOTICE: type reference feed_channel.link%TYPE converted to text 
NOTICE: type reference feed_channel.title%TYPE converted to text 
NOTICE: type reference feed_channel.id%TYPE converted to integer 
CREATE FUNCTION 
channel_insert 
---------------- 
       1 
(1 row) 

channel_insert_wp 
------------------- 
       2 
(1 row) 

id | name | link  | title  
----+------+-------------+-------------- 
    1 |  | Bogus_link | Bogus_title 
    2 |  | Bogus_link2 | Bogus_title2 
(2 rows) 
+1

Se mi cade la stored procedure del tutto e invece basta usare: 'INSERT INTO feed_channel (fonte) SELEZIONARE% (fonte) s DOVE NON ESISTE ( SELECT 1 FROM feed_channel DOVE source =% (fonte) s );' potrò mai potenzialmente innescare una violazione unica supponendo che "fonte" abbia un vincolo univoco? – agnsaft

3

Il tuo primo problema è che un serial non costituisce una buona chiave primaria per la tabella feed_channel. La chiave primaria deve essere (link, title) o solo (link) se title può essere null. Quindi qualsiasi tentativo di inserire un feed esistente potrebbe generare un errore chiave primaria.

BTW v_id saranno null ogni volta title è null:

WHERE link=p_link AND title=p_title 
+0

Questo è un'idea interessante, ma non sarà doloroso fare una chiave di riferimento/straniera se la chiave primaria è composta da più campi? Inoltre, ho provato a creare un vincolo univoco per ottenere lo stesso risultato, tuttavia, poiché ho bisogno di restituire l'ID corretto anche in caso di errore dell'inserto a causa del vincolo univoco ho abbandonato tale approccio. Inoltre, ho trovato che fosse una seccatura trattare i commit quando improvvisamente si verifica un guasto a causa di contusioni. – agnsaft

+1

@invictus Perché sarebbe un dolore fare un riferimento/chiave esterna se la chiave primaria consisteva di più campi? Non ci sarebbe 'ID'. Non dovresti restituire nulla visto che già sapevi quale chiave naturale (link, titolo) hai provato a inserire. –

4

Sarà questa una condizione potenziale gara in PostgreSQL?

Sì, e in effetti sarebbe in qualsiasi motore di database.

Qual è il modo migliore per risolvere questo problema per evitare tali scenari?

Questa è una domanda caricata e richiederebbe una conoscenza approfondita dell'utilizzo del database da parte di più utenti. Tuttavia, ho intenzione di darvi alcune opzioni. In breve, l'unica opzione che hai è quella LOCK tavola durante questo processo, ma come di bloccare quel tavolo dipenderà da come il database viene utilizzato per tutta la giornata.

Cominciamo con il base LOCK:

LOCK TABLE feed_channel 

che blocca la tabella utilizzando l'opzione ACCESS EXCLUSIVE serratura.

conflitti con serrature di tutte le modalità (ACCESSO SHARE, ROW SHARE, ROW ESCLUSIVO, SHARE UPDATE esclusiva, la quota, SHARE ROW ESCLUSIVO, esclusivo ed accesso esclusivo). Questa modalità garantisce che il titolare è l'unica transazione che accede al tavolo in alcun modo.

Ora, questo è il blocco più restrittivo disponibile e risolverebbe sicuramente le condizioni della competizione, ma potrebbe non essere esattamente quello che desideri. È qualcosa che dovrai decidere. Quindi, anche se è chiaro si sta andando ad avere per LOCK tavolo, non è chiaro come.

Cosa resta da decidere?

  1. Come cosa desiderate LOCK the table? Studia le opzioni di blocco a quel link per rendere la tua determinazione.
  2. Dove vuoi il tavolo LOCK? O in altre parole, vuoi LOCK in superiore della funzione (che credo non sulla base del possibile condizione di gara), o non si vuole semplicemente LOCK destra prima del INSERT?

E 'possibile rendere l'intera stored procedure atomicamente, vale a dire che può essere eseguito solo una volta, allo stesso tempo?

No, il codice può essere eseguito da chiunque sia collegato al database.


Spero che questo ti abbia aiutato a indirizzarti.

Problemi correlati