System Design

هندسة أنظمة إشعارات قوية قابلة للتوسع

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

المعمارية الأساسية: الفصل عبر طوابير الرسائل

المبدأ الأساسي لنظام إشعارات قابل للتوسع هو الفصل. يجب ألا توقف منطق التطبيق الذي يولّد حدثاً (مثل "تم شحن الطلب") على آلية التوصيل. بدلاً من ذلك، يجب أن ينشر الحدث إلى وسيط رسائل، مثل Apache Kafka أو RabbitMQ أو Amazon SNS. يضمن ذلك بقاء التطبيق الرئيسي يعمل بشكل سليم حتى لو تعطلت خدمة البريد الإلكتروني.

فكّر في مقتطف الشيفرة الزائفة التالي الذي يوضح كيف ينشر خدمة تطبيق حدثاً دون توقف:

function onOrderShipped(orderId, trackingId) {
    const event = {
        type: "ORDER_SHIPPED",
        payload: { orderId, trackingId },
        timestamp: Date.now()
    };

    // إرسال ونسيان إلى طابور الرسائل
    messageBroker.publish("notification-events", event);
    
    // تسجيل للرقابة المراقبة دون انتظار اكتمال التوصيل
    logger.info("Notification event queued", { orderId });
}

التوصيل عبر قنوات متعددة وتحديد الأولويات

يتوقع المستخدمون الإشعارات عبر قنوات متعددة: البريد الإلكتروني، الرسائل النصية القصيرة (SMS)، الإشعارات الدافعة، والإشعارات داخل التطبيق. لكل قناة متطلبات مختلفة لزمن الاستجابة وهيكلية تكاليف. عادةً ما تكون الإشعارات الدافعة والرسائل داخل التطبيق فورية ومنخفضة التكلفة، بينما تكون الرسائل النصية القصيرة عالية التكلفة، والبريد الإلكتروني منخفض التكلفة لكن بزمن استجابة أعلى.

لإدارة ذلك، تحتاج إلى موجّه إشعارات. تستهلك هذه الخدمة الأحداث من الطابور وتطبّق منطق التوجيه بناءً على تفضيلات المستخدم وشدة الحدث. يجب أن تتجاوز الأحداث الحرجة (مثل "رمز المصادقة الثنائية") الطوابير القياسية وتستخدم قناة مباشرة ذات أولوية عالية. يمكن تجميع الأحداث غير العاجلة (مثل "متابع جديد") لتقليل استدعاءات واجهات برمجة التطبيقات (API) المزودين الخارجيين.

تنفيذ بنية طابور ذات أولويات أمر جوهري:

class NotificationQueue {
    constructor() {
        this.priorityMap = new Map(); // 1: حرج، 2: عالي، 3: عادي
    }

    enqueue(notification) {
        const priority = this.calculatePriority(notification);
        
        if (!this.priorityMap.has(priority)) {
            this.priorityMap.set(priority, []);
        }

        this.priorityMap.get(priority).push(notification);
    }

    dequeue() {
        // خدمة أعلى أولوية (أصغر رقم) دائماً أولاً
        for (let i = 1; i <= 3; i++) {
            const queue = this.priorityMap.get(i);
            if (queue && queue.length > 0) {
                return queue.shift();
            }
        }
        return null; // الطابور فارغ
    }
}

معالجة الأعطال وقابلية التكرار (Idempotency)

في الأنظمة الموزعة، العطل حتمي. قد تنتهي مهلة خدمة الإشعارات الدافعة، أو قد يرفض بوابة الرسائل النصية القصيرة طلباً. يجب أن يتعامل نظامك مع إعادة المحاولة بسلاسة دون تكرار الرسائل. هنا يأتي دور قابلية التكرار (Idempotency). يجب أن يكون لكل إشعار معرّف فريد. قبل الإرسال، يتحقق النظام من ذاكرة مؤقتة (مثل Redis) للتأكد من أن الإشعار لم تتم معالجته بالفعل.

بالإضافة إلى ذلك، نفّذ تراجعاً أُسّياً (Exponential Backoff) لإعادة المحاولة. إذا فشلت واجهة برمجة تطبيقات خارجية، انتظر ثانية واحدة، ثم ثانيتين، ثم أربع ثوانٍ، قبل إعادة المحاولة. بعد عدد معين من الفشل (مثلاً 5)، انقل الإشعار إلى "طابور الرسائل الميتة" (DLQ) للفحص اليدوي أو التوصيل الاحتياطي (مثلاً، إذا فشلت الرسالة النصية القصيرة، جرّب البريد الإلكتروني).

الخلاصة

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

Share: