Get a quote

مفاتيح الأمان من التكرار في تطبيقات Go SaaS: منع العمليات المزدوجة على النطاق الواسع

عندما يُعيد عميل الجوال طلب الدفع بسبب انقطاع الشبكة، أو يُرسَل webhook مرتين، تصبح idempotency الشيء الوحيد الذي يقف بين قاعدة بياناتك وسجلات الفوترة الفاسدة. هذا كيف ننفذها في تطبيقات Go SaaS.

عندما يُعيد عميل الجوال طلب الدفع بسبب انقطاع الشبكة، أو يُطلق نظام التسليم webhook مرتين نظراً لدلالاته "على الأقل مرة واحدة"، تصبح idempotency الشيء الوحيد الذي يقف بين قاعدة بياناتك وسجلات فوترة فاسدة. هذا كيف ننفذ مفاتيح الأمان من التكرار عبر تطبيقات Go SaaS التي تتعامل مع المدفوعات وإنشاء الطلبات وأي عملية يجب أن تُكمَّل مرة واحدة فقط.

لماذا تحتاج الأنظمة الموزعة إلى idempotency أكثر من القيود الفريدة؟

الإجابة المعتادة لمنع التكرار هي قيد UNIQUE في قاعدة البيانات على مفتاح طبيعي ما. هذا القيد ضروري لكنه غير كافٍ لعمليات SaaS التي تمتد عبر أنظمة متعددة.

تخيّل تدفق دفع يُنشئ طلباً، ويُحمّل بطاقة عبر بوابة دفع، ويُحدّث سجل الاشتراك. إذا نجح تحميل البطاقة لكن الشبكة انقطعت قبل وصول الاستجابة إلى العميل، سيُعيد العميل محاولة العملية بأكملها. قيد UNIQUE على معرّف الطلب يمنع إنشاء الطلب مرتين، لكن بوابة الدفع لا تعلم أن التحميل الأصلي نجح. بدون idempotency على مستوى API، سيُحمَّل العميل مرة ثانية.

تحلّ مفاتيح idempotency هذه المشكلة بتخزين الاستجابة الأصلية لعملية معينة وإعادتها حرفياً في الطلبات اللاحقة التي تحمل نفس المفتاح، دون إعادة تنفيذ العملية. يُنشئ العميل المفتاح (عادةً UUID v4)، ويُرسله في رأس الطلب، ويُخزّنه الخادم مقروناً بالاستجابة.

في منصات SaaS التي تخدم عملاء التجارة الإلكترونية في لبنان والخليج، حيث الاتصال المتقطع واقع يومي، يمنع هذا النمط الرسوم المزدوجة وتكرار الشحنات وتكرار سجلات الفواتير.

كيف تبدو بنية قاعدة البيانات لتتبع idempotency؟

مخزن idempotency يحتاج إلى معلومات كافية لإعادة بناء الاستجابة الأصلية دون إعادة تنفيذ العملية:

CREATE TABLE idempotency_keys (
    key           TEXT        NOT NULL PRIMARY KEY,
    tenant_id     UUID        NOT NULL,
    endpoint      TEXT        NOT NULL,
    response_code INT         NOT NULL,
    response_body JSONB       NOT NULL,
    created_at    TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    expires_at    TIMESTAMPTZ NOT NULL
);

CREATE INDEX idx_idempotency_tenant ON idempotency_keys (tenant_id, expires_at);

المفتاح الأساسي هو قيمة المفتاح الذي يُرسله العميل مباشرةً، لأنه آلية البحث ويحتاج إلى بحث في O(1).

عمود endpoint يُقيّد المفتاح بعملية محددة. مفتاح يُنشأ لنقطة نهاية الدفع لا يجب أن يتطابق مع مفتاح لنقطة نهاية إنشاء الطلب، حتى لو أعاد العميل استخدام نفس UUID بالخطأ.

عمود expires_at يقود عملية التنظيف. مفاتيح idempotency ليست سجلات دائمة. الاحتفاظ بها 24 ساعة يغطي جميع سيناريوهات إعادة المحاولة المعقولة.

كيف تُطبّق هذا في middleware بـ Go؟

يقع فحص idempotency في HTTP middleware يعمل قبل أي منطق handler. يتعامل مع ثلاثة حالات: لا مفتاح (تمرير مباشر)، مفتاح مُشاهَد سابقاً (إعادة الاستجابة المخزنة)، ومفتاح جديد (التسجيل ثم التنفيذ ثم التخزين).

عدم تخزين استجابات 5xx هو قرار متعمد. خطأ الخادم يعني أن العملية قد لا تكون اكتملت، لذا يجب السماح للعميل بإعادة المحاولة والتنفيذ من جديد.

كيف تتعامل مع الطلبات المتزامنة التي تحمل نفس المفتاح؟

السيناريو الخطير: عميل جوال سيء البرمجة يُطلق طلبين متطابقين بنفس مفتاح idempotency بفارق 50 ميلي ثانية. كلاهما يصل قبل أن يُكمل أي منهما التنفيذ. الطلب الأول يُنشئ الطلب ويُحمّل البطاقة. الطلب الثاني، إذا اجتاز فحص idempotency قبل أن يكتب الأول استجابته المخزنة، سيُحاول التنفيذ أيضاً.

الحل هو قفل على مستوى قاعدة البيانات باستخدام صف مفتاح idempotency نفسه. الطلب الأول يُدرج الصف ويحتفظ بقفل الصف خلال التنفيذ. الطلب الثاني يحاول إدراج نفس المفتاح فيُحجب. عندما يُودع الطلب الأول استجابته المخزنة، يقرأ الطلب الثاني القيمة المخزنة ويُعيدها.

هذا النمط يُسلسل التكرارات المتزامنة باستخدام قاعدة البيانات التي هي أصلاً مصدر الحقيقة لحالة idempotency.

ما سياسة TTL والتنظيف الصحيحة؟

اضبط TTL بناءً على نافذة إعادة المحاولة الواقعية للعملاء. عميل جوال يُعيد محاولة الدفع ثلاث مرات على مدى 30 ثانية يحتاج إلى TTL مدته 15 دقيقة. نظام تسليم webhook يُعيد المحاولة على مدى 24 ساعة يحتاج TTL مطابقاً.

التنظيف مهمة خلفية تعمل كل ساعة تحذف المفاتيح المنتهية الصلاحية. إذا كانت المنصة تتعامل مع حجم كافٍ يجعل التنظيف يحذف عشرات الآلاف من الصفوف كل ساعة، قسّم الجدول حسب نطاق expires_at.

أبرز الدروس من بيئة الإنتاج

لا تقتصر مفاتيح idempotency على المدفوعات فقط. أي عملية تُنشئ سجلات أو تُرسل إشعارات أو تُعدّل أنظمة خارجية تستفيد من هذا النمط. إنشاء الاشتراكات ودعوات أعضاء الفريق وتسجيل webhooks كلها عمليات تُسبب ألماً حقيقياً للمستخدمين عند تكرارها.

سجّل كل استجابة مُعادة مع معرّف المستأجر وبيانات الطلب الأصلي. دفعة أُعيدت ثلاث مرات في خمس دقائق إشارة على مشكلة في تكامل العميل، وتريد رؤيتها في سجلاتك قبل أن يتصل العميل بخط الدعم.

aختبر سيناريو التكرار المتزامن بشكل صريح في مجموعة الاختبارات. هذا السيناريو هو الذي يتخطاه المطورون في الاختبار، وهو أيضاً الذي يتسبب في حوادث الإنتاج.

العودة إلى المدونة
Chat on WhatsApp