إعداد Laravel Queue مع Redis و Horizon خطوة بخطوة
دليل عملي لإعداد Laravel Queue مع Redis وHorizon، وإنشاء Jobs ومراقبتها وضبط المحاولات والمهل وتشغيل العمال بأمان في الإنتاج.

لماذا تستخدم Laravel Queue مع Redis وHorizon؟
عندما يرسل تطبيقك بريدًا إلكترونيًا أو يعالج ملفًا كبيرًا داخل طلب HTTP، ينتظر المستخدم انتهاء العمل قبل مشاهدة الاستجابة. يتيح Laravel Queue نقل هذه الأعمال إلى مهام خلفية، فينتهي الطلب بسرعة بينما ينفذ عامل مستقل المهمة لاحقًا. هذا الفصل لا يجعل العملية نفسها أسرع بالضرورة، لكنه يحسن تجربة الاستخدام ويمنحك تحكمًا أفضل في الحمل والأخطاء.
يعمل Redis هنا بوصفه مخزنًا للطوابير، بينما يدير Laravel Horizon العمال ويعرض حالة التنفيذ والمهام الفاشلة ومؤشرات التشغيل. في هذا الدليل سنبني مثالًا لإرسال رسالة ترحيب، ثم نضبط المحاولات والمهل والحماية والنشر. نفترض وجود مشروع Laravel حديث وقاعدة بيانات عاملة، وأن التنفيذ والإنتاج على نظام يدعم عمليات Horizon، مثل Linux.
1. تجهيز Redis والاتصال من Laravel
ابدأ بالتأكد من تشغيل Redis وإمكانية الوصول إليه من البيئة التي يعمل فيها التطبيق. إذا كنت تستخدم الحاويات، فقد يكون اسم المضيف اسم خدمة Redis، وليس العنوان المحلي. الأمر التالي يفحص الاتصال المحلي، والاستجابة الطبيعية هي PONG. في البيئات المحمية مرر إعدادات المصادقة المناسبة دون حفظ كلمات المرور داخل سجل أوامر مشترك.
redis-cli ping
يحتاج Laravel إلى عميل Redis. يمكنك استخدام امتداد PhpRedis بعد تثبيته في بيئة PHP، أو تثبيت Predis بواسطة Composer. سنستخدم Predis لتبسيط المثال؛ لا تحتاج إلى تثبيت العميلين معًا. تأكد أيضًا من توفر امتدادي pcntl وposix في PHP CLI الذي يشغّل Horizon.
composer require predis/predis
php -m
عدّل ملف البيئة بالقيم المناسبة لخادمك. اسم اتصال Redis داخل إعدادات الطابور يختلف عن اسم برنامج تشغيل الطابور؛ الأول يحدد الخادم، والثاني يحدد آلية التخزين.
QUEUE_CONNECTION=redis
REDIS_CLIENT=predis
REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null
REDIS_PORT=6379
REDIS_DB=0
REDIS_QUEUE_CONNECTION=default
REDIS_QUEUE=default
REDIS_QUEUE_RETRY_AFTER=120
لا تفتح منفذ Redis للعامة. استخدم شبكة خاصة ومصادقة وصلاحيات مناسبة، وفعّل التشفير إذا كانت بنيتك تحتاج إليه. عند مشاركة Redis بين تطبيقات متعددة، اعزل المفاتيح بإعدادات بادئة واضحة، ولا تتعامل مع خادم الطوابير كأنه ذاكرة تخزين مؤقت قابلة للمسح دون تبعات.
2. تثبيت Laravel Horizon ونشر إعداداته
ثبّت Horizon داخل المشروع، ثم شغّل أمر التثبيت لنشر الملفات اللازمة. احتفظ به ضمن اعتماديات التشغيل، وليس اعتماديات التطوير فقط، لأن خادم الإنتاج يحتاج إليه لتشغيل العمال. يدعم Horizon طوابير Redis؛ تغيير اتصال الطابور إلى قاعدة بيانات لا يحقق الإعداد الموضح هنا.
composer require laravel/horizon
php artisan horizon:install
php artisan config:clear
راجع ملف config/horizon.php والمزوّد المنشور في التطبيق. قد تختلف بعض القيم الافتراضية بين الإصدارات، لذلك عدّل الأقسام الموجودة بدل استبدال الملف بالكامل. يزيل أمر مسح الإعدادات النسخة المخبأة محليًا حتى يقرأ التطبيق تغييرات البيئة، وسنعيد بناء الكاش عند النشر.
3. ضبط اتصال الطابور وحماية المهام من التنفيذ المبكر
افتح config/queue.php وتحقق من اتصال redis. نستخدم مهلة إعادة إتاحة تبلغ 120 ثانية، ونفعّل after_commit كي لا تُرسل المهام المرتبطة بمعاملة قاعدة بيانات قبل اعتماد بياناتها. هذا مهم عندما تنشئ مستخدمًا ثم ترسل مهمة تقرأ سجله؛ فقد يبدأ العامل قبل اكتمال المعاملة إن لم تضبط هذا السلوك.
'redis' => [
'driver' => 'redis',
'connection' => env('REDIS_QUEUE_CONNECTION', 'default'),
'queue' => env('REDIS_QUEUE', 'default'),
'retry_after' => (int) env('REDIS_QUEUE_RETRY_AFTER', 120),
'block_for' => 5,
'after_commit' => true,
],
قيمة retry_after ليست وقت الانتظار بين المحاولات؛ إنها مدة اعتبار المهمة المحجوزة قيد التنفيذ قبل السماح بإتاحتها مجددًا إذا لم تُحذف بنجاح. أما block_for فيحدد مدة انتظار اتصال Redis لوصول عمل جديد. الانتظار المحدود يساعد العمال على الاستجابة لإشارات الإيقاف بدل الحجب غير المحدود.
4. إنشاء Job حقيقية لإرسال بريد الترحيب
أنشئ المهمة باستخدام Artisan. يعتمد المثال على نموذج User الافتراضي وحقلي name وemail، كما يفترض تجهيز اتصال البريد. أثناء التجربة يمكنك استخدام MAIL_MAILER=log لتسجيل محتوى الرسالة بدل إرسال بريد فعلي، مع الانتباه إلى أن السجلات قد تحتوي بيانات شخصية.
php artisan make:job SendWelcomeEmail
استبدل محتوى ملف المهمة بالتعريف التالي. نخزن معرّف المستخدم فقط، ثم نجلب بياناته عند التنفيذ. إذا حُذف الحساب قبل معالجة المهمة، ننهيها بهدوء بدل تكرار محاولة إرسال رسالة إلى مستخدم غير موجود.
<?php
namespace App\Jobs;
use App\Models\User;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Mail;
class SendWelcomeEmail implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public int $tries = 3;
public int $timeout = 60;
public function __construct(public int $userId)
{
}
public function backoff(): array
{
return [10, 30, 60];
}
public function handle(): void
{
$user = User::find($this->userId);
if (! $user) {
return;
}
Mail::raw(
'Welcome, '.$user->name.'!',
function ($message) use ($user) {
$message->to($user->email)
->subject('Welcome to our application');
}
);
}
}
وجود ShouldQueue هو ما يجعل المهمة قابلة للتنفيذ عبر الطابور. لا تخزن كلمات مرور أو ملفات ضخمة داخل بيانات المهمة؛ مرر معرّفًا أو مسارًا آمنًا بدلًا منها. واضبط مهل اتصال مزود البريد أيضًا، لأن مهلة العامل ليست بديلًا عن مهل عمليات الشبكة.
5. إرسال المهمة إلى الطابور الصحيح
يمكن إرسال المهمة من المتحكم بعد تسجيل المستخدم، أو من مستمع حدث. للتجربة السريعة افتح Tinker واستخدم مستخدمًا موجودًا. اخترنا طابور emails لتفصيل أعمال البريد عن المهام العامة؛ يجب أن يراقب Horizon هذا الاسم تحديدًا حتى يعالج الرسائل.
php artisan tinker
$user = App\Models\User::query()->firstOrFail();
App\Jobs\SendWelcomeEmail::dispatch($user->id)
->onQueue('emails');
إذا أرسلت المهمة داخل معاملة، فإن after_commit الذي ضبطناه يؤخر إرسالها حتى اعتماد المعاملة، ويتجاهل المهام المؤجلة معها عند التراجع عنها. تجنب dispatchSync في هذا المسار لأنه ينفذ المهمة فورًا داخل العملية الحالية، فلا يحقق هدف نقل العمل خارج الطلب.
6. إعداد مشرف Horizon والمهل
داخل قسم environments في config/horizon.php، اضبط مشرف الإنتاج ليعالج emails وdefault. يحدد connection اتصالًا من config/queue.php، وليس اسم اتصال Redis المباشر من config/database.php. الأرقام التالية نقطة بداية لتطبيق صغير، وليست وصفة ثابتة لجميع الخوادم.
'environments' => [
'production' => [
'supervisor-1' => [
'connection' => 'redis',
'queue' => ['emails', 'default'],
'balance' => 'auto',
'minProcesses' => 1,
'maxProcesses' => 5,
'tries' => 3,
'timeout' => 90,
'memory' => 128,
],
],
'local' => [
'supervisor-1' => [
'connection' => 'redis',
'queue' => ['emails', 'default'],
'balance' => 'auto',
'maxProcesses' => 3,
'tries' => 3,
'timeout' => 90,
],
],
],
في المثال مهلة المهمة 60 ثانية، ومهلة Horizon تساوي 90، وretry_after تساوي 120. اجعل مهلة Horizon أعلى من مهلة المهمة وأقل من retry_after بهامش مناسب. خلاف ذلك قد تصبح المهمة متاحة لعامل آخر بينما التنفيذ الأول لم ينتهِ بعد. وإعداد tries داخل المهمة يتقدم على القيمة العامة للمشرف.
هل ترتيب الطوابير يعني أولوية صارمة؟
لا تعتمد على ترتيب الأسماء مع balance=auto لضمان أولوية مطلقة. يوزع Horizon العمليات بحسب حمل الطوابير. إذا كانت لديك مدفوعات أو إشعارات عاجلة، خصص لها مشرفًا منفصلًا وموارد محسوبة، مع مراقبة استهلاك الذاكرة وحدود الاتصالات بخدماتك الخارجية.
7. تشغيل Horizon وحماية لوحة المراقبة
شغّل Horizon محليًا، ثم افتح مسار /horizon داخل التطبيق. يجب أن ترى المهمة تنتقل إلى الاكتمال عند نجاحها. يعرض أمر الحالة وضع Horizon، لكنه لا يغني عن متابعة معدل الفشل وزمن انتظار المهام فعليًا.
php artisan horizon
php artisan horizon:status
نفذ أمر الحالة في طرفية أخرى لأن أمر التشغيل يبقى في الواجهة. في الإنتاج، لا تجعل لوحة المراقبة عامة؛ فهي تعرض تفاصيل تشغيلية وقد تكشف بيانات المهام. عدّل بوابة viewHorizon في المزوّد المنشور، وتأكد من إعداد المصادقة والمسارات بما يناسب التطبيق.
use Illuminate\Support\Facades\Gate;
protected function gate(): void
{
Gate::define('viewHorizon', function ($user) {
return in_array($user->email, [
'ops@example.com',
], true);
});
}
استبدل عنوان المثال بحساب إداري موثوق، واستخدم نظام صلاحيات مركزيًا عندما يتوفر. اختبر الوصول بحساب عادي وبزائر غير مسجل؛ نجاح الدخول بحساب المدير وحده لا يثبت أن الحماية صحيحة.
8. التعامل مع المهام الفاشلة والمحاولات
تحقق من إعداد failed داخل config/queue.php ووجود جدول failed_jobs إذا كنت تستخدم تخزين الفشل في قاعدة البيانات. تتضمن مشاريع حديثة ترحيله مسبقًا؛ لا تنشئ ترحيلًا مكررًا. عند غيابه يمكنك توليده بالأمر التالي ثم تشغيل الترحيلات.
php artisan make:queue-failed-table
php artisan migrate
php artisan queue:failed
php artisan queue:retry FAILED_JOB_UUID
استبدل المعرّف بالقيمة المعروضة، وأعد المحاولة بعد إصلاح السبب، لا قبله. الأخطاء المؤقتة تستفيد من backoff، لكن عنوان بريد غير صالح أو إعداد اتصال مفقود يحتاج تصحيحًا. وقد تُحسب عمليات تحرير المهمة بواسطة بعض البرمجيات الوسيطة ضمن المحاولات، لذا راجع سلوكها قبل اختيار حد منخفض.
الطوابير لا تضمن تنفيذًا وحيدًا تمامًا. قد يصل البريد ثم تتعطل العملية قبل تسجيل النجاح، فتُرسل الرسالة مجددًا. للعمليات الحساسة استخدم مفاتيح idempotency وقيودًا فريدة وسجل حالات مناسبًا؛ منع الإرسال المتكرر للمهمة وحده لا يحل كل حالات تكرار الأثر الخارجي.
9. إبقاء Horizon قيد التشغيل في الإنتاج
لا تعتمد على جلسة SSH مفتوحة. استخدم مدير عمليات مثل Supervisor لإعادة تشغيل Horizon عند توقفه. المثال التالي يفترض وجود Supervisor ومشروع في /var/www/app؛ عدّل المسارات والمستخدم وفق ملكية الملفات وصلاحيات السجلات والتخزين.
[program:laravel-horizon]
process_name=%(program_name)s
command=/usr/bin/php /var/www/app/artisan horizon
directory=/var/www/app
autostart=true
autorestart=true
user=www-data
redirect_stderr=true
stdout_logfile=/var/www/app/storage/logs/horizon.log
stopwaitsecs=180
stopasgroup=true
killasgroup=true
sudo supervisorctl reread
sudo supervisorctl update
sudo supervisorctl status laravel-horizon
اجعل stopwaitsecs أطول من أطول مهمة مسموحة لديك، واضبط تدوير السجلات. لا تشغّل نسخة أخرى غير مقصودة من Horizon يدويًا، ولا تستخدم queue:work إضافيًا للطوابير نفسها دون تخطيط، حتى تبقى أعداد العمال وسعة التنفيذ تحت سيطرة واضحة.
10. النشر وتحديث العمال وقياس الأداء
العمال عمليات طويلة العمر، ولذلك لا يكفي رفع ملفات PHP ليستخدموا الشيفرة الجديدة. بعد تجهيز الإصدار وتشغيل الترحيلات المتوافقة، ابنِ كاش الإعدادات ثم اطلب إنهاء Horizon بأمان. يكمل أعماله الحالية ويخرج، ويعيد مدير العمليات تشغيله بالشيفرة الجديدة.
php artisan config:cache
php artisan horizon:terminate
لجمع لقطات مقاييس Horizon دوريًا، أضف المهمة المجدولة التالية إلى routes/console.php في البنية الحديثة، أو داخل تعريف الجدولة المناسب لإصدار مشروعك.
use Illuminate\Support\Facades\Schedule;
Schedule::command('horizon:snapshot')->everyFiveMinutes();
يجب تشغيل مجدول Laravel نفسه كل دقيقة، مثل إدخال cron التالي. إضافة التعريف دون تشغيل المجدول لن تنتج لقطات تلقائية.
* * * * * cd /var/www/app && /usr/bin/php artisan schedule:run >> /dev/null 2>&1
11. قائمة التحقق واستكشاف الأعطال
عندما تتراكم المهام، ابدأ بالاتصال والإعدادات قبل زيادة العمال. راقب زمن الانتظار إلى جانب زمن التنفيذ؛ فقد تكون المهمة سريعة لكن عدد العمال غير كافٍ. في المقابل، زيادة العمليات بلا قياس قد تستنزف اتصالات قاعدة البيانات أو تتجاوز حدود مزود البريد.
- تأكد أن QUEUE_CONNECTION يساوي redis، وليس sync.
- طابق اسم الطابور المرسل مع قائمة الطوابير في Horizon.
- راجع APP_ENV ووجود إعداد مشرف للبيئة الحالية.
- بعد تغيير البيئة، أعد بناء كاش الإعدادات وأعد تشغيل Horizon بأمان.
- افحص سجلات Laravel وSupervisor واتصال Redis من خادم العامل.
- جهّز سياسة استمرارية وذاكرة Redis بما يمنع فقد الطوابير أو إخلاء مفاتيحها عشوائيًا.
بهذا يصبح إعداد Laravel Queue مع Redis وHorizon منظومة تشغيل قابلة للمراقبة، وليس مجرد أمر يعمل في الطرفية. ابدأ بسعة محدودة، واختبر الفشل وإعادة المحاولة والنشر، ثم وسّع الموارد بناءً على قياسات فعلية ومتطلبات تطبيقك.
Why Use Laravel Queue with Redis and Horizon?
Laravel queues move slow operations outside HTTP requests. Redis stores queued work, while Horizon manages workers and provides operational visibility. This walkthrough assumes a recent Laravel application, a working database, and a Linux environment compatible with Horizon.
1. Prepare Redis and the Laravel Client
Check Redis connectivity and install Predis if you are not using PhpRedis. PHP CLI also needs the pcntl and posix extensions.
redis-cli ping
composer require predis/predis
php -m
QUEUE_CONNECTION=redis
REDIS_CLIENT=predis
REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null
REDIS_PORT=6379
REDIS_DB=0
REDIS_QUEUE_CONNECTION=default
REDIS_QUEUE=default
REDIS_QUEUE_RETRY_AFTER=120
Use the actual Redis hostname in containers. Keep Redis private, configure authentication, and isolate application keys. Do not treat queue storage as disposable cache.
2. Install Laravel Horizon
composer require laravel/horizon
php artisan horizon:install
php artisan config:clear
Keep Horizon as a production dependency. Review the published configuration instead of replacing it wholesale, since defaults vary between versions. Horizon requires Redis queues.
3. Configure the Queue Connection
Update the redis connection in config/queue.php. Enabling after_commit prevents jobs from running before their database transaction commits.
'redis' => [
'driver' => 'redis',
'connection' => env('REDIS_QUEUE_CONNECTION', 'default'),
'queue' => env('REDIS_QUEUE', 'default'),
'retry_after' => (int) env('REDIS_QUEUE_RETRY_AFTER', 120),
'block_for' => 5,
'after_commit' => true,
],
The retry_after setting controls reservation expiry, not retry backoff. A finite block_for also allows workers to respond to shutdown signals without indefinite blocking.
4. Create a Welcome Email Job
This example uses the default User model. Configure your mail transport, or use MAIL_MAILER=log for local testing.
php artisan make:job SendWelcomeEmail
<?php
namespace App\Jobs;
use App\Models\User;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Mail;
class SendWelcomeEmail implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public int $tries = 3;
public int $timeout = 60;
public function __construct(public int $userId)
{
}
public function backoff(): array
{
return [10, 30, 60];
}
public function handle(): void
{
$user = User::find($this->userId);
if (! $user) {
return;
}
Mail::raw(
'Welcome, '.$user->name.'!',
function ($message) use ($user) {
$message->to($user->email)
->subject('Welcome to our application');
}
);
}
}
Pass small identifiers rather than secrets or large payloads. Configure network timeouts for the mail transport as well.
5. Dispatch to the Correct Queue
Open Tinker and dispatch a job for an existing user.
php artisan tinker
$user = App\Models\User::query()->firstOrFail();
App\Jobs\SendWelcomeEmail::dispatch($user->id)
->onQueue('emails');
Horizon must watch emails. With after_commit enabled, dispatching inside a transaction waits for commit; rolled-back transactions discard those deferred jobs. Do not use dispatchSync when background execution is required.
6. Configure the Horizon Supervisor
Merge these environments into config/horizon.php. The connection name refers to a queue connection.
'environments' => [
'production' => [
'supervisor-1' => [
'connection' => 'redis',
'queue' => ['emails', 'default'],
'balance' => 'auto',
'minProcesses' => 1,
'maxProcesses' => 5,
'tries' => 3,
'timeout' => 90,
'memory' => 128,
],
],
'local' => [
'supervisor-1' => [
'connection' => 'redis',
'queue' => ['emails', 'default'],
'balance' => 'auto',
'maxProcesses' => 3,
'tries' => 3,
'timeout' => 90,
],
],
],
Keep the job timeout below Horizon's timeout, and Horizon's timeout below retry_after: 60, 90, and 120 seconds here. Job-level tries overrides the supervisor value.
Does Queue Order Guarantee Priority?
Automatic balancing does not provide strict priority based on queue order. Use separate supervisors with dedicated capacity for critical workloads.
7. Start Horizon and Secure Its Dashboard
php artisan horizon
php artisan horizon:status
Run the status command in another terminal. Visit /horizon and secure production access through the published provider's authorization gate and appropriate application authentication.
use Illuminate\Support\Facades\Gate;
protected function gate(): void
{
Gate::define('viewHorizon', function ($user) {
return in_array($user->email, [
'ops@example.com',
], true);
});
}
Replace the example address with a trusted administrator. Verify that ordinary users and unauthenticated visitors cannot access the dashboard.
8. Handle Failed Jobs and Retries
Check the failed-job configuration and database table. Only generate the migration if your project does not already include it.
php artisan make:queue-failed-table
php artisan migrate
php artisan queue:failed
php artisan queue:retry FAILED_JOB_UUID
Replace the identifier and fix the underlying issue before retrying. Backoff helps temporary failures, not invalid configuration. Middleware releases may consume attempts.
Queues do not guarantee exactly-once effects. An email might be delivered before a worker crashes and then be sent again. Protect sensitive operations with idempotency keys, unique constraints, and suitable state tracking.
9. Keep Horizon Running in Production
Use a process manager rather than an open SSH session. Adjust this Supervisor configuration to your paths and permissions.
[program:laravel-horizon]
process_name=%(program_name)s
command=/usr/bin/php /var/www/app/artisan horizon
directory=/var/www/app
autostart=true
autorestart=true
user=www-data
redirect_stderr=true
stdout_logfile=/var/www/app/storage/logs/horizon.log
stopwaitsecs=180
stopasgroup=true
killasgroup=true
sudo supervisorctl reread
sudo supervisorctl update
sudo supervisorctl status laravel-horizon
Set stopwaitsecs above the longest permitted job duration and configure log rotation. Avoid unintended extra Horizon or queue:work processes.
10. Deploy Updates and Collect Metrics
After preparing a release and compatible migrations, rebuild configuration and gracefully terminate Horizon. Supervisor restarts it with the new code.
php artisan config:cache
php artisan horizon:terminate
Schedule metric snapshots in routes/console.php for modern application structures, or the scheduling location used by your version.
use Illuminate\Support\Facades\Schedule;
Schedule::command('horizon:snapshot')->everyFiveMinutes();
Ensure Laravel's scheduler actually runs every minute.
* * * * * cd /var/www/app && /usr/bin/php artisan schedule:run >> /dev/null 2>&1
11. Troubleshooting Checklist
Investigate configuration before increasing worker counts. Monitor queue waiting time as well as execution duration.
- Confirm QUEUE_CONNECTION is redis rather than sync.
- Match dispatched queue names with Horizon's watched queues.
- Check APP_ENV and its supervisor configuration.
- Rebuild configuration and restart Horizon after environment changes.
- Inspect application logs, Supervisor logs, and Redis connectivity.
- Plan Redis persistence and memory policies to protect queued work.
Start with modest capacity, test failures and deployments, and scale using measurements rather than assumptions.
عبدالرحمن ربيع
Software Engineer & AI Builder
مطور برمجيات متكامل ومصمم جرافيك مع أكثر من 4 سنوات خبرة في بناء تطبيقات الويب الحديثة باستخدام PHP و JavaScript و HTML و CSS. خلفية قوية في تصميم UI/UX واستخدام متقدم لأدوات الذكاء الاصطناعي لتعزيز كفاءة التطوير والأتمتة واتخاذ القرارات. حاصل على ماجستير تنفي...
التعليقات (0)
كن أول من يعلّق على هذا المقال.