โทรไปยังฟังก์ชั่นเดียวกันพร้อมกัน: เกิดการชะงักงันได้อย่างไร


15

ฟังก์ชั่นของฉันnew_customerถูกเรียกหลายครั้งต่อวินาที (แต่เพียงครั้งเดียวต่อเซสชัน) โดยเว็บแอปพลิเคชัน สิ่งแรกที่มันทำคือล็อคcustomerตาราง (เพื่อทำการ 'แทรกถ้าไม่มี' - ตัวแปรง่าย ๆ ของupsert)

ความเข้าใจของฉันเกี่ยวกับเอกสารก็คือการโทรอื่น ๆnew_customerควรเพียงแค่รอจนกว่าการโทรก่อนหน้านี้ทั้งหมดจะเสร็จสิ้น:

LOCK TABLE จะได้รับการล็อคระดับโต๊ะโดยรอถ้าจำเป็นเพื่อให้การปลดล็อกที่มีข้อขัดแย้งเกิดขึ้น

ทำไมบางครั้งมันถึงตายแทน

ความหมาย:

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

ข้อผิดพลาดจากบันทึก:

2015-07-28 08:02:58 รายละเอียด BST: กระบวนการ 12380 รอสำหรับ ExclusiveLock ในความสัมพันธ์ 16438 ของฐานข้อมูล 12141; ถูกบล็อกโดยกระบวนการ 12379
        กระบวนการ 12379 รอสำหรับ ExclusiveLock ในความสัมพันธ์ 16438 ของฐานข้อมูล 12141 ถูกบล็อกโดยกระบวนการ 12380
        กระบวนการ 12380: เลือก new_customer (ถอดรหัส ($ 1 :: ข้อความ, 'hex'))
        กระบวนการ 12379: เลือก new_customer (ถอดรหัส ($ 1 :: ข้อความ 'hex')
2015-07-28 08:02:58 คำแนะนำ BST: ดูบันทึกเซิร์ฟเวอร์เพื่อดูรายละเอียดแบบสอบถาม
2015-07-28 08:02:58 BST บริบท: ฟังก์ชัน SQL "new_customer" คำสั่ง 1
2015-07-28 08:02:58 งบ BST: เลือก new_customer (ถอดรหัส ($ 1 :: ข้อความ, 'hex')

ความสัมพันธ์:

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

แก้ไข:

ฉันจัดการเพื่อให้ได้กรณีทดสอบที่ทำซ้ำได้ง่าย สำหรับฉันนี่ดูเหมือนข้อผิดพลาดเนื่องจากสภาพการแข่งขันบางประเภท

สคี:

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

สคริปต์ทุบตีทำงานพร้อมกันในสองช่วงเวลาทุบตี:

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

บันทึกข้อผิดพลาด (โดยทั่วไปจะมี deadlock จำนวนหนึ่งในการโทร 1,000 ครั้ง):

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

แก้ไข 2:

@ypercube แนะนำตัวแปรที่มีlock tableฟังก์ชันภายนอก:

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

ที่น่าสนใจนี้ช่วยลดการหยุดชะงัก


2
ในการทำธุรกรรมเดียวกันก่อนที่จะเข้าสู่ฟังก์ชั่นนั้นจะถูกcustomerใช้ในลักษณะที่จะทำให้ล็อคอ่อนแอ อาจเป็นปัญหาการอัพเกรดล็อค
Daniel Vérité

2
ฉันไม่สามารถอธิบายสิ่งนี้ได้ ดาเนียลอาจมีประเด็น อาจคุ้มค่าที่จะเพิ่มสิ่งนี้บน pgsql-general คุณรู้วิธีการใช้ UPSERT ใน Postgres 9.5 ที่กำลังจะมาถึงหรือไม่? Depesz ดูที่มัน
Erwin Brandstetter

2
ฉันหมายถึงภายในธุรกรรมเดียวกันไม่ใช่เฉพาะเซสชันเดียวกัน (เนื่องจากการล็อกถูกปล่อยที่ tx end) คำตอบโดย @alexk คือสิ่งที่ฉันคิด แต่ถ้า tx เริ่มต้นและจบลงด้วยฟังก์ชั่นที่ไม่สามารถอธิบายการหยุดชะงัก
Daniel Vérité

1
@ เออร์วินคุณอย่างไม่ต้องสงสัยจะสนใจคำตอบที่ฉันได้จากการโพสต์ที่ pgsql-bugs :)
แจ็คบอกว่าลอง topanswers.xyz

2
น่าสนใจมากจริงๆ ทำให้รู้สึกว่ามันใช้งานได้ใน plpgsql ด้วยเช่นกันฉันจำกรณี plpgsql ที่คล้ายกันทำงานได้ตามที่คาดไว้
Erwin Brandstetter

คำตอบ:


10

ฉันโพสต์สิ่งนี้ไปยัง pgsql-bugsและการตอบกลับจาก Tom Lane บ่งบอกว่านี่เป็นปัญหาการเลื่อนระดับการล็อกซึ่งถูกเปิดเผยโดยกลไกของวิธีการประมวลผลฟังก์ชันภาษา SQL โดยพื้นฐานแล้วการล็อคที่สร้างโดยการinsertได้มาก่อนการล็อคแบบเอกสิทธิ์บนโต๊ะ :

ฉันเชื่อว่าปัญหาของเรื่องนี้คือฟังก์ชั่น SQL จะทำการวิเคราะห์คำ (และอาจจะวางแผนด้วยเช่นกันอย่ารู้สึกเหมือนกำลังตรวจสอบโค้ดตอนนี้) สำหรับตัวฟังก์ชันทั้งหมดในครั้งเดียว ซึ่งหมายความว่าเนื่องจากคำสั่ง INSERT คุณได้รับ RowExclusiveLock ในตาราง "ทดสอบ" ระหว่างการแยกวิเคราะห์เนื้อหาของฟังก์ชันก่อนที่คำสั่ง LOCK จะดำเนินการจริง ดังนั้น LOCK จึงเป็นความพยายามในการเพิ่มล็อคและคาดว่าจะมีการหยุดชะงัก

เทคนิคการเข้ารหัสนี้จะปลอดภัยใน plpgsql แต่ไม่ได้อยู่ในฟังก์ชันภาษา SQL

มีการพูดคุยเกี่ยวกับการปรับใช้ฟังก์ชั่นภาษา SQL อีกครั้งเพื่อให้การแยกวิเคราะห์เกิดขึ้นครั้งละหนึ่งคำสั่ง แต่อย่ากลั้นลมหายใจเกี่ยวกับสิ่งที่เกิดขึ้นในทิศทางนั้น ดูเหมือนจะไม่เป็นประเด็นสำคัญสำหรับทุกคน

ขอแสดงความนับถือทอมเลน

สิ่งนี้ยังอธิบายว่าเหตุใดการล็อกตารางนอกฟังก์ชันในบล็อกการตัด plpgsql (ตามที่แนะนำโดย @ypercube) ป้องกันการหยุดชะงัก


3
จุดที่ดี: ypercube ทดสอบการล็อคใน SQL ธรรมดาในการทำธุรกรรมอย่างชัดเจนนอกฟังก์ชั่นซึ่งไม่เหมือนกับบล็อกplpgsql
Erwin Brandstetter

1
ค่อนข้างถูกต้องฉันไม่ดี ฉันคิดว่าฉันสับสนกับสิ่งอื่นที่เราลอง (ซึ่งไม่ได้ป้องกันการหยุดชะงัก)
แจ็คบอกว่าลอง topanswers.xyz

4

สมมติว่าคุณเรียกใช้คำสั่งอื่นก่อนที่จะเรียก new_customer และผู้ที่ได้รับการล็อคที่ขัดแย้งกับEXCLUSIVE(โดยทั่วไปการปรับเปลี่ยนข้อมูลใด ๆ ในตารางลูกค้า) คำอธิบายนั้นง่ายมาก

หนึ่งสามารถสร้างปัญหาด้วยตัวอย่างง่ายๆ (ไม่รวมถึงฟังก์ชั่น):

CREATE TABLE test(id INTEGER);

เซสชันที่ 1:

BEGIN;

INSERT INTO test VALUES(1);

ช่วงที่ 2

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

เซสชันที่ 1

LOCK TABLE test IN EXCLUSIVE MODE;

เมื่อเซสชั่นแรกทำการแทรกมันจะได้รับROW EXCLUSIVEล็อคบนโต๊ะ ในขณะเดียวกันเซสชั่น 2 พยายามROW EXCLUSIVEล็อคและพยายามEXCLUSIVEล็อค จุดที่จะมีการรอให้เซสชั่นที่ 1 ตั้งแต่ล็อคความขัดแย้งกับEXCLUSIVE ROW EXCLUSIVEในที่สุดเซสชั่นที่ 1 กระโดดฉลามและพยายามที่จะEXCLUSIVEล็อค แต่เนื่องจากล็อคได้มาตามลำดับมันจะรอหลังจากเซสชั่นที่ 2 ในทางกลับกันรอคนที่ 1 ทำให้เกิดการหยุดชะงัก:

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

ทางออกของปัญหานี้คือการได้รับล็อคเร็วที่สุดเท่าที่จะทำได้โดยปกติจะเป็นสิ่งแรกในการทำธุรกรรม ในทางตรงกันข้ามปริมาณงานของ PostgreSQL นั้นต้องการเพียงการล็อกในบางกรณีที่หายากมากดังนั้นฉันจึงขอแนะนำให้ทบทวนวิธีที่คุณใช้งานจริง (ดูที่บทความนี้http://www.depesz.com/2012/06/10 / Why-is-upert-so-complex / )


2
ทั้งหมดนี้น่าสนใจ แต่ข้อความในบันทึกของ db จะอ่านดังนี้: Process 28514 : select new_customer(decode($1::text, 'hex')); Process 28084 : BEGIN; INSERT INTO test VALUES(1); select new_customer(decode($1::text, 'hex'))ในขณะที่แจ็คเพิ่งได้รับ: Process 12380: select new_customer(decode($1::text, 'hex')) Process 12379: select new_customer(decode($1::text, 'hex'))- ระบุว่าการเรียกใช้ฟังก์ชั่นเป็นคำสั่งแรกในการทำธุรกรรมทั้งสองรายการ
Erwin Brandstetter

ขอบคุณและฉันเห็นด้วยกับสิ่งที่คุณพูด แต่นี่ไม่ได้เป็นสาเหตุในกรณีนี้ ชัดเจนมากขึ้นในกรณีทดสอบขั้นต่ำที่ฉันได้เพิ่มไว้ในคำถาม (ซึ่งคุณสามารถลองด้วยตัวเองได้)
แจ็คบอกว่าลอง topanswers.xyz

2
จริงๆแล้วมันกลับกลายเป็นของคุณถูกต้องเกี่ยวกับการเพิ่มล็อค - แม้ว่ากลไกเป็นที่ลึกซึ้ง
แจ็คบอกว่าลอง topanswers.xyz
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.