Moja funkcja new_customer
jest wywoływana kilka razy na sekundę (ale tylko raz na sesję) przez aplikację internetową. Pierwszą rzeczą, jaką robi, jest zablokowanie customer
tabeli (wykonanie „wstaw, jeśli nie istnieje” - prosty wariant upsert
).
Rozumiem te dokumenty, że inne wywołania new_customer
powinny po prostu stać w kolejce, aż wszystkie poprzednie połączenia zakończą się:
LOCK TABLE uzyskuje blokadę na poziomie tabeli, czekając w razie potrzeby na zwolnienie wszelkich sprzecznych blokad.
Dlaczego zamiast tego czasami się zakleszcza?
definicja:
create function new_customer(secret bytea) returns integer language sql
security definer set search_path = postgres,pg_temp as $$
lock customer in exclusive mode;
--
with w as ( insert into customer(customer_secret,customer_read_secret)
select secret,decode(md5(encode(secret, 'hex')),'hex')
where not exists(select * from customer where customer_secret=secret)
returning customer_id )
insert into collection(customer_id) select customer_id from w;
--
select customer_id from customer where customer_secret=secret;
$$;
błąd z dziennika:
2015-07-28 08:02:58 SZCZEGÓŁY BST: Proces 12380 czeka na ExclusiveLock w relacji 16438 bazy danych 12141; zablokowane przez proces 12379. Proces 12379 czeka na ExclusiveLock na relacji 16438 bazy danych 12141; zablokowane przez proces 12380. Przetwarzaj 12380: wybierz nowego klienta (dekoduj (1 $ :: tekst, „hex”)) Proces 12379: wybierz nowego klienta (dekoduj (1 $ :: tekst, „hex”)) 2015-07-28 08:02:58 BST WSKAZÓWKA: szczegółowe informacje dotyczące zapytań znajdują się w dzienniku serwera. 2015-07-28 08:02:58 KONTEKST BST: Funkcja SQL „new_customer” instrukcja 1 2015-07-28 08:02:58 OŚWIADCZENIE BST: wybierz nowego klienta (dekoduj (1 $ :: tekst, „hex”))
relacja:
postgres=# select relname from pg_class where oid=16438;
┌──────────┐
│ relname │
├──────────┤
│ customer │
└──────────┘
edytować:
Udało mi się uzyskać prosty, powtarzalny przypadek testowy. Dla mnie wygląda to na błąd z powodu warunków wyścigu.
schemat:
create table test( id serial primary key, val text );
create function f_test(v text) returns integer language sql security definer set search_path = postgres,pg_temp as $$
lock test in exclusive mode;
insert into test(val) select v where not exists(select * from test where val=v);
select id from test where val=v;
$$;
skrypt bash działa jednocześnie w dwóch sesjach bash:
for i in {1..1000}; do psql postgres postgres -c "select f_test('blah')"; done
dziennik błędów (zwykle garść zakleszczeń na 1000 połączeń):
2015-07-28 16:46:19 BST ERROR: deadlock detected
2015-07-28 16:46:19 BST DETAIL: Process 9394 waits for ExclusiveLock on relation 65605 of database 12141; blocked by process 9393.
Process 9393 waits for ExclusiveLock on relation 65605 of database 12141; blocked by process 9394.
Process 9394: select f_test('blah')
Process 9393: select f_test('blah')
2015-07-28 16:46:19 BST HINT: See server log for query details.
2015-07-28 16:46:19 BST CONTEXT: SQL function "f_test" statement 1
2015-07-28 16:46:19 BST STATEMENT: select f_test('blah')
edycja 2:
@ypercube zasugerował wariant z lock table
funkcją zewnętrzną:
for i in {1..1000}; do psql postgres postgres -c "begin; lock test in exclusive mode; select f_test('blah'); end"; done
co ciekawe, eliminuje to impasy.
źródło
customer
używany w sposób, który uchwyciłby słabszy zamek? Może to być problem z uaktualnieniem blokady.Odpowiedzi:
Wysłałem to do pgsql-bugs, a odpowiedź tamta Tom Lane wskazuje, że jest to problem eskalacji blokady, ukryty pod mechaniką sposobu przetwarzania funkcji języka SQL. Zasadniczo blokada wygenerowana przez
insert
jest uzyskiwana przed wyłączną blokadą na stole :To wyjaśnia również, dlaczego zablokowanie tabeli poza funkcją w zawijanym bloku plpgsql (jak sugeruje @ypercube) zapobiega zakleszczeniom.
źródło
Zakładając, że uruchamiasz inne instrukcje przed wywołaniem new_customer, a te uzyskują blokadę, która jest w konflikcie
EXCLUSIVE
(zasadniczo każda modyfikacja danych w tabeli klientów), wyjaśnienie jest bardzo proste.Problem można odtworzyć na prostym przykładzie (nawet bez funkcji):
Pierwsza sesja:
2. sesja
1. sesja
Kiedy pierwsza sesja wykonuje wstawkę, uzyskuje
ROW EXCLUSIVE
blokadę na stole. Tymczasem sesja 2 próbuje również uzyskaćROW EXCLUSIVE
blokadę i próbuje uzyskaćEXCLUSIVE
blokadę. W tym momencie musi czekać na pierwszą sesję, ponieważEXCLUSIVE
blokada powoduje konfliktROW EXCLUSIVE
. W końcu pierwsza sesja skacze rekinom i próbuje zdobyćEXCLUSIVE
zamek, ale ponieważ zamki są zdobywane w kolejności, kolejkuje po drugiej sesji. To z kolei czeka na 1., powodując impas:Rozwiązaniem tego problemu jest uzyskanie blokad tak wcześnie, jak to możliwe, zwykle jako pierwsza rzecz w transakcji. Z drugiej strony obciążenie PostgreSQL wymaga blokad tylko w niektórych bardzo rzadkich przypadkach, dlatego sugeruję przemyślenie sposobu, w jaki robisz upsert (spójrz na ten artykuł http://www.depesz.com/2012/06/10 / dlaczego-jest-upsert-tak skomplikowane / ).
źródło
Process 28514 : select new_customer(decode($1::text, 'hex')); Process 28084 : BEGIN; INSERT INTO test VALUES(1); select new_customer(decode($1::text, 'hex'))
mniej więcej tak: Podczas gdy Jack właśnie dostał:Process 12380: select new_customer(decode($1::text, 'hex')) Process 12379: select new_customer(decode($1::text, 'hex'))
- wskazując, że wywołanie funkcji jest pierwszym poleceniem w obu transakcjach (chyba że czegoś brakuje).