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

ปัญหาจริง: สองกระบวนการทำงานเดียวกันซ้ำ
การประมวลผลซ้ำเกิดขึ้นเมื่อรายการเดียวกันถูกจัดการสองครั้งเพราะผู้กระทำสองฝ่ายต่างคิดว่าตัวเองเป็นผู้รับผิดชอบ ในแอปจริง มันอาจแสดงผลเป็นลูกค้าถูกเรียกเก็บเงินสองครั้ง การอนุมัติถูกนำไปใช้อีกครั้ง หรืออีเมล “ใบแจ้งหนี้พร้อมแล้ว” ถูกส่งสองครั้ง ทุกอย่างอาจดูปกติในการทดสอบ แต่พังเมื่อมีทราฟฟิกจริง
โดยทั่วไปมันเกิดขึ้นเมื่อเวลาแน่นและมีมากกว่าหนึ่งสิ่งที่สามารถทำงานได้พร้อมกัน:
สอง 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 แต่คุณต้องยอมรับความเสี่ยงการชนกันเล็กน้อยและต้องใช้สม่ำเสมอทุกที่
ไม่ว่าจะเลือกแบบไหน ให้จดฟอร์แมตและเก็บไว้รวมศูนย์ “เกือบจะเหมือนกัน” ในสองที่คือสาเหตุที่พบบ่อยในการนำปัญหากลับมา
ทีละขั้นตอน: รูปแบบปลอดภัยสำหรับการประมวลผลทีละหนึ่ง
เวิร์กโฟลว์ advisory-lock ที่ดีนั้นเรียบง่าย: lock, verify, act, record, commit. ล็อกไม่ใช่กฎธุรกิจด้วยตัวเอง แต่มันเป็นราวกันตกที่ทำให้กฎนั้นเชื่อถือได้เมื่อสอง worker เข้าถึงระเบียนเดียวกันพร้อมกัน
รูปแบบปฏิบัติ:
- เปิด transaction เมื่อผลลัพธ์ต้องเป็น atomic
- ขอล็อกสำหรับหน่วยงานเฉพาะของงาน เลือกล็อกที่ผูกกับ transaction (
pg_advisory_xact_lock) เพื่อให้ปล่อยอัตโนมัติ - ตรวจสถานะซ้ำในฐานข้อมูล อย่าสมมติว่าคุณเป็นคนแรก ยืนยันว่ายังสามารถทำได้
- ทำงานและเขียนมาร์กเกอร์ “เสร็จ” ที่ทนทานในฐานข้อมูล (อัปเดตสถานะ, บันทึกบัญชี, แถว audit)
- 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
ตัวอย่างสมจริง: หยุดการอนุมัติซ้ำในพอร์ทัล
ลองนึกภาพคำขออนุมัติในพอร์ทัล: ใบสั่งซื้อรออยู่ และผู้จัดการสองคนกด 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 สำหรับการเรียกภายนอก
เช็คลิสต์ด่วนก่อนปล่อยระบบ
ปฏิบัติกับ 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.
คำถามที่พบบ่อย
ใช้ advisory lock เมื่อคุณต้องการให้มี “เพียงผู้ดำเนินการหนึ่งคนเท่านั้นในแต่ละครั้ง” สำหรับหน่วยงานเฉพาะของงาน เช่น การอนุมัติคำขอ การชำระเงินของใบแจ้งหนี้ หรืองานที่กำหนดเวลา มันเป็นประโยชน์อย่างยิ่งเมื่อมีหลายอินสแตนซ์ของแอปที่อาจเข้าถึงรายการเดียวกันและคุณไม่ต้องการเพิ่มบริการล็อกแยกต่างหาก
ล็อกแถว (เช่น SELECT ... FOR UPDATE) ปกป้องแถวในตารางจริงและเหมาะเมื่อการทำงานทั้งหมดแม็พกับการอัปเดตแถวเดียว แต่ advisory locks ปกป้องคีย์ที่คุณเลือก ดังนั้นมันจึงใช้งานได้แม้เวิร์กโฟลว์จะสัมผัสหลายตาราง เรียกใช้บริการภายนอก หรือเริ่มก่อนที่แถวสุดท้ายจะถูกสร้าง
ตั้งค่าเริ่มต้นเป็น pg_advisory_xact_lock (แบบผูกกับ transaction) สำหรับการทำงานแบบ request/response เพราะมันถูกปล่อยโดยอัตโนมัติเมื่อคุณ commit หรือ rollback ใช้ pg_advisory_lock (แบบผูกกับ session) เมื่อคุณต้องการให้ล็อกคงอยู่ยาวกว่าระยะเวลาของ transaction จริงๆ และแน่ใจว่าจะต้องเรียก unlock ก่อนคืนการเชื่อมต่อกลับสู่พูล
สำหรับงานที่มี UI ให้ใช้ try-lock (pg_try_advisory_xact_lock) เพื่อให้คำขอล้มเหลวเร็วและคืนข้อความชัดเจนว่า “กำลังประมวลผลอยู่” สำหรับงานแบ็กกราวด์ การรอแบบบล็อกอาจยอมรับได้ แต่ต้องตั้ง lock_timeout เพื่อไม่ให้งานที่ติดค้างชะงักทั้งคลัสเตอร์
ล็อกสิ่งที่เล็กที่สุดที่ต้องไม่ให้ทำซ้ำ แนะนำมักเป็น “แถวเดียว” เช่น ใบแจ้งหนี้หนึ่งรายการหรือคำขออนุมัติหนึ่งรายการ หากล็อกกว้างเกินไป (เช่น ต่อผู้ใช้) อาจลดความสามารถในการประมวลผล แต่หากล็อกแคบเกินไปโดยไม่มีคีย์ร่วม คุณก็ยังอาจเจอการทำซ้ำได้
เลือกฟอร์แมตรหัสล็อกที่มั่นคงและใช้ซ้ำทุกที่ที่สามารถทำการกระทำสำคัญเดียวกันได้ วิธีที่ใช้บ่อยคือสองจำนวนเต็ม: กำหนด namespace คงที่สำหรับเวิร์กโฟลว์ แล้วใช้ entity ID เป็นค่าอีกตัว เพื่อให้เวิร์กโฟลว์ต่างกันไม่บล็อกกันเองโดยไม่ตั้งใจ
ไม่ใช่ ล็อกช่วยป้องกันการรันพร้อมกัน แต่ไม่ได้พิสูจน์ว่าการดำเนินการทำซ้ำได้อย่างปลอดภัย คุณยังต้องตรวจซ้ำสถานะภายใน transaction (เช่น ยืนยันว่ายังเป็น pending) และใช้ unique constraints หรือ idempotency เมื่อเหมาะสม
เก็บช่วงที่ถือล็อกให้น้อยและเน้นงานฐานข้อมูล: ดึงล็อก, ตรวจสอบสถานะ, เขียนสถานะใหม่ แล้ว commit ทำงานด้านนอกที่ช้า (เช่น การชำระเงิน อีเมล เว็บฮุก) หลัง commit หรือผ่าน outbox เพื่อไม่ให้ถือล็อกขณะรอเครือข่าย
สาเหตุที่พบบ่อยที่สุดคือ session-level lock ที่ถูกถือโดยการเชื่อมต่อจากพูลที่ไม่เคยถูกปลดล็อกเพราะบั๊กในโค้ด แนะนำใช้ transaction-level เป็นค่าเริ่มต้น หากจำเป็นต้องใช้ session locks ให้แน่ใจว่า pg_advisory_unlock ถูกเรียกอย่างเชื่อถือได้ก่อนคืนการเชื่อมต่อ
บันทึก entity ID และคีย์ล็อกที่คำนวณได้, ว่าล็อกถูกได้หรือไม่, ใช้เวลานานแค่ไหนในการได้ล็อก, และระยะเวลาการทำงานของ transaction รวมถึงผลลัพธ์เช่น lock_busy, already_processed, หรือ processed_ok เพื่อแยกแยะระหว่าง contention กับการทำซ้ำจริงๆ


