ปัญหาการล็อคด้วย DELETE / INSERT พร้อมกันใน PostgreSQL


35

มันค่อนข้างง่าย แต่ฉันก็งงกับสิ่งที่ PG ทำ (v9.0) เราเริ่มต้นด้วยตารางง่ายๆ

CREATE TABLE test (id INT PRIMARY KEY);

และไม่กี่แถว:

INSERT INTO TEST VALUES (1);
INSERT INTO TEST VALUES (2);

การใช้เครื่องมือสืบค้น JDBC ที่ฉันโปรดปราน (ExecuteQuery) ฉันเชื่อมต่อหน้าต่างสองเซสชันเข้ากับฐานข้อมูลที่ตารางนี้ใช้งานอยู่ ทั้งคู่เป็นธุรกรรม (เช่น auto-commit = false) ลองเรียกพวกมันว่า S1 และ S2

รหัสเดียวกันสำหรับแต่ละ:

1:DELETE FROM test WHERE id=1;
2:INSERT INTO test VALUES (1);
3:COMMIT;

ทีนี้ลองทำแบบนี้ในแบบสโลว์โมชั่นโดยเรียกใช้ทีละตัวในหน้าต่าง

S1-1 runs (1 row deleted)
S2-1 runs (but is blocked since S1 has a write lock)
S1-2 runs (1 row inserted)
S1-3 runs, releasing the write lock
S2-1 runs, now that it can get the lock. But reports 0 rows deleted. HUH???
S2-2 runs, reports a unique key constraint violation

ตอนนี้ทำงานได้ดีใน SQLServer เมื่อ S2 ทำการลบมันจะรายงานการลบ 1 แถว จากนั้นเม็ดมีดของ S2 ก็ใช้ได้ดี

ฉันสงสัยว่า PostgreSQL กำลังล็อกดัชนีในตารางที่มีแถวนั้นอยู่ในขณะที่ SQLServer ล็อคค่าคีย์จริง

ฉันถูกไหม? สามารถใช้งานได้หรือไม่

คำตอบ:


39

Mat และ Erwin นั้นถูกต้องแล้วและฉันแค่เพิ่มคำตอบเพื่อขยายสิ่งที่พวกเขาพูดในแบบที่ไม่เหมาะสมกับความคิดเห็น เนื่องจากคำตอบของพวกเขาดูเหมือนจะไม่เป็นที่พอใจของทุกคนและมีข้อเสนอแนะที่นักพัฒนา PostgreSQL ควรได้รับการพิจารณาและฉันเป็นหนึ่งเดียวฉันจะทำอย่างละเอียด

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

นี่คือสิ่งที่เกิดขึ้นภายใน PostgreSQL โดยละเอียด:

S1-1 ทำงาน (ลบ 1 แถว)

แถวเก่าถูกทิ้งไว้เนื่องจาก S1 อาจยังย้อนกลับ แต่ตอนนี้ S1 มีการล็อกแถวเพื่อให้เซสชันอื่น ๆ ที่พยายามแก้ไขแถวจะรอเพื่อดูว่า S1 กระทำหรือย้อนกลับ ใด ๆอ่านของตารางยังสามารถดูแถวเก่าจนกว่าพวกเขาพยายามที่จะล็อคด้วยหรือSELECT FOR UPDATESELECT FOR SHARE

S2-1 ทำงาน (แต่ถูกบล็อกเนื่องจาก S1 มีล็อคการเขียน)

ตอนนี้ S2 ต้องรอดูผลลัพธ์ของ S1 หาก S1 ต้องย้อนกลับแทนที่จะคอมมิชชัน S2 จะลบแถวนั้น โปรดทราบว่าหาก S1 แทรกเวอร์ชันใหม่ก่อนจะย้อนกลับเวอร์ชันใหม่จะไม่เคยอยู่ในมุมมองของธุรกรรมอื่น ๆ และจะไม่ลบเวอร์ชันเก่าออกจากมุมมองของธุรกรรมอื่น ๆ

S1-2 ทำงาน (แทรก 1 แถว)

แถวนี้เป็นอิสระจากแถวเก่า หากมีการอัปเดตของแถวที่มี id = 1 เวอร์ชันเก่าและใหม่จะเกี่ยวข้องกันและ S2 สามารถลบเวอร์ชันที่อัปเดตของแถวได้เมื่อไม่ถูกบล็อก การที่แถวใหม่เกิดขึ้นจะมีค่าเหมือนกันกับบางแถวที่มีอยู่ในอดีตไม่ได้ทำให้เหมือนกันกับเวอร์ชันที่อัปเดตของแถวนั้น

S1-3 ทำงานปล่อยการล็อกการเขียน

ดังนั้นการเปลี่ยนแปลงของ S1 จึงยังคงอยู่ หายไปหนึ่งแถว เพิ่มหนึ่งแถวแล้ว

S2-1 ทำงานทันทีที่สามารถล็อคได้ แต่รายงานถูกลบ 0 แถว ฮะ???

สิ่งที่เกิดขึ้นภายในคือมีตัวชี้จากหนึ่งเวอร์ชันของแถวไปเป็นเวอร์ชันถัดไปของแถวเดียวกันนั้นหากมีการอัพเดต หากลบแถวจะไม่มีรุ่นถัดไป เมื่อทREAD COMMITTEDรานแซคชันตื่นขึ้นมาจากบล็อกบนข้อขัดแย้งในการเขียนรายการนั้นจะติดตามสายโซ่นั้นจนจบ หากแถวยังไม่ถูกลบและหากยังคงเป็นไปตามเกณฑ์การเลือกของแบบสอบถามจะถูกประมวลผล แถวนี้ถูกลบไปแล้วดังนั้นการค้นหาของ S2 จะดำเนินต่อไป

S2 อาจหรือไม่ได้รับแถวใหม่ในระหว่างการสแกนของตาราง หากเป็นเช่นนั้นจะเห็นว่าแถวใหม่ถูกสร้างขึ้นหลังจากDELETEคำสั่งของ S2 เริ่มต้นขึ้นและไม่ได้เป็นส่วนหนึ่งของชุดแถวที่มองเห็นได้

หาก PostgreSQL ต้องเริ่มต้นงบ DELETE ทั้งหมดของ S2 ใหม่ตั้งแต่เริ่มต้นด้วยสแนปชอตใหม่มันจะทำงานเหมือน SQL Server ชุมชน PostgreSQL ไม่ได้เลือกที่จะทำเช่นนั้นด้วยเหตุผลด้านประสิทธิภาพ ในกรณีง่าย ๆ นี้คุณจะไม่สังเกตเห็นความแตกต่างของประสิทธิภาพ แต่ถ้าคุณมีสิบล้านแถวในDELETEเมื่อคุณถูกบล็อกคุณจะต้องแน่นอน มีการแลกเปลี่ยนที่นี่ที่ PostgreSQL เลือกประสิทธิภาพเนื่องจากรุ่นที่เร็วกว่านั้นยังคงเป็นไปตามข้อกำหนดของมาตรฐาน

S2-2 ทำงานรายงานการละเมิดข้อ จำกัด คีย์ที่ไม่ซ้ำกัน

แน่นอนแถวนั้นมีอยู่แล้ว นี่เป็นส่วนที่น่าแปลกใจที่สุดของภาพ

ในขณะที่มีพฤติกรรมที่น่าแปลกใจบางอย่างที่นี่ทุกอย่างสอดคล้องกับมาตรฐาน SQL และภายในขอบเขตของ "การใช้งานเฉพาะ" ตามมาตรฐาน แน่นอนว่าอาจเป็นเรื่องที่น่าแปลกใจหากคุณสมมติว่าพฤติกรรมของการนำไปใช้งานอื่น ๆ นั้นมีอยู่ในการนำไปใช้ทั้งหมด แต่ PostgreSQL พยายามอย่างหนักเพื่อหลีกเลี่ยงความล้มเหลวของการทำให้เป็นอนุกรมในREAD COMMITTEDระดับแยกและทำให้พฤติกรรมบางอย่างที่แตกต่างจากผลิตภัณฑ์อื่น ๆ

ตอนนี้ส่วนตัวผมไม่ได้เป็นแฟนตัวยงของREAD COMMITTEDระดับแยกธุรกรรมในใด ๆการดำเนินงานของผลิตภัณฑ์ พวกเขาทั้งหมดอนุญาตให้เงื่อนไขการแข่งขันเพื่อสร้างพฤติกรรมที่น่าแปลกใจจากมุมมองของการทำธุรกรรม เมื่อมีคนคุ้นเคยกับพฤติกรรมแปลก ๆ ที่ได้รับอนุญาตจากผลิตภัณฑ์หนึ่งพวกเขามักจะพิจารณาว่า "ปกติ" และการแลกเปลี่ยนที่ถูกเลือกโดยผลิตภัณฑ์อื่นที่แปลก แต่ทุกผลิตภัณฑ์เพื่อให้มีการจัดเรียงของบางอย่างการปิดสำหรับโหมดใด ๆ SERIALIZABLEที่ไม่ใช้งานจริงเป็น จุดที่นักพัฒนา PostgreSQL เลือกที่จะลากเส้นREAD COMMITTEDคือการลดการบล็อกให้น้อยที่สุด (อ่านไม่บล็อกการเขียนและการเขียนไม่บล็อกอ่าน) และเพื่อลดโอกาสของความล้มเหลวในการทำให้เป็นอนุกรม

มาตรฐานกำหนดให้การSERIALIZABLEทำธุรกรรมเป็นค่าเริ่มต้น แต่ผลิตภัณฑ์ส่วนใหญ่ไม่ทำเช่นนั้นเพราะทำให้ประสิทธิภาพการทำงานเหนือระดับการแยกธุรกรรมหละหลวมมากขึ้น ผลิตภัณฑ์บางอย่างไม่ได้จัดทำธุรกรรมที่สามารถทำให้เป็นอนุกรมได้อย่างแท้จริงเมื่อSERIALIZABLEเลือก - โดยเฉพาะอย่างยิ่ง Oracle และเวอร์ชันของ PostgreSQL ก่อนหน้า 9.1 แต่การใช้SERIALIZABLEธุรกรรมอย่างแท้จริงเป็นวิธีเดียวที่จะหลีกเลี่ยงผลกระทบที่น่าประหลาดใจจากสภาพการแข่งขันและการSERIALIZABLEทำธุรกรรมจะต้องปิดกั้นเพื่อหลีกเลี่ยงสภาพการแข่งขันหรือย้อนกลับธุรกรรมบางอย่างเพื่อหลีกเลี่ยงสภาพการแข่งขันที่กำลังพัฒนา การใช้งานSERIALIZABLEธุรกรรมที่พบบ่อยที่สุดคือ Strict Two-Phase Locking (S2PL) ซึ่งมีทั้งการบล็อกและความล้มเหลวของการทำซีเรียลไลซ์เซชั่น (ในรูปแบบของ deadlocks)

การเปิดเผยอย่างเต็มรูปแบบ: ฉันทำงานกับ Dan Ports ของ MIT เพื่อเพิ่มธุรกรรมที่ทำให้เป็นอนุกรมได้อย่างแท้จริงใน PostgreSQL เวอร์ชัน 9.1 โดยใช้เทคนิคใหม่ที่เรียกว่า Serializable Snapshot Isolation


ฉันสงสัยว่าวิธีที่ถูก (วิเศษจริง ๆ ) ราคาถูกจริงๆหรือไม่ที่จะทำให้งานนี้คือการลบสองรายการตามด้วย INSERT ในการทดสอบที่ จำกัด (2 เธรด) ของฉันมันใช้งานได้ดี แต่ต้องทดสอบเพิ่มเติมเพื่อดูว่าจะมีเธรดจำนวนมากหรือไม่
DaveyBob

ตราบใดที่คุณใช้READ COMMITTEDธุรกรรมคุณมีสภาวะการแข่งขัน: จะเกิดอะไรขึ้นถ้าธุรกรรมอื่นแทรกแถวใหม่หลังจากที่DELETEเริ่มต้นครั้งแรกและก่อนที่จะDELETEเริ่มต้นที่สอง ด้วยธุรกรรมที่เข้มงวดน้อยกว่าSERIALIZABLEสองวิธีหลักในการปิดเงื่อนไขการแข่งขันคือการส่งเสริมความขัดแย้ง (แต่นั่นไม่ได้ช่วยอะไรเมื่อแถวถูกลบ) และเป็นรูปธรรมของความขัดแย้ง คุณสามารถทำให้ข้อขัดแย้งเกิดขึ้นได้โดยมีตาราง "id" ที่อัปเดตสำหรับทุกแถวที่ถูกลบหรือโดยการล็อคตารางอย่างชัดเจน หรือลองใช้ใหม่โดยมีข้อผิดพลาด
kgrittn

ลองใหม่อีกครั้ง ขอบคุณมากสำหรับข้อมูลเชิงลึกที่มีค่า!
DaveyBob

21

ฉันเชื่อว่าสิ่งนี้เกิดจากการออกแบบตามคำอธิบายของระดับการแยกแบบอ่านอย่างเดียวสำหรับ PostgreSQL 9.2:

ปรับปรุงลบเลือกสำหรับการปรับปรุงและเลือกสำหรับคำสั่งหุ้นประพฤติเช่นเดียวกับการเลือกในแง่ของการค้นหาแถวเป้าหมาย: พวกเขาเท่านั้นที่จะพบแถวเป้าหมายที่มีความมุ่งมั่นเป็นเวลาเริ่มต้นคำสั่ง 1 อย่างไรก็ตามแถวเป้าหมายดังกล่าวอาจได้รับการอัปเดต (หรือลบหรือล็อค) โดยธุรกรรมอื่นพร้อมกันในเวลาที่พบ ในกรณีนี้ตัวอัปเดตที่ต้องการจะรอให้ธุรกรรมการอัปเดตครั้งแรกส่งหรือย้อนกลับ (หากยังอยู่ระหว่างดำเนินการ) หากตัวอัปเดตครั้งแรกย้อนกลับไปเอฟเฟกต์ของมันจะถูกลบล้างและตัวอัปเดตตัวที่สองสามารถดำเนินการอัปเดตแถวที่พบในตอนแรกได้ หากตัวอัปเดตตัวแรกยอมรับการตัวอัปเดตตัวที่สองจะไม่สนใจแถวนั้นหากตัวอัปเดตตัวแรกลบไป2มิฉะนั้นจะพยายามใช้การดำเนินการกับแถวที่อัปเดตแล้ว

แถวที่คุณใส่ในS1ไม่ได้อยู่ แต่เมื่อS2's DELETEเริ่มต้น ดังนั้นจะไม่สามารถเห็นได้โดยการลบS2ตาม ( 1 ) ด้านบน สิ่งที่S1ถูกลบจะถูกละเว้นโดยS2's DELETEตาม ( 2 )

ดังนั้นในS2การลบจะไม่ทำอะไรเลย เมื่อมีการแทรกเกิดขึ้นแทรกจะเห็นการS1แทรกของ:

เพราะโหมดอ่านมุ่งมั่นที่จะเริ่มต้นแต่ละคำสั่งกับภาพใหม่ที่มีการทำธุรกรรมทั้งหมดมุ่งมั่นถึงทันทีที่คำสั่งตามมาในรายการเดียวกันจะเห็นผลกระทบของการทำธุรกรรมที่เกิดขึ้นพร้อมกันมุ่งมั่นในกรณีใดประเด็นที่กล่าวถึงข้างต้นคือคำสั่งเดียวหรือไม่ที่เห็นมุมมองที่สอดคล้องกันอย่างสมบูรณ์ของฐานข้อมูล

ดังนั้นการพยายามแทรกโดยS2ล้มเหลวด้วยการละเมิดข้อ จำกัด

การอ่านเอกสารต่อไปว่าการใช้การอ่านซ้ำหรือแม้กระทั่งการทำให้เป็นอนุกรมจะไม่สามารถแก้ปัญหาของคุณได้อย่างสมบูรณ์ - เซสชั่นที่สองจะล้มเหลวด้วยข้อผิดพลาดในการลบซีเรียลไลเซชัน

สิ่งนี้จะช่วยให้คุณลองทำธุรกรรมอีกครั้ง


ขอบคุณจ้า ในขณะที่ดูเหมือนจะเป็นสิ่งที่เกิดขึ้นดูเหมือนจะมีข้อบกพร่องในตรรกะที่ ดูเหมือนว่าสำหรับฉันในระดับ iso READ_COMMITTED แล้วคำสั่งทั้งสองนี้จะต้องประสบความสำเร็จภายใน tx: ลบจากการทดสอบ WHERE ID = 1 INSERT INTO VALUES (1) ฉันหมายความว่าถ้าฉันลบแถวแล้วแทรกแถว จากนั้นแทรกที่ควรจะประสบความสำเร็จ SQLServer ได้รับสิทธินี้ ตามที่เป็นฉันมีเวลายากมากที่จัดการกับสถานการณ์นี้ในผลิตภัณฑ์ที่ต้องทำงานกับฐานข้อมูลทั้งสอง
DaveyBob

11

ผมเห็นด้วยกับ@ คำตอบที่ดีจ้าของ ฉันเขียนเพียงคำตอบอื่นเพราะมันจะไม่เหมาะสมกับความคิดเห็น

ในการตอบความคิดเห็นของคุณ: DELETEin S2 นั้นเชื่อมต่อกับเวอร์ชันแถวใดแถวหนึ่งแล้ว เนื่องจากสิ่งนี้ถูกฆ่าโดย S1 ในเวลาเดียวกัน S2 จึงถือว่าตัวเองประสบความสำเร็จ แม้ว่าจะไม่ชัดเจนจากภาพรวมอย่างรวดเร็ว แต่เหตุการณ์ต่างๆก็เป็นเช่นนี้:

   S1 DELETE สำเร็จแล้ว  
S2 DELETE (สำเร็จโดยพร็อกซี - ลบจาก S1)  
   S1 แทรกค่าที่ถูกลบอีกครั้งในระหว่างนี้  
S2 INSERT ล้มเหลวด้วยการละเมิดข้อ จำกัด คีย์ที่ไม่ซ้ำกัน

มันคือทั้งหมดที่ออกแบบ คุณต้องใช้SERIALIZABLEธุรกรรมตามความต้องการของคุณจริงๆและตรวจสอบให้แน่ใจว่าคุณลองอีกครั้งเนื่องจากความล้มเหลวของการทำให้เป็นอนุกรม


1

ใช้คีย์หลักDEFERRABLEแล้วลองอีกครั้ง


ขอบคุณสำหรับเคล็ดลับ แต่การใช้ DEFERRABLE ไม่ได้สร้างความแตกต่างเลย เอกสารอ่านอย่างที่ควรจะมี แต่ไม่มี
DaveyBob

-2

เรายังประสบปัญหานี้ วิธีการแก้ปัญหาของเราคือการเพิ่ม ก่อนselect ... for update delete from ... whereต้องแยกระดับการอ่านออกจากกัน

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