أقفال PostgreSQL الإرشادية لعمليات آمنة بالتزامن
تعلم استخدام أقفال PostgreSQL الإرشادية لمنع المعالجة المزدوجة في الموافقات، الفوترة والمجدولات مع أنماط عملية ومقتطفات SQL وفحوص بسيطة.

المشكلة الحقيقية: عمليتان تقومان بنفس العمل
المعالجة المزدوجة تحدث عندما يُعالَج نفس العنصر مرتين لأن جهتين مختلفتين تعتقدان أنهما مسؤولتان. في التطبيقات الحقيقية يظهر ذلك كأن يُخصم من عميل مرتين، أو تُطبَّق موافقة مرتين، أو يُرسل بريد "الفاتورة جاهزة" مرتين. كل شيء قد يبدو جيدًا في الاختبار ثم ينهار تحت ضغط حركة حقيقية.
عادةً يحدث هذا عندما تصبح التوقيتات ضيقة ويمكن لأكثر من شيء أن يتصرف:
عاملان يلتقطان نفس المهمة في الوقت نفسه. محاولة إعادة تُشغَّل لأن نداء الشبكة كان بطيئًا بينما المحاولة الأولى لا تزال قيد التشغيل. المستخدم يضغط موافق مرتين لأن الواجهة تجمّدت لثانية. جدولان مجدولان يتداخلان بعد نشر أو انحراف ساعة. حتى ضغطة واحدة قد تصبح طلبين إذا أعاد تطبيق جوال الإرسال بعد مهلة.
الجزء المؤلم أن كل جهة تتصرف "بعقلانية" بمفردها. الخطأ هو الفجوة بينهما: لا يعرف أي منهما أن الآخر يعالج نفس السجل بالفعل.
الهدف بسيط: لأي عنصر معين (طلب، طلب موافقة، فاتورة)، يجب أن يُسمَح لممثل واحد فقط بأداء العمل الحرج في كل وقت. الباقون ينتظرون قليلاً أو يتراجعون ويحاولون لاحقًا.
أقفال PostgreSQL الإرشادية تساعد في ذلك. تمنحك طريقة خفيفة لتقول «أنا أعمل على العنصر X» باستخدام قاعدة البيانات التي تثق بها بالفعل من ناحية التناسق.
ضع توقعات واضحة: القفل ليس نظام طابور كامل. لن يدوّن المهام نيابةً عنك، ولا يضمن ترتيبًا، ولا يخزن رسائل. هو بوابة أمان حول جزء من سير العمل الذي لا يجب أن يعمل مرتين.
ما هي أقفال PostgreSQL الإرشادية (وما ليست عليه)
الأقفال الإرشادية في PostgreSQL وسيلة للتأكّد من أن عاملًا واحدًا فقط يؤدي قطعة عمل في كل مرة. تختار مفتاح القفل (مثل "فاتورة 123"), تطلب من قاعدة البيانات قفله، تقوم بالعمل، ثم تحرره.
كلمة "إرشادي" مهمة. PostgreSQL لا يعرف ماذا يعني مفتاحك، ولن يحمي أي شيء تلقائيًا. يتتبع حقيقة واحدة فقط: هل هذا المفتاح مقفول أم لا. على كودك أن يتفق على تنسيق المفتاح ويأخذ القفل قبل تشغيل الجزء الحساس.
من المفيد أيضًا مقارنة الأقفال الإرشادية بأقفال الصف. أقفال الصف (مثل SELECT ... FOR UPDATE) تحمي صفوف الجداول الفعلية. هي ممتازة عندما يتوافق العمل بشكل واضح مع صف واحد. الأقفال الإرشادية تحمي مفتاحًا تختاره، وهو مفيد عندما يلمس سير العمل جداول متعددة، يستدعي خدمات خارجية، أو يبدأ قبل وجود الصف.
الأقفال الإرشادية مفيدة عندما تحتاج:
- إجراءات واحد-في-كل-مرة لكل كيان (موافقة واحدة لكل طلب، شحنة واحدة لكل فاتورة)
- تنسيق عبر خوادم تطبيق متعددة دون إضافة خدمة قفل منفصلة
- حماية حول خطوة سير عمل أكبر من مجرد تحديث صف واحد
هي ليست بديلاً عن أدوات الأمان الأخرى. لا تجعل العمليات متعادلة نتيجة لها، ولا تفرض قواعد عمل، ولن توقف التكرارات إذا نسي مسار الشيفرة أخذ القفل.
غالبًا ما تُسمى "خفيفة" لأنك تستطيع استخدامها دون تغييرات في المخطط أو بنية تحتية إضافية. في كثير من الحالات، يمكنك إصلاح المعالجة المزدوجة بإضافة استدعاء قفل واحد حول قسم حرج مع إبقاء بقية التصميم كما هو.
أنواع الأقفال التي ستستخدمها فعليًا
عندما يقول الناس "أقفال PostgreSQL الإرشادية" عادةً يقصدون مجموعة صغيرة من الدوال. اختيار الدالة الصحيحة يغيّر ما يحدث عند الأخطاء، وانقضاء الوقت، وإعادة المحاولة.
أقفال مستوى الجلسة مقابل مستوى المعاملة
قفل مرتبط بالجلسة (pg_advisory_lock) يستمر طالما استمر اتصال قاعدة البيانات. قد يكون ذلك مناسبًا للعمال طويلين التشغيل، لكنه يعني أيضًا أن القفل قد يبقى معلقًا إذا تعطل تطبيقك بطريقة تترك اتصالًا في المجمّع.
قفل مرتبط بالمعاملة (pg_advisory_xact_lock) مربوط بالمعاملة الحالية. عند الالتزام أو التراجع، يفرج PostgreSQL عنه تلقائيًا. لمعظم تدفقات طلب-استجابة (موافقات، نقرات دفع، إجراءات إدارة)، هذا الافتراضي الأكثر أمانًا لأنه من الصعب نسيان الإفراج.
الانتظار مقابل try-lock
استدعاءات الانتظار تحاول الانتظار حتى يصبح القفل متاحًا. بسيطة، لكنها قد تجعل طلب ويب يبدو عالقًا إذا كان جلسة أخرى تحتفظ بالقفل.
استدعاءات try-lock تعود فورًا:
pg_try_advisory_lock(مستوى الجلسة)pg_try_advisory_xact_lock(مستوى المعاملة)
عادةً ما يكون try-lock أفضل لإجراءات الواجهة. إذا كان القفل مأخوذًا، يمكنك إرجاع رسالة واضحة مثل "قيد المعالجة" وطلب إعادة المحاولة.
مشترك مقابل حصري
الأقفال الحصرية هي "واحد في كل مرة". الأقفال المشتركة تسمح لحملة متعددة ولكن تمنع قفلًا حصريًا. معظم مشاكل المعالجة المزدوجة تستخدم أقفالًا حصرية. الأقفال المشتركة مفيدة عند السماح للقرّاء المتعدّدين بالتقدّم بينما يحتاج كاتب نادر للتشغيل بمفرده.
كيف تُفرج الأقفال
الإفراج يعتمد على النوع:
- أقفال الجلسة: تُفرج عند قطع الاتصال، أو صراحةً بواسطة
pg_advisory_unlock - أقفال المعاملة: تُفرج تلقائيًا عند انتهاء المعاملة
اختيار مفتاح القفل الصحيح
القفل الإرشادي يعمل فقط إذا حاول كل عامل قفل نفس المفتاح تمامًا لنفس قطعة العمل. إذا قفل مسار واحد "فاتورة 123" ومسار آخر "عميل 45"، فلا يزال من الممكن أن يحدث تكرار.
ابدأ بتسمية "الشيء" الذي تريد حمايته. اجعله ملموسًا: فاتورة واحدة، طلب موافقة واحد، تشغيل مهمة مجدولة، أو دورة فوترة شهرية لعميل. هذا الاختيار يحدد مقدار التزامن الذي تسمح به.
اختر نطاقًا يتماشى مع المخاطر
تتوقف معظم الفرق في أحد هذه الخيارات:
- لكل سجل: الأكثر أمانًا للموافقات والفواتير (قفل حسب
invoice_idأوrequest_id) - لكل عميل/حساب: مفيد عندما يجب تسلسل الإجراءات لكل عميل (الفوترة، تغييرات الرصيد)
- لكل خطوة سير عمل: عندما يمكن لخطوات مختلفة أن تعمل متوازية، لكن كل خطوة يجب أن تكون واحد-في-كل-مرة
اعتبر النطاق قرارًا منتجياً لا تفصيلًا لقاعدة البيانات. "لكل سجل" يمنع النقرات المزدوجة من الشحن مرتين. "لكل عميل" يمنع وظيفتين خلفيتين من توليد كشوف متداخلة.
اختر استراتيجية مفتاح مستقرة
عادة لديك خياران: عددان صحيحان 32-بت (غالبًا مساحة أسماء + id)، أو عدد صحيح 64-بت (bigint)، يُنشأ أحيانًا عن طريق تجزئة معرف نصي.
مفاتيح الإثنين-int سهلة التوحيد: اختر رقم مساحة أسماء ثابتًا لكل سير عمل (مثلاً، الموافقات مقابل الفوترة)، واستخدم معرف السجل كالقيمة الثانية.
التجزئة قد تكون مفيدة عندما يكون المعرف UUID، لكن عليك قبول احتمال تصادم صغير وأن تكون متسقًا في كل مكان.
مهما اخترت، دوّن التنسيق ومركّزه في مكان واحد. "مفتاح شبه مطابق" في مكانين هو طريقة شائعة لإعادة إدخال التكرارات.
خطوة بخطوة: نمط آمن للمعالجة واحد-في-كل-مرة
نمط قفل إرشادي جيد بسيط: اقفل، تحقق، نفّذ، سجّل، التزم. القفل ليس قاعدة العمل بحد ذاته. هو حاجز يجعل القاعدة موثوقة عندما يصطدم عاملان بنفس السجل في الوقت نفسه.
نمط عملي:
- افتح معاملة عندما يجب أن تكون النتيجة ذرية.
- احصل على القفل للوحدة المحددة من العمل. فضّل قفل مرتبط بالمعاملة (
pg_advisory_xact_lock) ليُفرج عنه تلقائيًا. - أعد التحقق من الحالة في قاعدة البيانات. لا تفترض أنك الأول. تأكد أن السجل ما زال مؤهلاً.
- قم بالعمل واكتب علامة "منجَز" دائمة في القاعدة (تحديث حالة، إدخال في دفتر الأستاذ، صف تدقيق).
- التزم ودع القفل يزول. إذا استخدمت قفل جلسة، فكّه قبل إعادة الاتصال إلى المجمّع.
مثال: خادمان يستقبلان "اعتمد الفاتورة #123" في نفس الثانية. كلاهما يبدأ، لكن واحدًا فقط يحصل على القفل لـ 123. الفائز يتحقق أن الفاتورة ما تزال pending, يضعها approved, يكتب سجل التدقيق/المدفوعات، ويلتزم. الخادم الثاني إما يفشل سريعًا (try-lock) أو ينتظر قليلًا، ثم يعيد التحقق ويخرج دون إنشاء تكرار. في كلتا الحالتين تتجنّب التكرار مع الحفاظ على استجابة الواجهة.
أين تناسب الأقفال الإرشادية: الموافقات، الفوترة، والجداول الزمنية
الأقفال الإرشادية تناسب الأفضل عندما تكون القاعدة واضحة: لشيء محدد، لا يمكن لعدة عمليات أن تؤدّي العمل "الفائز" في آنٍ واحد. تحتفظ بقاعدة البيانات والتطبيق اللذين لديك، لكن تضيف بوابة صغيرة تجعل حالات السباق أصعب في الحدوث.
الموافقات
الموافقات صناديق فخارية للتزامن. اثنان من المراجعِين (أو نفس الشخص يضغط مرتين) قد يضغطان "موافق" في غضون ميلي ثانية. بقفل مرتبط بمعرّف الطلب، تُنفّذ معاملة واحدة فقط تغيير الحالة. الباقون يعرفون النتيجة بسرعة ويمكن عرض رسالة واضحة مثل "معتمد بالفعل" أو "مرفوض بالفعل".
هذا شائع في بوابات العملاء ولوحات الإدارة حيث يراقب كثيرون نفس الطابور.
الفوترة
الفوترة عادةً تحتاج قاعدة أشد صرامة: محاولة دفع واحدة لكل فاتورة، حتى عند حدوث إعادة محاولات. مهلة في الشبكة قد تجعل المستخدم يضغط Pay مرة أخرى، أو محاول إعادة تشغيل خلفية تعمل بينما المحاولة الأولى لا تزال جارية.
قفل مربوط بمعرف الفاتورة يضمن أن مسارًا واحدًا فقط يتحدث إلى مزوّد الدفع في كل وقت. المحاولة الثانية يمكن أن تعيد "الدفع جارٍ" أو تقرأ حالة الدفع الأخيرة. هذا يمنع العمل المزدوج ويقلّل خطر الشحن المزدوج.
الجداول والعمال الخلفيون
في إعدادات متعددة النُسخ، يمكن للجدولات أن تشغل نفس النافذة بالتوازي عن غير قصد. قفل مرتبط باسم المهمة مع نافذة الوقت (مثلاً "daily-settlement:2026-01-29") يضمن أن نسخة واحدة فقط تشغّلها.
نفس النهج يعمل للعمال الذين يسحبون عناصر من جدول: اقفل على معرف العنصر حتى عامل واحد فقط يعالجه.
المفاتيح الشائعة تشمل معرف طلب الموافقة، معرف الفاتورة، اسم المهمة مع نافذة الوقت، معرف العميل "لتصدير واحد في المرة"، أو مفتاح عدم التكرار الفريد لإعادة المحاولات.
مثال واقعي: إيقاف الموافقة المزدوجة في بوابة
تخيّل طلب موافقة في بوابة: طلب شراء بانتظار، واثنان من المديرين يضغطان "موافق" في نفس الثانية. بدون حماية، كلا الطلبين يقرأان "قيد الانتظار" وكلاهما قد يكتب "معتمد"، مما يخلق سجلات تدقيق مكررة، إشعارات مكررة، أو أعمالًا لاحقة تُشغّل مرتين.
أقفال PostgreSQL الإرشادية تعطيك طريقة مباشرة لجعل هذا الإجراء واحد-في-كل-مرة لكل موافقة.
سير العمل
عندما تستقبل الـ API فعل الموافقة، تأخذ أولاً قفلًا بناءً على approval_id (حتى تظل الطلبات المختلفة قابلة للمعالجة بالتوازي).
نمط شائع: اقفل على approval_id, اقرأ الحالة الحالية, حدّث الحالة, ثم اكتب سجل تدقيق، كلها داخل معاملة واحدة.
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;
ماذا يشعر به النقر الثاني
الطلب الثاني إما لا يحصل على القفل (فيرجع بسرعة "قيد المعالجة") أو يحصل على القفل بعد انتهاء الأول، ثم يرى أن الحالة بالفعل معتمدة ويخرج دون تغيير. بأي حال تتجنّب المعالجة المزدوجة مع الحفاظ على استجابة الواجهة.
لأغراض التصحيح، سجّل ما يكفي لتتبع كل محاولة: معرّف الطلب، معرّف الموافقة ومفتاح القفل المحسوب، معرّف الفاعل، النتيجة (lock_busy, already_approved, approved_ok), والوقت المستغرق.
التعامل مع الانتظار، انتهاء المهلة، وإعادة المحاولة بدون تجميد التطبيق
الانتظار للحصول على قفل يبدو غير مؤذ حتى يتحوّل إلى زر يدور، عامل عالق، أو تراكم لا يزول. عندما لا تستطيع الحصول على القفل، فشل بسرعة حيث ينتظر إنسان، وانتظر فقط حيث يكون الانتظار آمنًا.
لإجراءات المستخدم: try-lock وردّ واضح
إذا ضغط أحدهم "موافق" أو "ادفع"، لا تحبس طلبه لثوانٍ. استخدم try-lock حتى يجيب التطبيق فورًا.
نهج عملي: حاول القفل، وإذا فشل، أعد استجابة واضحة "مشغول، حاول مرة أخرى" (أو حدّث عرض العنصر). هذا يقلّل المهلات ويمنع النقرات المتكررة.
اجعل القسم المقفل قصيرًا: تحقق من الحالة، طبّق التغيير، والتزم.
للمهام الخلفية: الانتظار مقبول لكن حدده
للمجدولات والعمال، الانتظار قد يكون مقبولًا لأن لا إنسان ينتظر. لكنك تحتاج حدودًا، وإلا ستعطل مهمة بطيئة أسطولًا كاملًا.
استخدم مهلات بحيث يمكن للعامل أن يتخلى ويتابع:
SET lock_timeout = '2s';
SET statement_timeout = '30s';
SELECT pg_advisory_lock(123456);
حدد أيضًا زمن تشغيل متوقع أقصى للمهمة نفسها. إذا كانت الفوترة عادةً تنتهي في أقل من 10 ثوانٍ، اعتبر دقيقتين حادثًا. تتبّع وقت البدء، معرف المهمة، ومدة القفل المحفوظة. إذا يدعم المشغل إلغاء المهام، ألغِ المهام التي تتجاوز الحد حتى تنتهي الجلسة ويُفرَج عن القفل.
خطط لإعادة المحاولات عن عمد. عندما لا تُكتسب القفل، قرِّر ما يحدث لاحقًا: أعد جدولتها قريبًا مع تراجع وزعي، تجاوز العمل غير الحتمي لهذه الدورة، أو علّم العنصر كمتنازع إذا تكررت الإخفاقات وتحتاج انتباهًا.
أخطاء شائعة تسبب أقفالًا عالقة أو تكرارات
المفاجأة الأكثر شيوعًا هي أقفال مستوى الجلسة التي لا تُفرَج أبدًا. مجمّعات الاتصالات تبقي الاتصالات مفتوحة، لذا يمكن للجلسة أن تعيش أطول من الطلب. إذا أخذت قفل جلسة ونسيّت فكّه، قد يبقى القفل حتى يُعاد تدوير الاتصال. العاملون الآخرون سينتظرون (أو يفشلون) وقد يصعب رؤية السبب.
مصدر آخر للتكرارات هو أخذ القفل دون إعادة التحقق من الحالة. القفل يضمن أن عاملًا واحدًا ينفّذ الجزء الحرج في كل مرة، لكنه لا يضمن أن السجل ما زال مؤهلاً. دائمًا أعد التحقق داخل نفس المعاملة (مثلاً تأكد pending قبل الانتقال إلى approved).
مفاتيح القفل أيضًا تُربك الفرق. إذا قفل خدمة واحدة على order_id وأخرى على مفتاح محسوب مختلف لنفس المورد الواقعي، يصبح لديك قفلان. كلا المسارين قد يعملان في آن واحد، ما يخلق شعورًا زائفًا بالأمان.
الاحتفاظ الطويل بالأقفال عادةً ما يكون من صنعك. إذا أجريت نداءات شبكية بطيئة أثناء الاحتفاظ بالقفل (مزود دفع، بريد/SMS، webhooks)، يتحوّل حاجز قصير إلى عنق زجاجة. اجعل القسم المقفول مركزًا على عمل قاعدة بيانات سريع: تحقق من الحالة، اكتب الحالة الجديدة، سجّل ما يجب أن يحدث بعد ذلك. ثم أطلق الآثار الخارجية بعد الالتزام.
أخيرًا، الأقفال الإرشادية لا تحل محل عدم التكرار أو قيود قاعدة البيانات. اعتبرها إشارة مرور، لا نظام إثبات. استخدم قيودًا فريدة حيث تناسب، ومفاتيح عدم التكرار للنداءات الخارجية.
قائمة فحص سريعة قبل النشر
عامل الأقفال الإرشادية كعقد صغير: يجب أن يعرف كل الفريق ماذا يعني القفل، ما الذي يحميه، وما المسموح حدوثه أثناء الاحتفاظ به.
قائمة قصيرة تلتقط معظم المشاكل:
- مفتاح قفل واحد وواضح لكل مورد، مدوَن ويُعاد استخدامه في كل مكان
- الحصول على القفل قبل أي شيء لا رجعة فيه (مدفوعات، رسائل، نداءات API خارجية)
- أعد التحقق من الحالة بعد حيازة القفل وقبل كتابة التغييرات
- اجعل القسم المقفول قصيرًا وقابلًا للقياس (سجل زمن انتظار القفل ووقت التنفيذ)
- قرّر ماذا يعني "القفل مشغول" لكل مسار (رسالة واجهة، إعادة محاولة بتراجع، تجاوز)
الخطوات التالية: طبّق النمط وحافظ على قابليته للصيانة
اختر مكانًا واحدًا حيث تسبب التكرارات أكبر ضرر وابدأ هناك. أهداف البداية الجيدة هي إجراءات تكلف مالًا أو تغيّر الحالة نهائيًا، مثل "شحن فاتورة" أو "الموافقة على طلب". لف فقط هذا القسم الحرج بقفل إرشادي، ثم وسّع إلى خطوات قريبة عندما تثق في السلوك.
أضِف قابلية رصد أساسية مبكرًا. سجّل متى لا يحصل العامل على قفل، وكم يستغرق العمل المقفول. إذا قفزت أوقات انتظار القفل فهذا عادةً يعني أن القسم الحرج كبير جدًا أو استعلام بطيء مخفي بداخله.
الأقفال تعمل أفضل فوق أمان البيانات، لا بدلًا عنه. حافظ على حقول حالة واضحة (pending, processing, done, failed) ودعمها بقيود حيث يمكنك. إذا حدثت إعادة محاولة في أسوأ لحظة، يمكن أن تكون القيد الفريد أو مفتاح عدم التكرار خط الدفاع الثاني.
إذا كنت تبني تدفقات في AppMaster (appmaster.io)، يمكنك تطبيق نفس النمط بحفظ تغيّر الحالة الحرج داخل معاملة واحدة وإضافة خطوة SQL صغيرة لأخذ قفل إرشادي مرتبط بالمعاملة قبل خطوة "الإنهاء".
الأقفال الإرشادية مناسبة حتى تحتاج فعلاً ميزات الطابور (أولويات، مهام مؤجلة، معالجة رسائل ميتة)، لديك ازدحام كبير وتحتاج توازٍ أذكى، يجب التنسيق عبر قواعد بيانات دون PostgreSQL مشترك، أو تحتاج قواعد عزلة أشد. الهدف هو الاعتمادية المعتادة: اجعل النمط صغيرًا، متسقًا، واضحًا في السجلات، ومدعومًا بقيود.
الأسئلة الشائعة
استخدم القفل الإرشادي عندما تحتاج "مجرّد ممثل واحد في كل مرة" لوحدة عمل محددة، مثل الموافقة على طلب، أو شحن فاتورة، أو تشغيل نافذة مجدولة. يفيد ذلك خصوصًا حينما يمكن لنسخ متعددة من التطبيق الوصول إلى نفس العنصر ولا تريد إضافة خدمة قفل منفصلة.
أقفال الصف (SELECT ... FOR UPDATE) تحمي الصفوف الحقيقية التي تختارها وهي ممتازة عندما تطابق العملية تحديث صف واحد بشكل واضح. أما الأقفال الإرشادية فتحمي مفتاحًا تختاره، فتعمل حتى عندما يتضمن سير العمل جداول متعددة أو يستدعي خدمات خارجية أو يبدأ قبل وجود الصف النهائي.
افضّل pg_advisory_xact_lock (قفل مرتبط بالمعاملة) لإجراءات الطلب/الاستجابة لأن PostgreSQL يفرج عنه تلقائيًا عند الالتزام أو التراجع. استخدم pg_advisory_lock (قفل مرتبط بالجلسة) فقط إذا كنت تحتاج فعلًا أن يبقى القفل بعد انتهاء المعاملة وتثق أنك ستفك القفل دائمًا قبل إعادة الاتصال لمجّمع الاتصالات.
بالنسبة لإجراءات واجهة المستخدم، فضّل try-lock (pg_try_advisory_xact_lock) حتى يفشل الطلب بسرعة وتعيد للمستخدم رسالة واضحة "قيد المعالجة". أما للوظائف الخلفية، فانتظار القفل قد يكون مقبولًا، لكن ضع حدًا للانتظار باستخدام lock_timeout حتى لا يعرقل مهمة بطيئة كل النظام.
اقفل أصغر وحدة يجب ألّا تعمل مرتين عادةً؛ غالبًا "فاتورة واحدة" أو "طلب موافقة واحد". القفل الواسع جدًا (مثل per-customer) يقلل الإنتاجية، والقفل الضيّق جدًا قد لا يمنع التكرارات إن لم يتشارك كل مسار نفس المفتاح.
اختر تنسيق مفتاح ثابت واستعمله في كل مكان يستطيع تنفيذ نفس الإجراء الحرج. نهج شائع هو اثنان من الأعداد الصحيحة: مساحة أسماء ثابتة للسيناريو + معرّف الكيان، حتى لا تحجب السيناريوهات المختلفة بعضها البعض مع الاحتفاظ بالتنسيق الموحد.
لا. القفل يمنع التنفيذ المتزامن فقط؛ لا يثبت أن العملية آمنة للتكرار. عليك إعادة التحقق من الحالة داخل المعاملة (مثلاً التأكد أن العنصر لا يزال pending) والاعتماد على قيود فريدة أو مفاتيح عدم التكرار عند الحاجة.
اجعل القسم المحمي بالقفل قصيرًا ومركّزًا على قاعدة البيانات: احصل على القفل، أعد التحقق من الأهلية، اكتب الحالة الجديدة، والتزم. نفّذ الآثار الجانبية البطيئة (مدفوعات، رسائل، webhooks) بعد الالتزام أو عبر سجل خروج ليتم معالجة الشبكات بدون إبقاء القفل.
السبب الأشهر هو قفل مرتبط بالجلسة بقي في اتصال من مجمّع الاتصالات بسبب خطأ في المسار البرمجي. الأفضل استخدام الأقفال المرتبطة بالمعاملة، وإذا لزم استخدام أقفال الجلسة فتأكد من أن pg_advisory_unlock ينفّذ بثبات قبل إعادة الاتصال للمجمّع.
سجّل معرّف الكيان والمفتاح المحسوب، وما إذا تم الحصول على القفل، ومدة الانتظار للحصول على القفل، ومدة تشغيل المعاملة. سجّل النتيجة مثل lock_busy، already_processed، أو processed_ok حتى تميّز بين التنافس الحقيقي والتكرارات الفعلية.


