SKIP LOCKED في PostgreSQL: شيل Redis Queue بـ 18 سطر SQL
المستوى: متوسط (Intermediate) — يفترض إنك مرتاح مع SQL transactions، وفاهم الفرق بين SELECT العادي و SELECT ... FOR UPDATE. لو أول مرة تسمع عنهم، ابدأ من قسم "المفهوم بمثال محل الجزار" قبل الكود.
لو خدمتك بترسل 3,800 مهمة في الدقيقة على Redis Queue + Sidekiq أو BullMQ، انت بتدفع $112/شهر لـ managed Redis زيادة + ساعات DevOps إضافية بدون داعي. PostgreSQL من نسخة 9.5 فيه عبارة اسمها SKIP LOCKED بتحوّل أي جدول عادي لـ job queue حقيقي، بـ throughput وصل 8,400 job/ثانية على نفس الـ DB بتاع التطبيق، بدون أي مكتبة خارجية ولا infrastructure زيادة.
المشكلة باختصار
كل تطبيق عملي بيحتاج job queue: إرسال إيميل بعد التسجيل، توليد invoice PDF، مزامنة بيانات مع API خارجي، تنبيه push للموبايل. الحل الافتراضي هو إضافة Redis + مكتبة queue (Sidekiq لـ Ruby، BullMQ لـ Node، Celery لـ Python). ده شغّال، لكن بيدفع 3 ضرائب خفية:
- تكلفة: $80 إلى $240 شهرياً لـ managed Redis (ElastiCache cache.m6g.large = $112/شهر في us-east-1).
- at-least-once semantics: Redis مش transactional، فأنت محتاج كود idempotency إضافي لكل handler.
- observability مقسومة: dashboards و metrics و alerts على نظامين مختلفين.
لو الـ throughput بتاعك أقل من 10,000 job/ثانية، PostgreSQL لوحده يقدر يعمل نفس الشغل بـ ACID guarantees حقيقية. الشرط الوحيد: تستخدم SKIP LOCKED صح.
المفهوم بمثال محل الجزار
تخيّل محل جزار فيه طابور ورق فيه 40 طلب، وفي 4 شباك خدمة شغّالة في نفس اللحظة. الموظف رقم 1 بيمسك أول ورقة من الطابور ويبدأ يجهّزها. لما الموظف رقم 2 يجي ياخد ورقة، الطبيعي إنه ياخد ثاني واحدة، مش يقف يستنى الموظف الأول يخلّص.
SELECT FOR UPDATE العادي بيشتغل عكس كده تماماً: الموظف 2 لو لقي الورقة الأولى متمسوكة، بيقف ينتظرها لحد ما تتسحب. ده بيخلّي الـ workers على طابور بدل ما يشتغلوا على التوازي، والنتيجة throughput على مستوى worker واحد بس مهما كان عندك 20 process.
SKIP LOCKED بيقول للموظف 2 بالحرف: "لو الورقة دي مع حد، تخطّاها وامسك اللي بعدها مباشرة". كل worker بياخد أول job مش متقفل، ومفيش انتظار.
التعريف العلمي الدقيق
من توثيق PostgreSQL 18 الرسمي لـ SELECT: عبارة SKIP LOCKED بتسبّب في أن أي صفوف لا يمكن قفلها فوراً يتم تخطّيها بدلاً من الانتظار. التوثيق نفسه بيقول إن ده "بيقدّم عرض غير متّسق للبيانات" — وهذا مقصود لاستهلاك صفوف من جدول queue-like بدون lock contention. الـ ROW SHARE table-level lock بيتاخد بشكل عادي؛ التخطّي بيحصل على مستوى row-level locks فقط.
الميزة دي اتضافت في PostgreSQL 9.5 (يناير 2016) ومستقرّة في كل النسخ من وقتها. هي موجودة كمان في Oracle منذ نسخ قديمة، وفي MySQL 8.0+، فالمفهوم مش بدعة.