يمكن لـ PostgreSQL LISTEN/NOTIFY تشغيل لوحات معلومات وتنبيهات حيّة بإعداد بسيط. تعرّف أين يناسب، حدوده، ومتى تضيف وسيط رسائل.

"التحديثات الحيّة" في واجهة المنتج عادة تعني أن الشاشة تتغير بعد حدوث شيء ما مباشرةً، بدون أن يضغط المستخدم على تحديث. رقم يزيد في لوحة، شارة حمراء تظهر في صندوق الوارد، مسؤول يرى طلبًا جديدًا، أو نافذة منبثقة تقول "نجح البناء" أو "فشل الدفع". المفتاح هنا هو التوقيت: يبدو الأمر فورياً حتى لو استغرق ثانية أو ثانيتين.
تبدأ فرق كثيرة بالاستطلاع: المتصفح يسأل الخادم "هل هناك شيء جديد؟" كل عدة ثوانٍ. الاستطلاع يعمل، لكنه له سلبيتان شائعتان.
أولًا، يبدو بطيئًا لأن المستخدم يرى التغيير فقط في استدعاء الاستطلاع التالي.
ثانيًا، قد يصبح مكلفًا لأنك تُجري فحوصات متكررة حتى عندما لا يتغير شيء. اضرب ذلك في آلاف المستخدمين فيتحول إلى ضجيج.
PostgreSQL LISTEN/NOTIFY موجود لحالة أبسط: "أخبرني عندما تغيّر شيء". بدلًا من السؤال مرارًا وتكرارًا، يمكن لتطبيقك الانتظار والتفاعل عندما ترسل القاعدة إشارة صغيرة.
يناسب هذا السيناريو واجهات حيث يكفي التنبيه البسيط. على سبيل المثال:
المقابل هنا هو البساطة مقابل الضمانات. LISTEN/NOTIFY سهل الإضافة لأنه موجود بالفعل في Postgres، لكنه ليس نظام رسائل كامل. الإشعار هو تلميح، ليس سجلًا دائمًا. إذا انقطع المستمع فقد يفوته الإشارة.
طريقة عملية لاستخدامه: دع NOTIFY يوقظ تطبيقك، ثم يقرأ تطبيقك الحقيقة من الجداول.
فكّر في LISTEN/NOTIFY كجرس باب بسيط مدمج في قاعدة البيانات. يمكن لتطبيقك الانتظار لقرع الجرس، وجزء آخر من النظام يقراه عندما يتغير شيء.
للإشعار جزآن: اسم القناة وحمولة اختيارية. القناة تشبه تسمية الموضوع (مثلاً orders_changed). الحمولة رسالة نصية قصيرة تلحق بها (مثلاً معرف الطلب). PostgreSQL لا يفرض بنية، لذلك فرق كثيرة ترسل سلاسل JSON صغيرة.
يمكن تشغيل الإشعار من كود التطبيق (خادم الـ API ينفّذ NOTIFY) أو من داخل القاعدة باستخدام تريجر (تريجر ينفّذ NOTIFY بعد إدراج/تحديث/حذف).
على جهة الاستقبال، يفتح خادم التطبيق اتصالًا بقاعدة البيانات وينفّذ LISTEN channel_name. يبقى هذا الاتصال مفتوحًا. عندما يحدث NOTIFY channel_name, 'payload'، تدفع PostgreSQL رسالة إلى كل الاتصالات المستمعة على تلك القناة. يتفاعل تطبيقك بعد ذلك (تحديث الكاش، جلب الصف المتغيّر، دفع حدث WebSocket إلى المتصفح، وهكذا).
من الأفضل فهم NOTIFY كإشارة لا كخدمة توصيل:
باستخدامها بهذه الطريقة، يمكن لـ PostgreSQL LISTEN/NOTIFY تشغيل تحديثات واجهة المستخدم الحيّة دون إضافة بنية تحتية إضافية.
يتفوّق LISTEN/NOTIFY عندما تحتاج الواجهة فقط إلى تلميح أن شيئًا ما تغيّر، وليس إلى تيار أحداث كامل. فكّر بـ"أعد تحميل هذه القطعة" أو "وصل عنصر جديد" بدلاً من "عالج كل نقرة بالترتيب".
يناسب عندما تكون قاعدة البيانات بالفعل مصدر الحقيقة وتريد إبقاء الواجهة متزامنة معها. نمط شائع: اكتب الصف، أرسل إشعارًا صغيرًا يحتوي معرفًا، وتقوم الواجهة (أو واجهة برمجة التطبيقات) بجلب الحالة الأحدث.
عادةً ما يكون LISTEN/NOTIFY كافياً عندما تكون معظم هذه الشروط صحيحة:
مثال ملموس: لوحة دعم داخلية تعرض "التذاكر المفتوحة" وعداد "ملاحظات جديدة". عندما يضيف وكيل ملاحظة، يكتب خادمك ذلك في Postgres ويفعل NOTIFY للقناة ticket_changed مع معرف التذكرة. يتلقاها المتصفح عبر اتصال WebSocket ويعيد جلب بطاقة تلك التذكرة. لا بنية تحتية إضافية، والواجهة تبدو حيّة.
قد يعطي LISTEN/NOTIFY انطباعًا جيدًا في البداية، لكنه له حدود صلبة. تظهر هذه الحدود عندما تعامل الإشعارات كنظام رسائل بدلًا من نقرات خفيفة على الكتف.
الفجوة الأكبر هي المتانة. NOTIFY ليس مهمة مؤجلة. إذا لم يكن هناك أحد يستمع في تلك اللحظة، تُفقد الرسالة. حتى عندما يكون المستمع متصلاً، قد تؤدي الأعطال أو النشر أو مشاكل الشبكة أو إعادة تشغيل قاعدة البيانات إلى فقدان الاتصال. لن تستعيد تلقائيًا الإشعارات التي فاتتك.
الانقطاعات مؤلمة خصوصًا للميزات الموجّهة للمستخدم. تخيّل لوحة تعرض طلبات جديدة. تبقى علامة تبويب المتصفح في وضع السكون، يعيد WebSocket الاتصال، وتبدو الواجهة "مجمدة" لأنها فاتتها أحداث. يمكنك العمل حول هذا، لكن الحل لن يكون مجرد LISTEN/NOTIFY: ستعيد بناء الحالة باستعلام القاعدة واستخدام NOTIFY كتلميح فقط.
التوزيع على المستمعين مشكلة شائعة أخرى. حدث واحد قد يوقظ مئات أو آلاف المستمعين (عدة خوادم تطبيق، الكثير من المستخدمين). إذا استخدمت قناة صاخبة واحدة مثل orders، يستيقظ كل مستمع حتى لو اهتم مستخدم واحد فقط. يمكن أن يخلق ذلك انفجارًا في استهلاك CPU والاتصالات في أسوأ الأوقات.
حجم الحمولة والتكرار فخاخ نهائية. حمولات NOTIFY صغيرة، والأحداث المتكررة قد تتراكم أسرع مما يمكن للعملاء معالجتها.
انتبه إلى هذه العلامات:
في تلك النقطة، احتفظ بـ NOTIFY كـ"نقرة" وانتقل بالموثوقية إلى جدول أو وسيط رسائل مناسب.
نمط موثوق مع LISTEN/NOTIFY هو معاملته كتلميح، لا كمصدر الحقيقة. صف القاعدة هو الحقيقة؛ الإشعار يخبر تطبيقك متى ينظر.
قم بالكتابة داخل معاملة، وأرسل الإشعار فقط بعد التزام التغيير. إذا أنذرت مبكرًا قد يستيقظ العميل ولا يجد البيانات بعد.
إعداد شائع هو تريجر يفعل بعد INSERT/UPDATE ويُرسل رسالة صغيرة.
NOTIFY dashboard_updates, '{\\\"type\\\":\\\"order_changed\\\",\\\"order_id\\\":123}'::text;
تعمل تسمية القنوات بشكل أفضل عندما تطابق كيفية تفكير الناس بالنظام. أمثلة: dashboard_updates، user_notifications، أو لكل مستأجر مثل tenant_42_updates.
احتفظ بالحمولة صغيرة. ضع معرّفات ونوع الحدث، لا السجلات كاملة. شكل مفيد افتراضيًا هو:
type (ماذا حدث)id (ما تغير)tenant_id أو user_id اختياريهذا يقلل عرض النطاق ويتجنب تسريب بيانات حساسة في سجلات الإشعارات.
الروابط تنقطع. خطط لذلك.
عند الاتصال، نفّذ LISTEN لكل القنوات المطلوبة. عند الانقطاع، أعد الاتصال مع تأخير قصير. عند إعادة الاتصال، نفّذ LISTEN مجددًا (الاشتراكات لا تستمر). بعد إعادة الاتصال، قم بإعادة جلب "التغييرات الأخيرة" بسرعة لتغطية الأحداث الفائتة.
لأغلب تحديثات الواجهة الحيّة، إعادة الجلب هي الأضمن: يستقبل العميل {type, id} ثم يطلب من الخادم الحالة الأحدث.
الترقيعات التدريجية قد تكون أسرع، لكنها أسهل في الخطأ (أحداث خارج الترتيب، فشل جزئي). وسط جيد هو: أعد جلب شرائح صغيرة (صف طلب واحد، بطاقة تذكرة واحدة، عدد الشارات) واترك المجاميع الثقيلة على مؤقت قصير.
عندما تنتقل من لوحة إدارية واحدة إلى العديد من المستخدمين الذين يراقبون نفس الأرقام، تصبح العادات الجيدة أهم من SQL الذكي. يمكن أن يظل LISTEN/NOTIFY مناسبًا، لكن عليك تشكيل كيفية تدفق الأحداث من القاعدة إلى المتصفحات.
خط أساس شائع: كل نسخة تطبيق تفتح اتصالًا طويلًا واحدًا تستمع، ثم تدفع التحديثات إلى العملاء المتصلين. هذا "مستمع واحد لكل نسخة" بسيط وغالبًا ما يكفي إذا كان لديك عدد صغير من خوادم التطبيق ويمكنك تحمل انقطاعات عرضية.
إذا كان لديك نسخ تطبيق كثيرة (أو عمال serverless)، قد يكون خدمة مستمعة مشتركة أسهل. عملية صغيرة تستمع مرة واحدة ثم توزع التحديثات إلى بقية البنية. تعطيك مكانًا واحدًا لإضافة التجميع، المقاييس والتحكم في الضغط.
للمتصفحات عادةً تدفع عبر WebSockets (ثنائي الاتجاه، رائع للواجهات التفاعلية) أو Server-Sent Events (SSE) (اتجاه واحد، أبسط للوحـات). بأي طريقة كانت، تجنّب إرسال "أعد تحديث الكل". أرسل إشارات مضغوطة مثل "الطلب 123 تغيّر" حتى يعيد الواجهة جلب ما تحتاجه فقط.
لحماية الواجهة من الاهتزاز، أضف بعض الحواجز:
تصميم القنوات مهم أيضًا. بدلاً من قناة عالمية واحدة، قسّم حسب المستأجر، الفريق، أو الميزة حتى يستقبل العملاء الأحداث ذات الصلة فقط. مثال: notify:tenant_42:billing وnotify:tenant_42:ops.
يبدو LISTEN/NOTIFY بسيطًا، ولهذا ترسله الفرق بسرعة ثم تُفاجأ في الإنتاج. معظم المشاكل تأتي من معاملته كقائمة رسائل مضمونة.
إذا أعاد تطبيقك الاتصال (نشر، عطل شبكة، فشل قاعدة)، أي NOTIFY أرسلت وأنت غير متصل تضيع. الحل هو اعتبار الإشعار إشارة ثم إعادة فحص القاعدة.
نمط عملي: خزّن الحدث الحقيقي في جدول (مع معرف وcreated_at) ثم عند إعادة الاتصال جلب كل ما هو أحدث من آخر معرف شاهدته.
حمولات LISTEN/NOTIFY ليست مهيأة لقطع JSON كبيرة. الحمولة الكبيرة تزيد العمل، المعالجة، ومخاطر الاصطدام بالحدود.
استخدم الحمولة كتلميح صغير مثل order:123. بعد ذلك يقرأ التطبيق الحالة الأحدث من القاعدة.
خطر شائع هو بناء الواجهة حول محتوى الحمولة كما لو كانت مصدر الحقيقة. هذا يجعل تغييرات المخطط وإصدارات العملاء مؤلمة.
حافظ على فصل واضح: نبه أن شيئًا تغيّر، ثم جلب البيانات الحالية باستعلام عادي.
تريجرات تفعل NOTIFY على كل تغيير صف قد تغمر نظامك، خصوصًا للجداول المشغولة.
أعلِن فقط عند انتقالات ذات معنى (مثلاً تغيّر الحالة). إذا كانت التحديثات صاخبة جدًا، جدولة الإشعارات (NOTIFY واحد لكل معاملة أو نافذة زمنية) أو اخرج تلك التحديثات من مسار الإشعار.
حتى لو كانت القاعدة ترسل إشعارات، واجهتك لا تزال قد تختنق. لوحة تعيد العرض على كل حدث قد تتجمد.
ألغِ التكرار على العميل، ادمج الانفجارات في تحديث واحد، وفضّل "بطلان وإعادة جلب" بدلًا من تطبيق كل دلتا. مثال: يمكن لجرس الإشعارات أن يتحدّث فورًا، لكن قائمة المنسدلة يمكن تحديثها مرة كل بضع ثوانٍ كحد أقصى.
LISTEN/NOTIFY مفيد عندما تريد إشارة صغيرة "تغيّر شيء" ليعيد التطبيق جلب البيانات الطازجة. ليس نظام رسائل كامل.
قبل أن تبني الواجهة حوله، أجب عن هذه الأسئلة:
قاعدة عملية: إذا يمكنك اعتبار NOTIFY كتلميح ("أعد قراءة الصف") بدلًا من الحمولة نفسها، فأنت في المنطقة الآمنة.
مثال: لوحة إدارية تعرض الطلبات الجديدة. إذا فاتك إشعار، الاستطلاع التالي أو تحديث الصفحة يُظهر العدد الصحيح — هذا مناسب. لكن إذا كنت ترسل أحداثًا مثل "خصم البطاقة" أو "شحن الطرد" وفقدان واحد يُسبب حادثًا، فالأمر لا يصلح.
تخيل تطبيق مبيعات صغير: لوحة تعرض عائدات اليوم، إجمالي الطلبات، وقائمة "الطلبات الأخيرة". في الوقت نفسه، يجب أن يتلقى كل مندوب إشعارًا سريعًا عند دفعة أو شحن لطلب يملكه.
نهج بسيط هو اعتبار PostgreSQL مصدر الحقيقة، واستخدام LISTEN/NOTIFY كتنبيه فقط. عند إنشاء طلب أو تغيير حالته، يقوم الخادم بخطوتين في طلب واحد: يكتب الصف (أو يحدثه) ثم يرسل NOTIFY بحمولة صغيرة (غالبًا مع معرّف الطلب ونوع الحدث). الواجهة لا تعتمد على حمولة NOTIFY كبيانات كاملة.
تدفق عملي يبدو هكذا:
orders_events مع {\\\"type\\\":\\\"status_changed\\\",\\\"order_id\\\":123}.هذا يحافظ على NOTIFY خفيفًا ويقيد الاستعلامات المكلفة.
عندما يزداد المرور تظهر الشقوق: انفجار من الأحداث قد يطغى على مستمع واحد، الإشعارات تُفقد عند إعادة الاتصال، وتحتاج لضمان التسليم وإمكانية إعادة التشغيل. عادةً ما تضيف في هذه النقطة طبقة أكثر موثوقية (جدول outbox بالإضافة إلى عامل، ثم وسيط إذا لزم الأمر) مع الحفاظ على Postgres كمصدر للحقيقة.
LISTEN/NOTIFY ممتاز عندما تحتاج إشارة سريعة "تغيّر شيء". لكنه ليس مُعدًا كنظام رسائل كامل. عندما تبدأ بالاعتماد على الأحداث كمصدر للحقيقة، حان وقت إضافة وسيط.
إذا ظهر أي من هذه، سيوفر لك وسيط الراحة:
LISTEN/NOTIFY لا يخزن الرسائل للوقت اللاحق. إنه إشارة دفع، ليس سجلًا محفوظًا. هذا مثالي لـ "أعد تحميل عنصر لوحة"، لكنه محفوف بالمخاطر لـ "شغّل فاتورة" أو "اشحن الطرد".
الوسيط يمنحك نموذج تدفق رسائل حقيقي: قوائم انتظار (عمل يجب إنجازه)، مواضيع (بث لكثيرين)، احتفاظ (الرسائل تبقى لعدة دقائق إلى أيام)، وإقرارات (المستهلك يؤكد المعالجة). هذا يتيح فصل "قاعدة البيانات تغيّرت" عن "كل ما يجب أن يحدث لأنّها تغيّرت".
لا يلزم اختيار أكثر الأدوات تعقيدًا. الخيارات الشائعة: Redis (pub/sub أو streams)، NATS، RabbitMQ، Kafka. يعتمد الاختيار على إن كنت تحتاج قوائم عمل بسيطة، توزيعًا لعدة خدمات، أو إمكانية إعادة تشغيل السجل.
يمكنك الانتقال بدون إعادة كتابة ضخمة. نمط عملي: احتفظ بـ NOTIFY كتلميح بينما يصبح الوسيط مصدر التسليم.
ابدأ بكتابة "صف حدث" في جدول داخل نفس المعاملة كتغيير العمل، ثم اجعل عاملًا ينشر ذلك الحدث إلى الوسيط. أثناء الانتقال، يمكن لـ NOTIFY أن يخبر طبقة الواجهة "تحقق من الأحداث الجديدة"، بينما تستهلك العمالة من الوسيط مع محاولات وإجراءات تدقيق.
بهذه الطريقة تظل اللوحات سريعة، وتصبح سير الأعمال الحرجة أقل اعتمادًا على إشعارات بأفضل جهد.
اختر شاشة واحدة (بلاطة لوحة، عداد شارة، نافذة إشعار "جديد") واصنعها من الطرف إلى الطرف. مع LISTEN/NOTIFY يمكنك الحصول على نتيجة مفيدة بسرعة، طالما حدّدت النطاق وقيّمت السلوك تحت حمل حقيقي.
ابدأ بالنمط الأبسط والموثوق: اكتب الصف، اكمل المعاملة، ثم انطلق بإشارة صغيرة أن شيئًا تغيّر. في الواجهة، تفاعل مع الإشارة عن طريق جلب الحالة الأحدث (أو الجزء الذي تحتاجه). هذا يحافظ على الحمولات صغيرة ويتجنب الأخطاء الخفية عند وصول الرسائل خارج الترتيب.
أضف قابلية المراقبة مبكرًا. لا تحتاج أدوات فاخرة لكن تحتاج إجابات عندما يصبح النظام صاخبًا:
حافظ على العقود بسيطة ومكتوبة. حدّد أسماء القنوات، أسماء الأحداث، وشكل أي حمولة (حتى لو كانت مجرد معرف). كتالوج أحداث قصير في المستودع يمنع الانحراف.
إذا كنت تبني بسرعة وتريد تقليل المكدس، منصة مثل Koder.ai (koder.ai) يمكن أن تساعدك على إطلاق النسخة الأولى مع واجهة React، خلفية Go، وPostgreSQL، ثم التدرّج عندما تتضح متطلباتك.
استخدم LISTEN/NOTIFY عندما تحتاج فقط إلى إشارة سريعة أن شيئًا ما تغيّر، مثل تحديث عداد شارة أو بلاطة في لوحة المعلومات. اعتبر الإشعار كتلميح لإعادة جلب البيانات الحقيقية من الجداول، لا كمصدر البيانات نفسه.
الاستطلاع (polling) يفحص التغييرات بجدول زمني، لذلك يرى المستخدم التحديثات متأخرة ويجري الخادم عملًا متكررًا حتى عندما لا يحدث شيء. يقوم LISTEN/NOTIFY بدفع إشارة صغيرة فور حدوث التغيير، ما يبدو أسرع عادة ويتجنب الكثير من الطلبات الفارغة.
لا، هو عملٌ بأفضل جهد. إذا كان المستمع غير متصل أثناء NOTIFY فقد يفتقد الإشارة لأن الإشعارات لا تُخزّن للاسترجاع لاحقًا.
احتفظ بها صغيرة واعتبرها تلميحًا. شكل عملي افتراضي هو JSON صغير يحوي type وid، ثم تطلب تطبيقك من Postgres الحالة الحالية.
نمط شائع هو إرسال الإشعار بعد الالتزام بالكتابة. إذا أرسلت الإشعار مبكرًا قد يستيقظ العميل ولا يجد الصف الجديد بعد.
كود التطبيق أسهل للفهم والاختبار لأنه صريح. التريجرات مفيدة عندما يكتب عدة كتاب إلى نفس الجدول وتريد سلوكًا متناسقًا بغض النظر عن الجهة التي أدخلت التغيير.
خطّط لإعادة الاتصال كحالة عادية. عند الاتصال أعد تنفيذ LISTEN للقنوات المطلوبة وعُد سريعًا لإعادة جلب التغييرات الحديثة لتغطية ما فاتك أثناء الانقطاع.
لا تجعل كل متصفح يتصل بPostgres. إعداد نموذجي هو اتصال طويل المدى واحد لكل نسخة تطبيق تستمع ثم يمرّر التحديثات إلى المتصفحات عبر WebSocket أو SSE ويطلب من الواجهة إعادة جلب ما تحتاجه.
استخدم قنوات أضيق حتى يستيقظ المستهلكون الصحيحون فقط، وجمّع الانفجارات الصاخبة. إلغاء التكرار لعدة مئات من المللي ثانية وتجميع التحديثات المكررة يمنع ازدحام الواجهة والخادم.
انتقل عندما تحتاج إلى متانة، إعادة المحاولة، مجموعات مستهلكين، ضمان ترتيب، أو أرشفة وإعادة تشغيل الأحداث. إذا كان فقدان حدث يسبب حادثًا حقيقيًا (مثل فوترة أو شحن)، استخدم جدول outbox مع عامل أو وسيط رسائل بدلاً من الاعتماد على NOTIFY فقط.