Các cuộc gọi đồng thời đến cùng chức năng: làm thế nào là bế tắc xảy ra?


15

Chức năng của tôi new_customerđược một ứng dụng web gọi vài lần mỗi giây (nhưng chỉ một lần mỗi phiên). Điều đầu tiên nó làm là khóa customerbảng (để thực hiện 'chèn nếu không tồn tại' một biến thể đơn giản của một upsert).

Sự hiểu biết của tôi về các tài liệu là các cuộc gọi khác new_customerchỉ nên xếp hàng cho đến khi tất cả các cuộc gọi trước kết thúc:

LOCK TABLE có được một khóa cấp bảng, chờ đợi nếu cần thiết cho bất kỳ khóa xung đột nào được phát hành.

Tại sao đôi khi nó bế tắc thay thế?

Định nghĩa:

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;
$$;

lỗi từ nhật ký:

2015-07-28 08:02:58 BST CHI TIẾT: Quá trình 12380 chờ đợi ExclusiveLock về mối quan hệ 16438 của cơ sở dữ liệu 12141; bị chặn bởi quá trình 12379.
        Quá trình 12379 chờ ExclusiveLock về mối quan hệ 16438 của cơ sở dữ liệu 12141; bị chặn bởi quá trình 12380.
        Quá trình 12380: chọn new_customer (giải mã ($ 1 :: văn bản, 'hex'))
        Quy trình 12379: chọn new_customer (giải mã ($ 1 :: văn bản, 'hex'))
2015-07-28 08:02:58 BST GỢI Ý: Xem nhật ký máy chủ để biết chi tiết truy vấn.
2015-07-28 08:02:58 BST TIẾP THEO: Câu lệnh SQL "new_customer" 1
2015-07-28 08:02:58 TUYÊN BỐ BST: chọn new_customer (giải mã ($ 1 :: văn bản, 'hex'))

quan hệ:

postgres=# select relname from pg_class where oid=16438;
┌──────────┐
 relname  
├──────────┤
 customer 
└──────────┘

biên tập:

Tôi đã quản lý để có được một trường hợp thử nghiệm tái sản xuất đơn giản. Đối với tôi điều này trông giống như một lỗi do một số điều kiện cuộc đua.

lược đồ:

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;
$$;

bash script chạy đồng thời trong hai phiên bash:

for i in {1..1000}; do psql postgres postgres -c "select f_test('blah')"; done

nhật ký lỗi (thường là một số ít các bế tắc trong 1000 cuộc gọi):

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')

chỉnh sửa 2:

@ypercube đề xuất một biến thể với lock tablechức năng bên ngoài:

for i in {1..1000}; do psql postgres postgres -c "begin; lock test in exclusive mode; select f_test('blah'); end"; done

thú vị là điều này giúp loại bỏ những bế tắc.


2
Trong cùng một giao dịch, trước khi vào chức năng đó, được customersử dụng theo cách nào sẽ lấy khóa yếu hơn? Sau đó, nó có thể là một vấn đề nâng cấp khóa.
Daniel Vérité

2
Tôi không thể giải thích điều này. Daniel có thể có một điểm. Có thể đáng để nâng cao điều này trên pssql-chung. Dù bằng cách nào, bạn có biết về việc triển khai UPSERT trong Postgres 9.5 sắp tới không? Depesz đang xem nó.
Erwin Brandstetter

2
Ý tôi là trong cùng một giao dịch, không chỉ cùng một phiên (vì các khóa được phát hành ở cuối tx). Câu trả lời của @alexk là những gì tôi đã nghĩ, nhưng nếu tx bắt đầu và kết thúc với chức năng, điều đó không thể giải thích được sự bế tắc.
Daniel Vérité

1
@Erwin bạn chắc chắn sẽ quan tâm đến câu trả lời tôi nhận được từ việc đăng bài tại pssql-bug :)
Jack Douglas

2
Thực sự rất thú vị. Cũng có nghĩa là điều này cũng hoạt động trong plpgsql, vì tôi nhớ các trường hợp plpgsql tương tự hoạt động như mong đợi.
Erwin Brandstetter

Câu trả lời:


10

Tôi đã đăng bài này lên các lỗi pssqlcâu trả lời từ Tom Lane cho thấy đây là vấn đề leo thang khóa, được ngụy trang bởi các cơ chế về cách xử lý các chức năng ngôn ngữ SQL. Về cơ bản, khóa được tạo bởi insertđược lấy trước khóa độc quyền trên bàn :

Tôi tin rằng vấn đề với điều này là một hàm SQL sẽ thực hiện phân tích cú pháp (và có thể lập kế hoạch quá; không cảm thấy muốn kiểm tra mã ngay bây giờ) cho toàn bộ thân hàm cùng một lúc. Điều này có nghĩa là do lệnh INSERT bạn có được RowExinatingLock trên bảng "kiểm tra" trong quá trình phân tích cú pháp cơ thể, trước khi lệnh LOCK thực sự thực thi. Vì vậy, LOCK đại diện cho một nỗ lực leo thang khóa, và những bế tắc sẽ được dự kiến.

Kỹ thuật mã hóa này sẽ an toàn trong plpgsql, nhưng không phải trong chức năng ngôn ngữ SQL.

Đã có các cuộc thảo luận về việc thực hiện lại các chức năng ngôn ngữ SQL để việc phân tích cú pháp xảy ra một câu lệnh tại một thời điểm, nhưng đừng nín thở về điều gì đó xảy ra theo hướng đó; nó dường như không phải là một mối quan tâm ưu tiên cao cho bất cứ ai.

liên quan, tom lane

Điều này cũng giải thích tại sao việc khóa bảng bên ngoài hàm trong một khối plpgsql (như được đề xuất bởi @ypercube) ngăn chặn các bế tắc.


3
Điểm hay: ypercube thực sự đã kiểm tra khóa trong SQL đơn giản trong một giao dịch rõ ràng bên ngoài một hàm, không giống như khối plpgsql .
Erwin Brandstetter

1
Hoàn toàn đúng, xấu của tôi. Tôi nghĩ rằng tôi đã bị nhầm lẫn với một điều khác mà chúng tôi đã cố gắng (điều đó không ngăn chặn được sự bế tắc).
Jack Douglas

4

Giả sử bạn chạy một câu lệnh khác trước khi gọi new_customer và những câu lệnh đó có khóa xung đột với EXCLUSIVE(về cơ bản, bất kỳ sửa đổi dữ liệu nào trong bảng khách hàng), việc giải thích rất đơn giản.

Người ta có thể tái tạo vấn đề bằng một ví dụ đơn giản (thậm chí không bao gồm hàm):

CREATE TABLE test(id INTEGER);

Phiên thứ 1:

BEGIN;

INSERT INTO test VALUES(1);

Phiên thứ 2

BEGIN;
INSERT INTO test VALUES(1);
LOCK TABLE test IN EXCLUSIVE MODE;

Phiên thứ 1

LOCK TABLE test IN EXCLUSIVE MODE;

Khi phiên đầu tiên thực hiện thao tác chèn, nó sẽ lấy ROW EXCLUSIVEkhóa trên bàn. Trong khi đó, phiên 2 cố gắng cũng có được ROW EXCLUSIVEkhóa và cố gắng để có được một EXCLUSIVEkhóa. Tại thời điểm đó, nó phải chờ phiên thứ 1, vì EXCLUSIVEkhóa xung đột với ROW EXCLUSIVE. Cuối cùng, phiên thứ 1 nhảy lên những con cá mập và cố gắng để có được một EXCLUSIVEkhóa, nhưng vì các khóa được mua theo thứ tự, nó xếp hàng sau phiên thứ 2. Đến lượt mình, điều này lại chờ đợi người thứ nhất, tạo ra bế tắc:

DETAIL:  Process 28514 waits for ExclusiveLock on relation 58331454 of database 44697822; blocked by process 28084.
Process 28084 waits for ExclusiveLock on relation 58331454 of database 44697822; blocked by process 28514

Giải pháp cho vấn đề này là mua khóa càng sớm càng tốt, thường là điều đầu tiên trong giao dịch. Mặt khác, khối lượng công việc của PostgreSQL chỉ cần khóa trong một số trường hợp rất hiếm, vì vậy tôi khuyên bạn nên xem xét lại cách bạn thực hiện upert (hãy xem bài viết này http://www.depesz.com/2012/06/10 / why-is-upsert-so-phức tạp / ).


2
Điều này thật thú vị, nhưng thông báo trong nhật ký db sẽ đọc một cái gì đó như: Process 28514 : select new_customer(decode($1::text, 'hex')); Process 28084 : BEGIN; INSERT INTO test VALUES(1); select new_customer(decode($1::text, 'hex'))Trong khi Jack vừa nhận: Process 12380: select new_customer(decode($1::text, 'hex')) Process 12379: select new_customer(decode($1::text, 'hex'))- chỉ ra rằng lệnh gọi hàm là lệnh đầu tiên trong cả hai giao dịch (trừ khi tôi thiếu thứ gì đó).
Erwin Brandstetter

Cảm ơn, và tôi đồng ý với những gì bạn nói, nhưng điều này dường như không phải là nguyên nhân trong trường hợp này. Điều đó rõ ràng hơn trong trường hợp thử nghiệm tối thiểu hơn mà tôi đã thêm vào câu hỏi (mà bạn có thể tự mình thử).
Jack Douglas

2
Trên thực tế, hóa ra bạn đã đúng về việc leo thang khóa - mặc dù cơ chế này rất tinh tế .
Jack Douglas
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.