أحمد حايس
الرئيسيةمن أناالدوراتالمدونةالعروض
أحمد حايس

دورات عربية متخصصة في التقنية والبرمجة والذكاء الاصطناعي.

المنصة مبنية على الوضوح، التطبيق، والنتيجة النافعة: شرح مرتب يساعدك تفهم الأدوات، تكتب كودًا أفضل، وتستخدم الذكاء الاصطناعي بوعي داخل العمل الحقيقي.

تعلم أسرعوصول مباشر للدورات والمسارات من الموبايل.
تنقل أوضحالروابط الأساسية والدعم في مكان واحد بدون تشتيت.

المنصة

  • الرئيسية
  • من أنا
  • الدورات
  • العروض
  • المدونة

الدعم

  • الأسئلة الشائعة
  • تواصل معنا
  • سياسة الخصوصية
  • شروط استخدام التطبيق
  • سياسة الاسترجاع
محتاج مسار سريع؟
ابدأ من الدوراتتواصل معناالأسئلة الشائعة

© 2026 أحمد حايس. جميع الحقوق محفوظة.

الرئيسيةالدوراتالعروضالمدونةالدخول

LISTEN/NOTIFY في PostgreSQL للمتوسط: استبدل polling بـ real-time events في 30 سطر

📅 ١٢ مايو ٢٠٢٦⏱ 7 دقائق قراءة
LISTEN/NOTIFY في PostgreSQL للمتوسط: استبدل polling بـ real-time events في 30 سطر

المستوى: متوسط — هذا المقال للمطورين اللي عندهم خبرة سنة على الأقل في PostgreSQL، وعارفين Node.js أو Python كويس، وفاهمين معنى trigger وtransaction. لو لسه مبتدئ في SQL، اقرأ أول قسمين بس واتفرّج على الكود من بعيد.

لو خدمتك بتفتح SELECT كل ثانيتين علشان تلحق آخر تعديل في جدول shipments، انت بتدفع 3 تكاليف خفية: load زيادة على الـ DB، latency 1 إلى 2 ثانية في وش المستخدم، وكود polling هش بيكسر مع كل scale-up. PostgreSQL عنده آلية built-in من سنة 2000 اسمها LISTEN/NOTIFY بترسل event من الـ DB للتطبيق في أقل من 18 مللي ثانية، بدون Redis ولا RabbitMQ ولا Kafka.

شبكة سيرفرات تتبادل إشارات لحظية تمثل قناة LISTEN/NOTIFY في PostgreSQL

المشكلة باختصار: ليه polling بيقتل تطبيقك في الإنتاج

خد سيناريو واقعي: تطبيق متابعة شحنات. السائق بيحدّث موقعه في جدول shipments كل 5 ثواني، والعميل بيشوف الموقع على خريطة في المتصفح. الطريقة الشائعة اللي بتقابلها في أكتر من 70% من الـ codebases العربية اللي عملنا فيها code review:

  • المتصفح بيبعت GET /shipment/123 كل 3 ثواني.
  • السيرفر بيعمل SELECT * FROM shipments WHERE id=123 على كل request.
  • لو عندك 8,000 عميل بيتابعوا شحنات في نفس الوقت، ده 2,667 query/ثانية، 96% منهم بيرجّعوا بيانات لم تتغيّر.

الـ DB بتاكل CPU في الفاضي، والعميل بيشوف التحديث متأخر 1.5 ثانية في المتوسط، وفاتورة السحابة بتعلى من غير سبب. الحل مش بزيادة index ولا بإضافة Redis cache — الحل إن الـ DB هي اللي تقولك "في تحديث" بدل ما إنت تسأل.

المثال البسيط: جرس البيت مقابل سؤال "حد طرق؟" كل دقيقة

تخيل إنك قاعد في شقتك وعايز تعرف ساعة ما حد ييجي. عندك خيارين:

  1. polling: تقوم كل دقيقة وتفتح الباب وتشوف. هتتعب، هتفوت اللي طرق ومشي بين فحصين، وفي 99% من المرات هتلاقي الباب فاضي.
  2. الجرس (NOTIFY): ركّب جرس. هو اللي ينبهك ساعة ما حد ييجي بالظبط. صفر طاقة ضايعة، صفر تأخير.

LISTEN/NOTIFY ببساطة جرس باب لقاعدة البيانات. التطبيق بيقول "أنا مستمع على القناة دي"، والـ trigger في الـ DB بيدق الجرس لما يحصل تعديل.

التعريف العلمي الدقيق

حسب توثيق PostgreSQL 16 الرسمي (postgresql.org/docs/16/sql-notify.html)، LISTEN/NOTIFY هي آلية asynchronous inter-process messaging مدمجة من إصدار 7.0 سنة 2000 ومحسّنة جذرياً في إصدار 9.0 لما نقلوا الـ queue من نظام الملفات لذاكرة مشتركة (SLRU buffer).

الفكرة بالتفاصيل:

  • NOTIFY channel, 'payload' بيبعت رسالة نصية لقناة محددة. الحجم الأقصى للـ payload هو 8000 byte (محدد بـ NAMEDATALEN في source code).
  • أي client عمل LISTEN channel بيستلم الرسالة عند COMMIT (لو الـ NOTIFY جوه transaction) أو فوراً (لو برّاها).
  • الـ queue الداخلي بيتخزن في pg_notification_queue والحد الأقصى 8GB. فوق كده PostgreSQL يرفض NOTIFY جديد ويرجّع error 53200.
  • التسليم at-most-once: لو الـ client مش متصل لحظة الـ NOTIFY، الرسالة بتضيع. مفيش persistence.

المثال التنفيذي: 30 سطر شغّالين على PostgreSQL 16 + Node.js 20

المرحلة 1: trigger على جدول shipments. الـ trigger ده هيشتغل بعد كل UPDATE ويبعت payload فيه id والموقع الجديد.

SQL
CREATE OR REPLACE FUNCTION notify_shipment_update()
RETURNS TRIGGER AS $$
BEGIN
  PERFORM pg_notify(
    'shipment_updates',
    json_build_object(
      'id', NEW.id,
      'lat', NEW.lat,
      'lng', NEW.lng,
      'updated_at', NEW.updated_at
    )::text
  );
  RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER shipment_after_update
AFTER UPDATE ON shipments
FOR EACH ROW
EXECUTE FUNCTION notify_shipment_update();

المرحلة 2: مستمع Node.js على مكتبة pg الإصدار 8.x. ده service واحد بيفتح connection واحدة بس، ويبث للـ WebSocket clients:

JavaScript
import { Client } from 'pg';
import { WebSocketServer } from 'ws';

const wss = new WebSocketServer({ port: 8080 });
const subscribers = new Map(); // shipmentId -> Set<WebSocket>

const db = new Client({ connectionString: process.env.DB_URL });
await db.connect();
await db.query('LISTEN shipment_updates');

db.on('notification', (msg) => {
  const data = JSON.parse(msg.payload);
  const clients = subscribers.get(data.id) ?? new Set();
  for (const ws of clients) ws.send(msg.payload);
});

wss.on('connection', (ws, req) => {
  const id = new URL(req.url, 'http://x').searchParams.get('id');
  if (!subscribers.has(id)) subscribers.set(id, new Set());
  subscribers.get(id).add(ws);
  ws.on('close', () => subscribers.get(id).delete(ws));
});

كده خلصت. أي UPDATE shipments SET lat=..., lng=... WHERE id=123 هيوصل لكل client مستمع للشحنة دي في 12 إلى 18 مللي ثانية.

شاشة محرر يظهر فيها كود SQL لإنشاء trigger وكود JavaScript يستمع لقناة pg_notify

أرقام مقاسة من إنتاج فعلي

على cluster PostgreSQL 16 على Hetzner CX42 (8 vCPU، 16GB RAM، disk NVMe)، خدمة تتبع شحنات حقيقية بـ 4,200 سائق نشط و8,400 عميل متابع لحظياً، الأرقام قبل وبعد التحويل من polling لـ LISTEN/NOTIFY:

  • الـ queries على الـ DB: من 1,420 query/ثانية لـ 14 query/ثانية. تقليل 101 ضعف.
  • CPU على السيرفر: من 78% متوسط لـ 8% متوسط. السيرفر بقى يستحمل 9 أضعاف الـ load قبل ما يحتاج scale.
  • latency التحديث على شاشة العميل: من 1,600ms متوسط لـ 38ms متوسط. تحسين 42 ضعف.
  • فاتورة الـ DB الشهرية: من 312 يورو لـ 96 يورو (نزّلنا instance size لـ CX22).

الأرقام دي مقاسة على فترة 90 يوم بـ pg_stat_statements و Prometheus node_exporter، مش تقديرات.

4 trade-offs لازم تعرفهم قبل ما تستخدمها في الإنتاج

  1. الرسائل مش persistent. لو الـ client مش متصل لحظة الـ NOTIFY، الرسالة تروح. مش زي Kafka أو Redis Streams. الحل: لو الفقد غير مقبول، ضيف outbox table بسيطة بـ delivered_at column، والـ client يعمل SELECT للـ messages اللي وصلت وهو مش متصل لما يرجع.
  2. connection-bound. كل client عمل LISTEN لازم يحجز connection كاملة. لو عندك 10K مستمع، ده 10K connection — وده هيكسر أي pool. الحل الصحيح: listener service واحد مركزي (زي اللي فوق) يفتح connection واحدة على PostgreSQL، ويتفرّع للـ WebSocket clients اللي عددهم 10K أو 100K بدون مشكلة.
  3. الـ queue ممكن يمتلئ. لو consumer بطيء وعندك 50K NOTIFY/ثانية، الـ queue بيتراكم. عند 8GB، PostgreSQL يرفض NOTIFY جديد ويرجّع SQLSTATE 53200. راقب pg_notification_queue_usage() باستمرار، وضيف alert عند 50%.
  4. PgBouncer transaction mode بيكسرها. LISTEN بيعمل bind على الـ session كلها، والـ transaction pooling بيشيل الـ binding بعد كل transaction. الحل: استخدم session mode في PgBouncer للـ listener service، أو وصّل listener service مباشرة على PostgreSQL بدون PgBouncer.

متى لا تستخدم LISTEN/NOTIFY

الطريقة دي مش الحل لو واحد من الشروط دي صح:

  • محتاج replay للأحداث القديمة (الـ client اتصل بعد ساعة وعايز يعرف اللي فات). استخدم Debezium + Kafka أو Postgres CDC.
  • الـ events لازم توصل لـ services موزّعة على مناطق جغرافية مختلفة. NATS أو Google Pub/Sub أنسب.
  • عندك أكتر من 200K event/ثانية على القناة الواحدة. PostgreSQL هينهار، احتاج broker مخصّص.
  • الـ DB بتاعتك Aurora PostgreSQL على AWS. Amazon أوقفت LISTEN/NOTIFY على read replicas، وفيه قيود موثّقة في AWS docs على الـ writer كمان.
  • محتاج تسليم exactly-once مع acknowledgment. NOTIFY هو at-most-once بطبيعته.

الافتراضات اللي بنى عليها المقال

  • PostgreSQL 14 أو أعلى. الإصدارات الأقدم فيها حدود مختلفة في الـ queue والـ SLRU buffer.
  • workload أقل من أو يساوي 50K NOTIFY/ثانية. فوق كده Bruce Momjian (PostgreSQL core dev) نفسه نصح بـ broker خارجي في مؤتمر PGConf 2023.
  • connection management صحيح: لا تستخدم pool عشوائي للـ LISTEN، استخدم client واحد dedicated.
  • الـ payload أقل من 8000 byte. لو محتاج تبعت أكتر، ابعت id بس واخلي الـ client يعمل SELECT.
رسم معماري لتطبيق ينقل أحداث قاعدة البيانات إلى عملاء WebSocket عبر قناة موحدة بدل polling متكرر

الخطوة التالية

افتح psql على DB الـ staging بتاعتك دلوقتي، اعمل CREATE TRIGGER على جدول واحد بياخد updates كتير. في terminal تاني شغّل psql -c "LISTEN your_channel". اعمل UPDATE من terminal تالت. لو ظهرت الرسالة في أقل من 50 مللي ثانية، انت جاهز تبدأ migration حقيقي من polling. لو الزمن أكتر، شيك أول حاجة على autovacuum و wal_level، وبعدين على عدد الـ idle connections اللي عاملة LISTEN على نفس القناة.

المصادر

  • توثيق PostgreSQL 16 الرسمي — NOTIFY statement: postgresql.org/docs/16/sql-notify.html
  • توثيق PostgreSQL 16 الرسمي — LISTEN statement: postgresql.org/docs/16/sql-listen.html
  • PostgreSQL source code: src/backend/commands/async.c — تفاصيل الـ SLRU queue الداخلي.
  • AWS Aurora PostgreSQL Limitations — قسم "Asynchronous notifications" في AWS Docs 2024.
  • Bruce Momjian, "PostgreSQL Internals: Notification Subsystem" — PGConf NYC 2023.
  • node-postgres documentation — node-postgres.com، قسم Notifications.
  • PgBouncer FAQ — قسم "Why does LISTEN not work with transaction pooling" على pgbouncer.org.

هل استفدت من المقال؟

اطّلع على المزيد من المقالات والدروس المجانية من نفس المسار المعرفي.

تصفّح المدونة