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 แต่คุณต้องยอมรับความเสี่ยงการชนกันเล็กน้อยและต้องใช้สม่ำเสมอทุกที่

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

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

สร้างตรรกะที่หลีกเลี่ยงการแข่งขันได้
ใช้ Business Process Editor เพื่อทำให้ส่วนสำคัญสั้นและตรวจสอบได้
ลองเลย

เวิร์กโฟลว์ 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

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

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

ลองนึกภาพคำขออนุมัติในพอร์ทัล: ใบสั่งซื้อรออยู่ และผู้จัดการสองคนกด 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 โดยไม่ทำให้แอปค้าง

ส่งมอบพอร์ทัลพร้อมใช้งานสำหรับโปรดักชั่น
สร้าง backend, เว็บ และแอปมือถือจากโปรเจกต์เดียวใน AppMaster
สร้างพอร์ทัล

การรอฟังดูไม่มีปัญหาจนกว่าจะกลายเป็นปุ่มหมุน, 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 สำหรับการเรียกภายนอก

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

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

ปฏิบัติกับ 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 ด้วยแผนฟรี
เมื่อคุณพร้อม คุณสามารถเลือกการสมัครที่เหมาะสมได้

เริ่ม