07 ก.ย. 2568·อ่าน 2 นาที

การใช้ Advisory Locks ของ PostgreSQL เพื่อเวิร์กโฟลว์ที่ปลอดภัยเมื่อทำงานพร้อมกัน

เรียนรู้การใช้ advisory locks ของ PostgreSQL เพื่อหยุดการประมวลผลซ้ำในระบบอนุมัติ การเรียกเก็บเงิน และตารางงาน พร้อมรูปแบบปฏิบัติ ตัวอย่าง SQL และการตรวจสอบง่ายๆ

การใช้ Advisory Locks ของ PostgreSQL เพื่อเวิร์กโฟลว์ที่ปลอดภัยเมื่อทำงานพร้อมกัน

ปัญหาจริง: สองกระบวนการทำงานเดียวกันซ้ำ

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

โดยทั่วไปมันเกิดขึ้นเมื่อเวลาแน่นและมีมากกว่าหนึ่งสิ่งที่สามารถทำงานได้พร้อมกัน:

สอง worker ดึงงานเดียวกันในเวลาเดียวกัน การ retry เกิดขึ้นเพราะการเรียกเครือข่ายช้า แต่ครั้งแรกยังรันอยู่ ผู้ใช้ดับเบิลคลิก Approve เพราะ UI ค้างหนึ่งวินาที สอง scheduler ทับกันหลัง deploy หรือความคลาดเคลื่อนของนาฬิกา แม้การแตะเพียงครั้งเดียวอาจกลายเป็นสองคำขอถ้าแอปมือถือส่งซ้ำหลัง timeout

ส่วนที่เจ็บปวดคือแต่ละฝ่ายทำหน้าที่ได้ “สมเหตุสมผล” ด้วยตัวมันเอง บั๊กคือช่องว่างระหว่างพวกเขา: ไม่มีฝ่ายใดรู้ว่าฝ่ายอื่นกำลังประมวลผลระเบียนเดียวกันอยู่

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

PostgreSQL advisory locks ช่วยได้ มันให้วิธีน้ำหนักเบาในการบอกว่า “ฉันกำลังทำงานของรายการ X” โดยใช้ฐานข้อมูลที่คุณเชื่อถืออยู่แล้วสำหรับความสอดคล้อง

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

Advisory locks ของ PostgreSQL คืออะไร (และไม่ใช่อะไร)

PostgreSQL advisory locks เป็นวิธีให้แน่ใจว่า worker คนเดียวเท่านั้นทำงานชิ้นหนึ่งในคราวเดียว คุณเลือกคีย์ล็อก (เช่น "invoice 123"), ขอให้ฐานข้อมูลล็อกคีย์นั้น, ทำงาน แล้วปล่อยล็อก

คำว่า “advisory” สำคัญ Postgres ไม่รู้ความหมายของคีย์คุณ และมันจะไม่ปกป้องอะไรโดยอัตโนมัติ มันแค่ติดตามข้อเท็จจริงเดียว: คีย์นี้ถูกล็อกหรือไม่ถูกล็อก โค้ดของคุณต้องตกลงกันในฟอร์แมตคีย์และต้องขอล็อกก่อนรันส่วนที่เสี่ยง

การเปรียบเทียบกับ row locks ก็ช่วยได้ Row locks (เช่น SELECT ... FOR UPDATE) ปกป้องแถวจริงในตาราง เหมาะเมื่อการทำงานแม็พกับแถวเดียวได้ชัดเจน Advisory locks ปกป้องคีย์ที่คุณเลือก ซึ่งมีประโยชน์เมื่อเวิร์กโฟลว์สัมผัสหลายตาราง เรียกบริการภายนอก หรือเริ่มก่อนที่แถวจะมีอยู่

Advisory locks มีประโยชน์เมื่อคุณต้องการ:

  • การกระทำทีละหนึ่งต่อเอนทิตี (เช่น อนุมัติหนึ่งครั้งต่อคำขอ, เรียกเก็บเงินหนึ่งครั้งต่อใบแจ้งหนี้)
  • ประสานงานข้ามหลายเซิร์ฟเวอร์แอปโดยไม่ต้องเพิ่มบริการล็อกแยกต่างหาก
  • ป้องกันรอบเวิร์กโฟลว์ที่ใหญ่กว่าการอัปเดตแถวเดียว

มันไม่ใช่ตัวแทนของเครื่องมือความปลอดภัยอื่นๆ มันไม่ทำให้การดำเนินการเป็น idempotent, ไม่บังคับใช้กฎธุรกิจ, และจะไม่หยุดการทำซ้ำถ้าเส้นทางโค้ดลืมขอล็อก

มักเรียกว่า “น้ำหนักเบา” เพราะคุณใช้ได้โดยไม่ต้องเปลี่ยนสคีมา หรือเพิ่มโครงสร้างพื้นฐาน ในหลายกรณี คุณแก้ปัญหาการประมวลผลซ้ำได้ด้วยการเพิ่มการเรียกล็อกรอบส่วนสำคัญเพียงครั้งเดียวโดยไม่ต้องเปลี่ยนโครงสร้างทั้งหมด

ประเภทล็อกที่คุณจะใช้จริง

เมื่อคนพูดถึง “PostgreSQL advisory locks” พวกเขามักหมายถึงชุดฟังก์ชันเล็กๆ การเลือกแบบที่ถูกต้องจะเปลี่ยนพฤติกรรมเมื่อเกิดข้อผิดพลาด, timeout, และการ retry

ล็อกแบบ session เทียบกับแบบ transaction

ล็อกระดับ session (pg_advisory_lock) จะอยู่นานเท่าการเชื่อมต่อฐานข้อมูล นั่นสะดวกสำหรับ worker ที่รันนาน แต่ก็หมายความว่าล็อกอาจคงค้างถ้าแอป crash ในแบบที่ทิ้งการเชื่อมต่อจากพูลไว้

ล็อกระดับ transaction (pg_advisory_xact_lock) ผูกกับ transaction ปัจจุบัน เมื่อ commit หรือ rollback PostgreSQL จะปล่อยล็อกให้โดยอัตโนมัติ สำหรับเวิร์กโฟลว์แบบ request-response ส่วนใหญ่ (การอนุมัติ การคลิกชำระเงิน การกระทำของแอดมิน) นี่เป็นค่าเริ่มต้นที่ปลอดภัยกว่าเพราะยากที่จะลืมปล่อยล็อก

แบบบล็อกหรือ try-lock

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

การเรียกแบบ try-lock คืนค่าทันที:

  • pg_try_advisory_lock (ระดับ session)
  • pg_try_advisory_xact_lock (ระดับ transaction)

Try-lock มักเหมาะกว่าในงานที่มี UI ถ้าล็อกถูกถืออยู่ คุณสามารถคืนข้อความชัดเจนเช่น “กำลังประมวลผลอยู่” แล้วขอให้ผู้ใช้ลองอีกครั้ง

แบบแชร์หรือเอ็กซ์คลูซีฟ

ล็อกเอ็กซ์คลูซีฟคือ “ทีละหนึ่ง” ส่วนล็อกแชร์อนุญาตหลาย holder แต่บล็อกล็อกเอ็กซ์คลูซีฟ ปัญหาการประมวลผลซ้ำส่วนใหญ่ใช้ล็อกเอ็กซ์คลูซีฟ ล็อกแชร์มีประโยชน์เมื่อต้องการให้ผู้อ่านหลายคนทำงานพร้อมกัน แต่มี writer หายากที่ต้องรันเดี่ยว

การปล่อยล็อก

การปล่อยขึ้นกับชนิด:

  • ล็อกระดับ session: ปล่อยเมื่อ disconnect หรือเรียก pg_advisory_unlock อย่างชัดเจน
  • ล็อกระดับ transaction: ปล่อยโดยอัตโนมัติเมื่อ transaction สิ้นสุด

การเลือกคีย์ล็อกที่ถูกต้อง

Advisory lock จะทำงานก็ต่อเมื่อทุก worker พยายามล็อกคีย์เดียวกันเป๊ะสำหรับงานเดียวกัน หากเส้นทางโค้ดหนึ่งล็อก invoice 123 และอีกเส้นทางล็อก customer 45 คุณยังคงเจอการทำซ้ำได้

เริ่มจากการตั้งชื่อ “สิ่ง” ที่คุณต้องการปกป้อง ให้เป็นรูปธรรม: ใบแจ้งหนี้หนึ่งรายการ, คำขออนุมัติหนึ่งรายการ, การรันตารางงานครั้งหนึ่ง หรือรอบการเรียกเก็บเงินรายเดือนของลูกค้าคนหนึ่ง การเลือกนี้จะกำหนดว่าคุณอนุญาตความขนานกันได้มากน้อยเพียงใด

เลือกขอบเขตที่ตรงกับความเสี่ยง

ทีมส่วนใหญ่ลงเอยด้วยหนึ่งในนี้:

  • ต่อระเบียน: ปลอดภัยที่สุดสำหรับการอนุมัติและใบแจ้งหนี้ (ล็อกโดย invoice_id หรือ request_id)
  • ต่อลูกค้า/บัญชี: มีประโยชน์เมื่อการกระทำต้องเรียงลำดับต่อหนึ่งลูกค้า (การเรียกเก็บเงิน การเปลี่ยนแปลงเครดิต)
  • ต่อขั้นตอนของเวิร์กโฟลว์: เมื่อขั้นตอนต่างกันสามารถรันคู่ขนานได้ แต่แต่ละขั้นตอนต้องทีละหนึ่ง

มองขอบเขตเป็นการตัดสินใจทางผลิตภัณฑ์ ไม่ใช่รายละเอียดฐานข้อมูล “ต่อระเบียน” ป้องกันการดับเบิลคลิกจากการเรียกเก็บสองครั้ง “ต่อลูกค้า” ป้องกันสองงานแบ็กกราวด์สร้างใบแจ้งหนี้ทับซ้อน

เลือกกลยุทธ์คีย์ที่มั่นคง

โดยทั่วไปมีสองตัวเลือก: สองจำนวนเต็ม 32 บิต (มักใช้เป็น namespace + id) หรือจำนวนเต็ม 64 บิต (bigint) ที่สร้างจากการแฮชสตริง ID

คีย์สองจำนวนเต็มมาตรฐานง่าย: เลือกหมายเลข namespace ตายตัวต่อเวิร์กโฟลว์ (เช่น approvals กับ billing) แล้วใช้ record ID เป็นค่าที่สอง

การแฮชมีประโยชน์เมื่อ identifier เป็น UUID แต่คุณต้องยอมรับความเสี่ยงการชนกันเล็กน้อยและต้องใช้สม่ำเสมอทุกที่

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

ทีละขั้นตอน: รูปแบบปลอดภัยสำหรับการประมวลผลทีละหนึ่ง

ทำให้การลองชำระเงินปลอดภัย
เพิ่มการประมวลผลใบแจ้งหนี้ทีละรายการก่อนเรียกใช้ Stripe หรือผู้ให้บริการชำระเงินอื่นๆ
เริ่มสร้าง

เวิร์กโฟลว์ advisory-lock ที่ดีนั้นเรียบง่าย: lock, verify, act, record, commit. ล็อกไม่ใช่กฎธุรกิจด้วยตัวเอง แต่มันเป็นราวกันตกที่ทำให้กฎนั้นเชื่อถือได้เมื่อสอง worker เข้าถึงระเบียนเดียวกันพร้อมกัน

รูปแบบปฏิบัติ:

  1. เปิด transaction เมื่อผลลัพธ์ต้องเป็น atomic
  2. ขอล็อกสำหรับหน่วยงานเฉพาะของงาน เลือกล็อกที่ผูกกับ transaction (pg_advisory_xact_lock) เพื่อให้ปล่อยอัตโนมัติ
  3. ตรวจสถานะซ้ำในฐานข้อมูล อย่าสมมติว่าคุณเป็นคนแรก ยืนยันว่ายังสามารถทำได้
  4. ทำงานและเขียนมาร์กเกอร์ “เสร็จ” ที่ทนทานในฐานข้อมูล (อัปเดตสถานะ, บันทึกบัญชี, แถว audit)
  5. Commit แล้วปล่อยล็อก หากใช้ล็อกระดับ session ให้ unlock ก่อนคืนการเชื่อมต่อสู่พูล

ตัวอย่าง: สองเซิร์ฟเวอร์รับคำสั่ง "อนุมัติใบแจ้งหนี้ #123" ภายในวินาทีเดียว ทั้งคู่เริ่ม แต่มีเพียงคนเดียวได้ล็อกสำหรับ 123 ผู้ชนะตรวจว่า invoice #123 ยังเป็น pending อยู่หรือไม่ เปลี่ยนเป็น approved เขียนบันทึก audit/payment แล้ว commit เซิร์ฟเวอร์ที่สองไม่ว่าจะ fail-fast (try-lock) หรือรอแล้วได้ล็อกภายหลัง ก็จะเห็นว่าสถานะเป็น approved แล้วและออกโดยไม่สร้างรายการซ้ำ ไม่ว่ากรณีใด คุณก็หลีกเลี่ยงการประมวลผลซ้ำและยังรักษา UI ให้ตอบสนอง

สำหรับการดีบัก ให้บันทึกพอให้ติดตามแต่ละความพยายาม: request id, approval id และคีย์ล็อกที่คำนวณ, actor id, ผลลัพธ์ (lock_busy, already_approved, approved_ok), และเวลา

ที่ที่ advisory locks เหมาะ: การอนุมัติ, การเรียกเก็บเงิน, ตารางงาน

Advisory locks เหมาะที่สุดเมื่อกฎชัดเจน: สำหรับสิ่งหนึ่งสิ่งใด ให้มีเพียงกระบวนการเดียวเท่านั้นที่สามารถทำงาน “ชนะ” ในครั้งนั้น คุณยังคงใช้ฐานข้อมูลและโค้ดแอปเดิม แต่เพิ่มประตูเล็กๆ ที่ทำให้ race condition เกิดขึ้นได้ยากขึ้นมาก

การอนุมัติ

การอนุมัติเป็นกับดักการแข่งขันประเภทคลาสสิก ผู้ตรวจสองคน (หรือคนเดียวกันที่ดับเบิลคลิก) อาจกด Approve ภายในมิลลิวินาทีเดียว ด้วยล็อกที่ใช้ request id เท่านั้น transaction เดียวเท่านั้นที่จะทำการเปลี่ยนสถานะ ส่วนที่เหลือก็จะเรียนรู้ผลลัพธ์อย่างรวดเร็วและแสดงข้อความชัดเจนเช่น “อนุมัติแล้ว” หรือ “ปฏิเสธแล้ว”

เรื่องนี้พบบ่อยในพอร์ทัลลูกค้าและแผงแอดมินที่หลายคนมองคิวเดียวกัน

การเรียกเก็บเงิน

การเรียกเก็บเงินมักต้องการกฎที่เข้มงวดกว่า: พยายามชำระเงินหนึ่งครั้งต่อใบแจ้งหนี้ แม้จะมีการ retry เกิดขึ้นก็ตาม Timeout ของเครือข่ายอาจทำให้ผู้ใช้กด Pay อีกครั้ง หรือ retry แบ็กกราวด์อาจรันในขณะที่ครั้งแรกยังอยู่ระหว่างการเรียก

ล็อกที่ใช้ invoice ID จะรับประกันว่าหนทางเดียวเท่านั้นที่จะคุยกับผู้ให้บริการชำระเงินได้ในคราวเดียว ความพยายามที่สองสามารถคืนว่า “กำลังประมวลผลการชำระเงิน” หรืออ่านสถานะการชำระล่าสุด ซึ่งป้องกันการทำงานซ้ำและลดความเสี่ยงการเรียกเก็บซ้ำ

ตารางงานและ worker แบ็กกราวด์

ในสภาพแวดล้อมหลายอินสแตนซ์ scheduler อาจเผลอรันหน้าต่างเดียวกันพร้อมกันได้ ล็อกที่ใช้ชื่องานบวกหน้าต่างเวลา (เช่น "daily-settlement:2026-01-29") จะรับประกันว่าอินสแตนซ์เดียวเท่านั้นที่รันมัน

แนวทางเดียวกันใช้ได้กับ worker ที่ดึงไอเท็มจากตาราง: ล็อกตาม item ID เพื่อให้มี worker เดียวเท่านั้นที่ประมวลผลมัน

คีย์ที่คนมักล็อกคือ approval request id เดียว, invoice id เดียว, ชื่องานบวกหน้าต่างเวลา, customer ID สำหรับ "export ทีละคน", หรือ unique idempotency key สำหรับ retry

ตัวอย่างสมจริง: หยุดการอนุมัติซ้ำในพอร์ทัล

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

ลองนึกภาพคำขออนุมัติในพอร์ทัล: ใบสั่งซื้อรออยู่ และผู้จัดการสองคนกด Approve ภายในวินาทีเดียว หากไม่มีการป้องกัน ทั้งสองคำขออาจอ่านว่า "pending" และทั้งสองเขียนเป็น "approved" ทำให้เกิดแถว audit ซ้ำ การแจ้งเตือนซ้ำ หรือการทำงาน downstream ถูกทริกเกอร์สองครั้ง

PostgreSQL advisory locks ให้วิธีตรงไปตรงมาทำให้การกระทำนี้เป็นทีละหนึ่งต่อการอนุมัติ

โฟลว์

เมื่อ API ได้รับการกระทำอนุมัติ มันจะขอล็อกก่อนโดยอิงจาก approval id (เพื่อให้การอนุมัติต่างกันยังประมวลผลพร้อมกันได้)

รูปแบบที่ใช้บ่อยคือ: ล็อกตาม approval_id, อ่านสถานะปัจจุบัน, อัปเดตสถานะ, แล้วเขียนบันทึก audit ทั้งหมดใน transaction เดียวกัน

BEGIN;

-- One-at-a-time per approval_id
SELECT pg_try_advisory_xact_lock($1) AS got_lock;  -- $1 = approval_id

-- If got_lock = false, return "someone else is approving, try again".

SELECT status FROM approvals WHERE id = $1 FOR UPDATE;

-- If status != 'pending', return "already processed".

UPDATE approvals
SET status = 'approved', approved_by = $2, approved_at = now()
WHERE id = $1;

INSERT INTO approval_audit(approval_id, actor_id, action, created_at)
VALUES ($1, $2, 'approved', now());

COMMIT;

ประสบการณ์ของการคลิกครั้งที่สอง

คำขอที่สองอาจไม่ได้รับล็อก (จึงคืน "กำลังมีคนอนุมัติอยู่ ลองอีกครั้ง") หรือจะได้ล็อกหลังจากครั้งแรกเสร็จ แล้วเห็นว่าสถานะเป็น approved อยู่แล้วและออกโดยไม่เปลี่ยนอะไร ไม่ว่าจะอย่างไร คุณหลีกเลี่ยงการประมวลผลซ้ำและยังทำให้ UI ตอบสนอง

เพื่อการดีบัก ให้บันทึกพอที่จะติดตามความพยายามแต่ละครั้ง: request id, approval id และคีย์ล็อกที่คำนวณ, actor id, ผลลัพธ์ (lock_busy, already_approved, approved_ok), และเวลา

การจัดการการรอ, timeout และ retry โดยไม่ทำให้แอปค้าง

ป้องกันการรันซ้อนของตารางงาน
รันงานที่มีหลายอินสแตนซ์อย่างปลอดภัยโดยล็อกตามชื่องานและหน้าต่างเวลา
สร้างแอป

การรอฟังดูไม่มีปัญหาจนกว่าจะกลายเป็นปุ่มหมุน, worker ติดค้าง, หรือแบ็กล็อกที่ไม่เคยเคลียร์ เมื่อไม่ได้ล็อก ให้ล้มเหลวเร็วในทางที่คนกำลังรอ และรอเฉพาะที่ปลอดภัยที่จะรอ

สำหรับการกระทำของผู้ใช้: ใช้ try-lock แล้วตอบกลับชัดเจน

ถ้าผู้ใช้กด Approve หรือ Charge อย่าบล็อกคำขอหลายวินาที ใช้ try-lock เพื่อให้แอปตอบทันที

แนวทางปฏิบัติ: พยายามขอล็อก และถ้าล้มเหลว ให้คืนคำตอบชัดเจนว่า "กำลังยุ่ง ลองอีกครั้ง" (หรือรีเฟรชสถานะของไอเท็ม) เพื่อลด timeout และลดการคลิกซ้ำ

รักษาช่วงที่ถือล็อกให้สั้น: ตรวจสอบสถานะ, ใช้การเปลี่ยนสถานะ, commit

สำหรับงานแบ็กกราวด์: การบล็อกอาจโอเค แต่จำกัดเวลา

สำหรับ scheduler และ worker การบล็อกอาจยอมรับได้เพราะไม่มีคนรอ แต่คุณยังต้องมีขีดจำกัด มิฉะนั้นงานช้าหนึ่งงานอาจค้างทั้งฟลีท

ใช้ timeout เพื่อให้ worker ยอมแพ้แล้วไปงานถัดไป:

SET lock_timeout = '2s';
SET statement_timeout = '30s';
SELECT pg_advisory_lock(123456);

นอกจากนี้ตั้งเวลารันงานสูงสุดที่คาดไว้ ถ้าการเรียกเก็บเงินมักเสร็จใน < 10 วินาที ให้ถือว่า 2 นาทีเป็นเหตุการณ์ติดขัด ติดตามเวลาเริ่ม, job id, และระยะเวลาที่ถือล็อก หาก runner ของคุณรองรับการยกเลิก ให้ยกเลิกงานที่เกินขีดจำกัดเพื่อให้ session สิ้นสุดและล็อกถูกปล่อย

วางแผน retry อย่างมีจุดมุ่งหมาย เมื่อไม่ได้รับล็อก ให้ตัดสินใจว่าจะทำอย่างไรต่อ: จัดตารางใหม่โดยมี backoff (และสุ่มเล็กน้อย), ข้ามงานที่พยายามแบบ best-effort รอบนี้, หรือมาร์กไอเท็มว่าเกิด contention หากล้มเหลวซ้ำหลายครั้งต้องเอาใจใส่

ข้อผิดพลาดทั่วไปที่ทำให้ล็อกติดหรือเกิดการทำซ้ำ

สิ่งที่ทำให้ประหลาดใจบ่อยที่สุดคือ session-level locks ที่ไม่เคยถูกปล่อย การเชื่อมต่อจาก connection pool คงอยู่ ดังนั้น session อาจอยู่นานกว่าคำขอ หากคุณเอาล็อกระดับ session แล้วลืม unlock ล็อกอาจคงอยู่จนกว่าจะมีการรีไซเคิลการเชื่อมต่อ Worker อื่นจะรอ (หรือ fail) และอาจยากที่จะหาสาเหตุ

แหล่งที่มาของการทำซ้ำอีกอย่างคือการล็อกแต่ไม่ตรวจสอบสถานะ ล็อกช่วยให้ worker เดียวรันส่วนสำคัญทีละหนึ่ง แต่มันไม่ได้รับประกันว่า record ยังสามารถทำได้ ตรวจซ้ำภายใน transaction เสมอ (เช่น ยืนยันว่าเป็น pending ก่อนเปลี่ยนเป็น approved)

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

การถือล็อกนานมักเป็นผลจากการทำเอง หากคุณทำการเรียกเครือข่ายช้า (ผู้ให้บริการชำระเงิน อีเมล/SMS เว็บฮุก) ขณะถือล็อก ส่วนป้องกันสั้นๆ จะกลายเป็นคอขวด เก็บช่วงที่ถือล็อกเน้นงานฐานข้อมูลที่เร็ว: ตรวจสถานะ เขียนสถานะใหม่ บันทึกว่าจะเกิดอะไรขึ้นต่อไป แล้วทริกเกอร์ side effects หลัง transaction commit

สุดท้าย advisory locks ไม่ได้แทนที่ idempotency หรือข้อจำกัดของฐานข้อมูล ใช้มันเป็นสัญญาณไฟจราจร ไม่ใช่ระบบพิสูจน์ ใช้ unique constraints เมื่อเหมาะสม และใช้ idempotency keys สำหรับการเรียกภายนอก

เช็คลิสต์ด่วนก่อนปล่อยระบบ

ออกแบบสถานะเวิร์กโฟลว์ของคุณ
ใช้ Data Designer เพื่อแมปตารางและเก็บฟิลด์สถานะให้ชัดเจน
ออกแบบข้อมูล

ปฏิบัติกับ advisory locks เหมือนสัญญาสั้นๆ: ทุกคนในทีมควรรู้ว่าล็อกหมายถึงอะไร ปกป้องอะไร และอนุญาตให้เกิดอะไรได้บ้างขณะถือมัน

เช็คลิสต์สั้นๆ ที่จับปัญหาได้ส่วนใหญ่:

  • คีย์ล็อกชัดเจนต่อทรัพยากรหนึ่งหน่วย จดไว้และใช้ซ้ำทุกที่
  • ขอล็อกก่อนทำสิ่งที่ไม่สามารถย้อนกลับได้ (การชำระเงิน อีเมล เรียก API ภายนอก)
  • ตรวจสถานะซ้ำหลังได้ล็อกและก่อนเขียนการเปลี่ยนแปลง
  • เก็บช่วงที่ถือล็อกให้สั้นและวัดได้ (บันทึกเวลารอและเวลารัน)
  • ตัดสินใจว่า "ล็อกไม่ว่าง" หมายถึงอะไรต่อแต่ละเส้นทาง (ข้อความ UI, retry ด้วย backoff, ข้าม)

ขั้นตอนต่อไป: นำรูปแบบไปใช้และรักษาให้ง่ายต่อการดูแล

เลือกจุดหนึ่งที่การทำซ้ำทำให้เสียหายมากที่สุดแล้วเริ่มจากตรงนั้น เป้าหมายเริ่มต้นที่ดีคือการกระทำที่มีค่าใช้จ่ายหรือเปลี่ยนสถานะถาวร เช่น “ชาร์จใบแจ้งหนี้” หรือ “อนุมัติคำขอ” ห่อเฉพาะส่วนสำคัญนั้นด้วย advisory lock แล้วขยายไปยังขั้นตอนใกล้เคียงเมื่อคุณมั่นใจในพฤติกรรม

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

ล็อกทำงานได้ดีที่สุดบนพื้นฐานความปลอดภัยของข้อมูล ไม่ใช่แทนที่มัน เก็บฟิลด์สถานะชัดเจน (pending, processing, done, failed) และหนุนด้วย constraints เมื่อทำได้ หาก retry เกิดขึ้นในช่วงที่เลวร้ายที่สุด unique constraint หรือ idempotency key อาจเป็นแนวป้องกันชั้นที่สอง

ถ้าคุณสร้างเวิร์กโฟลว์ใน AppMaster (appmaster.io) คุณสามารถใช้รูปแบบเดียวกันโดยเก็บการเปลี่ยนแปลงสถานะสำคัญไว้ใน transaction เดียวและเพิ่มสเต็ป SQL เล็กๆ เพื่อขอล็อกระดับ transaction ก่อนสเต็ป "finalize"

Advisory locks เหมาะจนกว่าคุณจะต้องการฟีเจอร์ของคิวจริงๆ (ลำดับความสำคัญ งานหน่วงเวลา การจัดการ dead-letter), มี contention สูงจนต้องการการขนานที่ชาญฉลาดกว่า, ต้องประสานงานข้ามฐานข้อมูลโดยไม่มี Postgres ร่วม, หรือจำเป็นต้องมีกฎการแยกส่วนที่เข้มงวดกว่า เป้าหมายคือความน่าเชื่อถือที่น่าเบื่อ: เก็บรูปแบบให้เล็ก สม่ำเสมอ มองเห็นได้ในล็อก และหนุนด้วย constraints.

คำถามที่พบบ่อย

เมื่อใดควรใช้ PostgreSQL advisory locks แทนที่จะเชื่อถือโลจิกของแอปอย่างเดียว?

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

Advisory locks ต่างจาก SELECT ... FOR UPDATE อย่างไร?

ล็อกแถว (เช่น SELECT ... FOR UPDATE) ปกป้องแถวในตารางจริงและเหมาะเมื่อการทำงานทั้งหมดแม็พกับการอัปเดตแถวเดียว แต่ advisory locks ปกป้องคีย์ที่คุณเลือก ดังนั้นมันจึงใช้งานได้แม้เวิร์กโฟลว์จะสัมผัสหลายตาราง เรียกใช้บริการภายนอก หรือเริ่มก่อนที่แถวสุดท้ายจะถูกสร้าง

ควรใช้ advisory locks แบบผูกกับ transaction หรือแบบผูกกับ session?

ตั้งค่าเริ่มต้นเป็น pg_advisory_xact_lock (แบบผูกกับ transaction) สำหรับการทำงานแบบ request/response เพราะมันถูกปล่อยโดยอัตโนมัติเมื่อคุณ commit หรือ rollback ใช้ pg_advisory_lock (แบบผูกกับ session) เมื่อคุณต้องการให้ล็อกคงอยู่ยาวกว่าระยะเวลาของ transaction จริงๆ และแน่ใจว่าจะต้องเรียก unlock ก่อนคืนการเชื่อมต่อกลับสู่พูล

ควรรอจนได้ล็อกหรือใช้ try-lock ดีกว่า?

สำหรับงานที่มี UI ให้ใช้ try-lock (pg_try_advisory_xact_lock) เพื่อให้คำขอล้มเหลวเร็วและคืนข้อความชัดเจนว่า “กำลังประมวลผลอยู่” สำหรับงานแบ็กกราวด์ การรอแบบบล็อกอาจยอมรับได้ แต่ต้องตั้ง lock_timeout เพื่อไม่ให้งานที่ติดค้างชะงักทั้งคลัสเตอร์

ฉันควรล็อกอะไร: ID ของระเบียน, ID ของลูกค้า หรืออย่างอื่น?

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

ฉันเลือกคีย์ล็อกอย่างไรให้ทุกบริการใช้เหมือนกัน?

เลือกฟอร์แมตรหัสล็อกที่มั่นคงและใช้ซ้ำทุกที่ที่สามารถทำการกระทำสำคัญเดียวกันได้ วิธีที่ใช้บ่อยคือสองจำนวนเต็ม: กำหนด namespace คงที่สำหรับเวิร์กโฟลว์ แล้วใช้ entity ID เป็นค่าอีกตัว เพื่อให้เวิร์กโฟลว์ต่างกันไม่บล็อกกันเองโดยไม่ตั้งใจ

Advisory locks แทนการตรวจ idempotency หรือ constraints ได้ไหม?

ไม่ใช่ ล็อกช่วยป้องกันการรันพร้อมกัน แต่ไม่ได้พิสูจน์ว่าการดำเนินการทำซ้ำได้อย่างปลอดภัย คุณยังต้องตรวจซ้ำสถานะภายใน transaction (เช่น ยืนยันว่ายังเป็น pending) และใช้ unique constraints หรือ idempotency เมื่อเหมาะสม

ควรทำอะไรบ้างในช่วงที่ถือล็อกเพื่อไม่ให้ชะงักระบบ?

เก็บช่วงที่ถือล็อกให้น้อยและเน้นงานฐานข้อมูล: ดึงล็อก, ตรวจสอบสถานะ, เขียนสถานะใหม่ แล้ว commit ทำงานด้านนอกที่ช้า (เช่น การชำระเงิน อีเมล เว็บฮุก) หลัง commit หรือผ่าน outbox เพื่อไม่ให้ถือล็อกขณะรอเครือข่าย

ทำไมบางครั้ง advisory locks ดูเหมือนถูก “ติด” แม้คำขอจะเสร็จแล้ว?

สาเหตุที่พบบ่อยที่สุดคือ session-level lock ที่ถูกถือโดยการเชื่อมต่อจากพูลที่ไม่เคยถูกปลดล็อกเพราะบั๊กในโค้ด แนะนำใช้ transaction-level เป็นค่าเริ่มต้น หากจำเป็นต้องใช้ session locks ให้แน่ใจว่า pg_advisory_unlock ถูกเรียกอย่างเชื่อถือได้ก่อนคืนการเชื่อมต่อ

ฉันควรบันทึกหรือมอนิเตอร์อะไรเพื่อยืนยันว่า advisory locks ทำงาน?

บันทึก entity ID และคีย์ล็อกที่คำนวณได้, ว่าล็อกถูกได้หรือไม่, ใช้เวลานานแค่ไหนในการได้ล็อก, และระยะเวลาการทำงานของ transaction รวมถึงผลลัพธ์เช่น lock_busy, already_processed, หรือ processed_ok เพื่อแยกแยะระหว่าง contention กับการทำซ้ำจริงๆ

ง่ายต่อการเริ่มต้น
สร้างบางสิ่งที่ น่าทึ่ง

ทดลองกับ AppMaster ด้วยแผนฟรี
เมื่อคุณพร้อม คุณสามารถเลือกการสมัครที่เหมาะสมได้

เริ่ม