مقدمه
یکی از مشکلات رایج در پروژههای بزرگ لاراول این است که با افزایش تعداد کاربران و حجم اطلاعات، بعضی درخواستها به زمان زیادی برای اجرا نیاز پیدا میکنند. برای مثال، ممکن است کاربر در یک فروشگاه اینترنتی سفارشی ثبت کند، اما سیستم همزمان بخواهد ایمیل تأیید ارسال کند، پیامک بفرستد، فاکتور PDF بسازد، موجودی انبار را بررسی کند و اطلاعات سفارش را به یک سرویس خارجی ارسال کند.
اگر تمام این عملیات در همان درخواست HTTP انجام شوند، کاربر مجبور است مدت زیادی منتظر بماند. در شرایط پرترافیک، این مسئله میتواند باعث افزایش زمان پاسخگویی، مصرف منابع سرور و حتی بروز خطاهای Timeout شود.
راهکار مناسب برای بسیاری از این سناریوها استفاده از Queue و Job در Laravel است. با انتقال عملیات زمانبر به صف، میتوان پاسخ درخواست کاربر را سریعتر ارسال کرد و پردازشهای طولانی را در پسزمینه انجام داد.
در این مقاله با استفاده از Laravel 12 یاد میگیریم چگونه یک سیستم صف برای پردازش کارهای سنگین طراحی کنیم، Jobها را در صفهای جداگانه قرار دهیم، Workerها را مدیریت کنیم و از تکرار ناخواسته عملیات جلوگیری کنیم.
در این مقاله چه چیزی یاد میگیریم؟
- Queue و Job در لاراول چه تفاوتی دارند؟
- چگونه یک Job برای پردازش سنگین بسازیم؟
- چگونه از Redis یا دیتابیس بهعنوان صف استفاده کنیم؟
- چطور برای کارهای مهم و غیرضروری اولویت تعیین کنیم؟
- چگونه تعداد تلاش مجدد و زمان اجرای Job را کنترل کنیم؟
- چطور از پردازش تکراری و مشکلات تراکنش دیتابیس جلوگیری کنیم؟
- چگونه صفها را در محیط واقعی مانیتور کنیم؟
Queue در Laravel چیست؟
Queue یا صف، سازوکاری برای به تعویق انداختن اجرای بعضی عملیات است. بهجای اینکه تمام کارها در طول درخواست کاربر انجام شوند، برنامه یک Job ایجاد میکند و آن را به صف میفرستد. سپس یک پردازش جداگانه به نام Worker، وظایف قرارگرفته در صف را اجرا میکند.
برای مثال، روند ثبت سفارش میتواند به این شکل باشد:
- کاربر سفارش خود را ثبت میکند.
- برنامه سفارش را در دیتابیس ذخیره میکند.
- برنامه Job ارسال پیامک یا ایمیل را به صف میفرستد.
- پاسخ ثبت سفارش به کاربر برگردانده میشود.
- Worker در پسزمینه پیامک یا ایمیل را ارسال میکند.
در این ساختار، کاربر برای پایان یافتن ارسال پیامک منتظر نمیماند.
تفاوت Job، Queue و Worker
Job: کلاسی است که وظیفه مشخصی را انجام میدهد؛ مانند ارسال پیامک، تولید گزارش یا پردازش تصویر.
Queue: محلی منطقی برای نگهداری Jobهایی است که باید اجرا شوند. این صف میتواند از طریق Redis، دیتابیس یا سرویسهای دیگر پیادهسازی شود.
Worker: پردازشی است که صف را بررسی میکند و Jobهای موجود را اجرا میکند.
نکته مهم این است که ساختن یک Job بهتنهایی به معنای اجرای پسزمینه آن نیست. باید یک Queue Connection مناسب انتخاب کنید و Worker مربوط به آن را نیز اجرا کنید.
چه کارهایی را باید به Queue منتقل کنیم؟
انتقال هر عملیات به Queue لزوماً تصمیم درستی نیست. بهتر است وظایفی را به صف منتقل کنیم که زمانبر هستند، به سرویس خارجی وابستگی دارند یا میتوان اجرای آنها را به بعد از پاسخ HTTP موکول کرد.
نمونههای مناسب عبارتاند از:
- ارسال ایمیل و پیامک
- ارسال اطلاعات به APIهای خارجی
- تولید گزارشهای حجیم و فایل PDF
- پردازش فایلهای CSV و Excel
- تغییر اندازه تصاویر و تولید نسخههای مختلف
- همگامسازی اطلاعات با سرویسهای دیگر
- ارسال اعلان به تعداد زیادی کاربر
- محاسبات آماری سنگین
- پردازش گروهی سفارشها یا اطلاعات محصولات
در مقابل، عملیات ضروری برای صحت یک درخواست، مانند اعتبارسنجی ورودی، بررسی مجوز کاربر و ثبت اولیه اطلاعات ضروری سفارش، معمولاً باید قبل از اعلام موفقیت به کاربر انجام شوند.
همچنین اگر عملیات نیازمند نتیجه فوری است، انتقال کامل آن به صف ممکن است طراحی مناسبی نباشد. برای چنین مواردی میتوان از ترکیبی از پردازش همزمان و پسزمینه استفاده کرد.
مرحله اول: انتخاب درایور Queue
لاراول یک API یکپارچه برای صفها ارائه میدهد. بنابراین میتوانید بدون تغییر اساسی در منطق Job، از درایورهای مختلف استفاده کنید.
دو انتخاب رایج عبارتاند از:
- Database: مناسب شروع کار و پروژههای کوچک یا متوسط، بدون نیاز به راهاندازی سرویس جداگانه.
- Redis: انتخاب متداول برای سامانههایی که به پردازش سریع صف و مدیریت حجم بالای Job نیاز دارند.
در این مقاله ابتدا از Database برای راهاندازی نمونه استفاده میکنیم و سپس به Redis میپردازیم.
تنظیم Queue با دیتابیس
در فایل .env مقدار زیر را تنظیم کنید:
QUEUE_CONNECTION=database
سپس بررسی کنید که جداول موردنیاز صف در دیتابیس وجود داشته باشند. در Laravel 12 میتوانید دستور زیر را اجرا کنید:
php artisan make:queue-table
php artisan migrate
اگر پروژه شما از قبل migration مربوط به صف را دارد، لازم نیست آن را دوباره تولید کنید. همچنین اگر جدول jobs قبلاً ساخته شده است، وضعیت migrationها را بررسی کنید تا از ایجاد migration تکراری جلوگیری شود.
بعد از تنظیم درایور، میتوانید Jobهای خود را در صف قرار دهید.
مرحله دوم: ساخت یک Job برای پردازش سنگین
فرض کنید یک فروشگاه اینترنتی دارید و پس از ثبت سفارش، باید برای مشتری ایمیل تأیید ارسال شود. ارسال ایمیل ممکن است به دلیل ارتباط با سرور SMTP زمانبر باشد؛ بنابراین میتوانیم این کار را به یک Job منتقل کنیم.
ابتدا دستور زیر را اجرا کنید:
php artisan make:job SendOrderConfirmation
با اجرای این دستور، فایلی در مسیر زیر ایجاد میشود:
app/Jobs/SendOrderConfirmation.php
کد فایل را به شکل زیر تنظیم کنید:
<?php
namespace App\Jobs;
use App\Models\Order;
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;
use App\Mail\OrderConfirmationMail;
class SendOrderConfirmation implements ShouldQueue
{
use Dispatchable;
use InteractsWithQueue;
use Queueable;
use SerializesModels;
public function __construct(
public int $orderId
) {
}
public function handle(): void
{
$order = Order::findOrFail($this->orderId);
Mail::to($order->customer_email)
->send(new OrderConfirmationMail($order));
}
}
در این مثال، فقط شناسه سفارش به Job داده شده است. هنگام اجرای Job، اطلاعات سفارش از دیتابیس خوانده میشود.
این روش در بسیاری از پروژهها از ارسال مجموعه بزرگی از اطلاعات مدل به صف جلوگیری میکند و مشخص میسازد که Job با کدام سفارش کار دارد.
البته مدلها نیز در بسیاری از موارد قابل ارسال به Job هستند و لاراول قابلیتهایی برای سریالسازی مدلهای Eloquent دارد؛ انتخاب شناسه بهخصوص زمانی مفید است که بخواهید کنترل بیشتری بر زمان بازیابی اطلاعات داشته باشید.
نکته: کلاس OrderConfirmationMail باید در پروژه وجود داشته باشد و تنظیمات ایمیل نیز بهدرستی انجام شده باشند. اگر سیستم ایمیل شما از قبل پیادهسازی شده است، از همان کلاس موجود استفاده کنید.
مرحله سوم: قرار دادن Job در صف
پس از ایجاد Job، باید آن را در زمان مناسب Dispatch کنیم.
برای مثال، در بخشی از منطق ثبت سفارش که سفارش در دیتابیس ذخیره شده است، میتوانیم بنویسیم:
use App\Jobs\SendOrderConfirmation;
$order = Order::create([
'customer_email' => $request->input('email'),
// سایر فیلدهای موردنیاز سفارش
]);
SendOrderConfirmation::dispatch($order->id);
در این مثال، بعد از ایجاد سفارش، Job ارسال ایمیل به صف فرستاده میشود.
کد بالا نمونهای از منطق موردنیاز است؛ در پروژه واقعی باید فیلدها، اعتبارسنجی و ارتباطات سفارش با ساختار دیتابیس خودتان هماهنگ باشند.
اجرای Job با تأخیر
گاهی لازم است یک عملیات کمی بعد از ثبت درخواست اجرا شود. برای این کار میتوانید از delay() استفاده کنید:
SendOrderConfirmation::dispatch($order->id)
->delay(now()->addMinutes(2));
در این حالت، اجرای Job برای دو دقیقه بعد برنامهریزی میشود. پشتیبانی دقیق از تأخیر به درایور صف و تنظیمات آن وابسته است.
مرحله چهارم: اجرای Queue Worker
تا این مرحله Job را ساختهایم و در صف قرار دادهایم؛ اما برای اجرای آن باید Worker فعال باشد.
در محیط توسعه دستور زیر را اجرا کنید:
php artisan queue:work
Worker تا زمانی که متوقف نشده باشد، صف را بررسی میکند و Jobهای آماده را اجرا میکند.
برای مشاهده خطاها و تست اولیه، ترمینال Worker را باز نگه دارید و از مسیر برنامه عملیاتی را انجام دهید که Job را ایجاد میکند.
اجرای Worker با تنظیمات مشخص
برای کنترل تعداد تلاشها و مدت مجاز اجرای هر Job میتوانید از گزینههای زیر استفاده کنید:
php artisan queue:work database --tries=3 --timeout=60
در این مثال:
databaseنام Connection صف است.--tries=3حداکثر تلاش مجاز را در سطح Worker مشخص میکند، مگر اینکه تنظیم اختصاصی Job اولویت داشته باشد.--timeout=60محدودیت زمانی اجرای Job را بر حسب ثانیه تعیین میکند.
مقادیر فوق نمونهاند؛ برای هر Job باید بر اساس مدت اجرای واقعی، زمان پاسخ سرویسهای خارجی و تنظیمات درایور انتخاب شوند.
آیا اجرای دستور queue:work در ترمینال کافی است؟
برای تست محلی بله، اما در محیط Production نباید به باز ماندن یک ترمینال دستی وابسته باشید.
در سرور واقعی باید Worker بهعنوان یک پردازش تحت مدیریت سرویسهایی مانند Supervisor یا سازوکار مناسب محیط استقرار اجرا شود. در غیر این صورت ممکن است پس از بسته شدن نشست SSH، ریاستارت سرور یا توقف پردازش، اجرای Jobها متوقف شود.
همچنین Workerهای لاراول پردازشهای طولانیعمر هستند. بعد از انتشار نسخه جدید برنامه، معمولاً لازم است Workerها بهصورت کنترلشده ریاستارت شوند تا کد جدید را بارگذاری کنند:
php artisan queue:restart
این دستور به Workerها علامت میدهد که پس از اتمام Job جاری، خارج شوند. برای راهاندازی مجدد خودکار، باید Process Manager یا ابزار استقرار شما این رفتار را مدیریت کند.
مرحله پنجم: چرا صفهای سنگین به چند Queue نیاز دارند؟
فرض کنید برنامه شما همزمان این کارها را انجام میدهد:
- ارسال پیامک ورود
- ارسال ایمیل تبلیغاتی به هزاران کاربر
- تولید گزارش مالی
- پردازش تصاویر محصولات
اگر همه این Jobها در یک صف قرار بگیرند، ممکن است کارهای زمانبر مانند تولید گزارش یا ارسال انبوه ایمیل، پشت سر هم قرار بگیرند و رسیدگی به عملیات مهمتر را به تأخیر بیندازند.
راهکار این است که کارها را بر اساس اهمیت و نوع پردازش به صفهای مجزا تقسیم کنیم.
برای مثال:
critical: عملیات مهم و حساس به زمانdefault: وظایف معمولیreports: گزارشگیری و پردازشهای طولانیbulk: عملیات انبوه
تعیین Queue هنگام Dispatch
میتوانید Job را به صف مشخصی بفرستید:
SendOrderConfirmation::dispatch($order->id)
->onQueue('critical');
برای مثال، اگر تولید گزارش سنگین دارید، Job مربوط به گزارش را در صف reports قرار دهید.
اجرای Worker برای چند صف
یک Worker میتواند چند صف را با ترتیب اولویت بررسی کند:
php artisan queue:work redis \
--queue=critical,default,reports \
--tries=3 \
--timeout=120
در این مثال، Worker صفها را به ترتیب اعلامشده بررسی میکند. این روش برای سادهسازی اولویتها مفید است، اما تضمین نمیکند که صفهای کماولویت همیشه فرصت پردازش کافی دریافت کنند.
اگر حجم کارها زیاد است، میتوانید برای صفهای مختلف Workerهای جداگانه و ظرفیت مشخص تعریف کنید. برای نمونه، یک گروه Worker برای critical و گروه دیگری برای reports در نظر بگیرید.
نکته مهم: تعداد Workerها را صرفاً بر اساس تعداد صفها افزایش ندهید. ظرفیت CPU، حافظه، اتصالات دیتابیس و محدودیت سرویسهای خارجی باید در تصمیمگیری لحاظ شوند.
مرحله ششم: استفاده از Redis برای صفهای پرترافیک
در پروژههایی که Jobهای زیادی تولید میکنند، Redis میتواند گزینه مناسبی برای مدیریت صف باشد. با این حال، استفاده از Redis بهتنهایی به معنای حل تمام مشکلات کارایی نیست؛ منطق Job، تعداد Workerها و منابع سرور نیز اهمیت دارند.
برای استفاده از Redis، ابتدا مطمئن شوید Redis روی محیط شما در دسترس است و تنظیمات اتصال آن در Laravel صحیح است.
سپس در فایل .env مقدار زیر را تنظیم کنید:
QUEUE_CONNECTION=redis
اگر تنظیمات اتصال یا نام Connectionهای Redis را تغییر دادهاید، پیکربندی نهایی را در فایل config/queue.php و تنظیمات Redis پروژه بررسی کنید.
بعد از تغییر متغیرهای محیطی، در صورت استفاده از Cache تنظیمات، آن را پاک یا بازسازی کنید:
php artisan config:clear
اکنون Worker را با Connection مربوط به Redis اجرا کنید:
php artisan queue:work redis
برای صفهای سنگین، تنظیمات مهمی مانند retry_after، block_for، Timeout مربوط به Worker و تعداد تلاشها باید با یکدیگر هماهنگ باشند.
بهویژه، زمان مجاز اجرای Job و زمان بازگرداندن Jobهای رزروشده به صف را طوری تنظیم کنید که یک Job در حین اجرای عادی بهاشتباه دوباره در دسترس Worker دیگری قرار نگیرد. تنظیم دقیق این مقادیر به درایور، زمان اجرای واقعی و رفتار Job بستگی دارد.
مرحله هفتم: مدیریت Jobهای ناموفق و تلاش مجدد
در محیط واقعی، همیشه همه Jobها با موفقیت اجرا نمیشوند. برای مثال:
- سرویس پیامک موقتاً در دسترس نیست.
- API خارجی پاسخ نمیدهد.
- اتصال دیتابیس قطع میشود.
- فایل موردنیاز پیدا نمیشود.
- عملیات از محدودیت زمانی عبور میکند.
اگر Job با خطای مدیریتنشده مواجه شود، لاراول میتواند مطابق تنظیمات آن را دوباره امتحان کند. برای این کار باید تعداد تلاشها و زمان مجاز اجرا را با توجه به نوع عملیات تنظیم کنید.
در کلاس Job میتوانید تنظیمات زیر را تعریف کنید:
public int $tries = 3;
public int $timeout = 60;
public array $backoff = [10, 30, 60];
در این نمونه، Job حداکثر سه تلاش دارد و فاصله تلاشهای مجدد بهترتیب ۱۰، ۳۰ و ۶۰ ثانیه در نظر گرفته شده است.
این تنظیمات باید با زمان اجرای Worker و درایور صف هماهنگ باشند. برای مثال، اگر Job بهدلیل محدودیت سرویس خارجی نیازمند تأخیر طولانیتری است، بهتر است برنامه تلاش مجدد آن را متناسب با همان محدودیت طراحی کنید.
مشاهده Jobهای ناموفق
برای مشاهده فهرست Jobهای ناموفق میتوانید اجرا کنید:
php artisan queue:failed
برای تلاش مجدد همه Jobهای ناموفق:
php artisan queue:retry all
برای تلاش مجدد یک Job مشخص، شناسه همان Job را به دستور بدهید:
php artisan queue:retry 5
عدد 5 در این مثال شناسه نمونه است؛ باید از شناسه واقعی خروجی دستور queue:failed استفاده کنید.
همچنین برای پاک کردن Jobهای ناموفق باید مطمئن باشید که دیگر به بررسی یا تلاش مجدد آنها نیاز ندارید.
مرحله هشتم: جلوگیری از پردازش تکراری
یکی از مهمترین چالشهای صفهای سنگین، اجرای تکراری عملیات است.
فرض کنید Job ارسال پیامک اجرا شده و سرویس پیامک پیام را دریافت کرده است، اما پیش از اینکه برنامه نتیجه را بهدرستی ثبت کند، اتصال قطع میشود. در تلاش بعدی ممکن است همان پیامک دوباره ارسال شود.
بنابراین نباید فرض کنیم که هر Job دقیقاً یک بار اجرا خواهد شد. در طراحی سیستمهای صف، لازم است امکان تلاش مجدد را در نظر بگیریم و عملیات حساس را تا حد ممکن idempotent طراحی کنیم؛ یعنی اجرای دوباره آنها اثر ناخواسته و تکراری ایجاد نکند.
راهکار اول: ثبت وضعیت عملیات
برای عملیات حساس، میتوانید وضعیت پردازش را در دیتابیس نگهداری کنید. برای مثال، یک رکورد اعلان میتواند وضعیتهایی مانند pending، processing، sent و failed داشته باشد.
پیش از اجرای عملیات، وضعیت آن بررسی میشود و پس از موفقیت، نتیجه ثبت میشود. با این حال، بررسی وضعیت بهتنهایی در پردازش همزمان کافی نیست؛ برای جلوگیری از رقابت بین Workerها ممکن است به قفل، تراکنش یا محدودیت یکتایی در دیتابیس نیز نیاز داشته باشید.
راهکار دوم: استفاده از کلید یکتا
اگر عملیات یک شناسه تجاری مشخص دارد، میتوانید آن شناسه را بهعنوان کلید یکتای عملیات در دیتابیس ذخیره کنید. برای مثال، ارسال اعلان مربوط به یک سفارش میتواند با ترکیب شناسه سفارش و نوع اعلان شناسایی شود.
در این حالت، محدودیت UNIQUE در دیتابیس میتواند از ایجاد رکوردهای تکراری جلوگیری کند.
البته این راهکار زمانی مؤثر است که ثبت نتیجه و اجرای عملیات خارجی بهدرستی طراحی شده باشند. برای سرویسهای خارجی، استفاده از قابلیت idempotency همان سرویس ــ اگر پشتیبانی شود ــ میتواند اهمیت زیادی داشته باشد.
راهکار سوم: جلوگیری از همزمانی ناخواسته
لاراول Middlewareهایی برای کنترل همپوشانی Jobها ارائه میکند؛ از جمله WithoutOverlapping. این قابلیت در سناریوهایی مفید است که نمیخواهید چند Worker بهطور همزمان روی یک منبع مشخص کار کنند.
با این حال، جلوگیری از همزمانی با تضمین جلوگیری از تکرار یکسان نیست. اگر Job پس از اجرا مجدداً به صف برگردد، همچنان باید رفتار تکراری آن را مدیریت کنید.
مرحله نهم: مدیریت تراکنش دیتابیس و Queue
یکی از خطاهای رایج در پروژههای واقعی، Dispatch کردن Job قبل از نهایی شدن تراکنش دیتابیس است.
برای مثال، برنامه سفارش را داخل تراکنش ایجاد میکند و بلافاصله Job را به صف میفرستد. اگر Worker سریعتر از Commit شدن تراکنش اجرا شود، ممکن است هنوز نتواند سفارش را پیدا کند.
برای کاهش این مشکل، میتوانید Job را بعد از Commit شدن تراکنش Dispatch کنید:
SendOrderConfirmation::dispatch($order->id)
->afterCommit();
این قابلیت به لاراول میگوید Dispatch را تا بعد از Commit شدن تراکنشهای فعال به تعویق بیندازد.
همچنین میتوانید رفتار مشابه را از طریق تنظیمات صف و گزینههای مربوط به after_commit مدیریت کنید. انتخاب مناسب به ساختار تراکنشهای پروژه بستگی دارد.
این موضوع بهخصوص در سیستمهای فروشگاهی، مالی و سامانههایی که اطلاعات چند جدول را همزمان تغییر میدهند اهمیت دارد.
مرحله دهم: پردازش حجم زیادی از دادهها
فرض کنید باید اطلاعات ۵۰ هزار محصول را از یک فایل وارد دیتابیس کنید. اگر همه اطلاعات در یک Job بزرگ پردازش شوند، احتمال افزایش مصرف حافظه، طولانی شدن اجرا و شکست کامل عملیات وجود دارد.
راهکار مناسب میتواند تقسیم عملیات به بخشهای کوچکتر باشد.
برای مثال، ابتدا دادهها را به بخشهای ۵۰۰تایی تقسیم کنید و سپس برای هر بخش یک Job بسازید. اندازه مناسب هر بخش به حجم هر رکورد، پیچیدگی پردازش، ظرفیت سرور و محدودیتهای دیتابیس بستگی دارد.
نمونهای از ایجاد چند Job برای بخشهای مستقل:
foreach ($chunks as $chunk) {
ProcessProductsChunk::dispatch($chunk->pluck('id')->all())
->onQueue('bulk');
}
در این نمونه فرض شده است که متغیر $chunks شامل مجموعههایی از رکوردهای محصولات است و کلاس ProcessProductsChunk نیز در پروژه پیادهسازی شده است.
برای حجمهای بالا، بهتر است دادهها را با روشهایی مانند chunkById() یا lazyById() از دیتابیس بخوانید تا مجبور نباشید همه رکوردها را همزمان در حافظه نگه دارید.
در پردازش دستهای، باید وضعیت هر بخش را مستقل مدیریت کنید؛ در غیر این صورت ممکن است با شکست یک بخش، مشخص نباشد کدام رکوردها با موفقیت پردازش شدهاند.
مرحله یازدهم: مانیتور کردن صفها
در پروژههای کوچک، بررسی لاگها و دستورهای Artisan ممکن است کافی باشد؛ اما در سامانههای پرترافیک بهتر است وضعیت صفها بهصورت منظم پایش شود.
مواردی که باید زیر نظر داشته باشید عبارتاند از:
- تعداد Jobهای منتظر اجرا
- تعداد Jobهای ناموفق
- مدت زمان انتظار در صف
- میانگین زمان اجرای Jobها
- تعداد تلاشهای مجدد
- مصرف CPU و حافظه Workerها
- خطاهای اتصال به Redis یا دیتابیس
- تعداد Jobهای در حال اجرا
برای پروژههای مبتنی بر Redis، Laravel Horizon ابزار شناختهشدهای برای مانیتور کردن صفهای Redis و مشاهده وضعیت Workerها است. استفاده از آن نیازمند نصب، پیکربندی و راهاندازی متناسب با نسخه و محیط پروژه است.
همچنین باید مشخص باشد که چه زمانی یک صف غیرعادی تلقی میشود. برای مثال، اگر صف ارسال اعلان معمولاً در چند ثانیه پردازش میشود اما ناگهان زمان انتظار آن چند دقیقه شود، ممکن است تعداد Workerها کافی نباشد یا یک سرویس خارجی کند شده باشد.
مرحله دوازدهم: تست کردن Jobها
قبل از انتقال یک Job به محیط Production، بهتر است رفتار آن را تست کنید. لاراول ابزارهایی برای Fake کردن صفها و بررسی Dispatch شدن Jobها ارائه میدهد.
برای مثال:
use App\Jobs\SendOrderConfirmation;
use Illuminate\Support\Facades\Queue;
public function test_order_confirmation_is_queued(): void
{
Queue::fake();
SendOrderConfirmation::dispatch(123);
Queue::assertPushed(
SendOrderConfirmation::class,
function ($job) {
return $job->orderId === 123;
}
);
}
این تست بررسی میکند که Job با شناسه سفارش مورد انتظار به صف فرستاده شده است. چون از Queue::fake() استفاده شده، Job در این تست واقعاً اجرا نمیشود.
بنابراین این تست جایگزین تست منطق داخل handle() نیست. بهتر است منطق پردازش را نیز جداگانه با دادههای آزمایشی تست کنید و برای بخشهایی که به سرویس خارجی وابستهاند، رفتار موفقیت و شکست را بررسی کنید.
اشتباهات رایج در مدیریت Queueهای سنگین
۱. تصور اینکه Dispatch به معنی اجرا شدن است
Dispatch معمولاً به معنی قرار دادن Job در صف است، نه اینکه حتماً بلافاصله اجرا شده باشد. اگر Worker فعال نباشد، Job ممکن است در صف باقی بماند.
۲. اجرای Worker بدون مدیریت در Production
اجرای دستی Worker در SSH برای آزمایش مناسب است، اما برای محیط واقعی باید راهکار مدیریت پردازش و راهاندازی مجدد داشته باشید.
۳. افزایش بیحساب تعداد Workerها
Worker بیشتر لزوماً به معنی عملکرد بهتر نیست. تعداد زیاد Worker میتواند فشار روی دیتابیس، Redis، CPU و سرویسهای خارجی را افزایش دهد.
۴. تنظیم نادرست Timeout و Retry
اگر محدودیت زمانی و تنظیمات تلاش مجدد با رفتار Job هماهنگ نباشند، ممکن است عملیات زودتر از موعد شکست بخورد یا چند بار اجرا شود.
۵. انجام تمام پردازشها در یک Job بزرگ
Jobهای بسیار بزرگ، عیبیابی و بازیابی پس از خطا را دشوار میکنند. در پردازشهای حجیم، تقسیم وظیفه به بخشهای مستقل معمولاً کنترل بیشتری فراهم میکند.
۶. نادیده گرفتن امنیت اطلاعات Job
از قرار دادن اطلاعات حساس غیرضروری مانند رمز عبور، کلید API یا دادههای محرمانه در Payload صف خودداری کنید. دسترسی به Redis، دیتابیس صف، لاگها و ابزارهای مانیتورینگ نیز باید محدود باشد.
۷. فرض اینکه هر Job فقط یک بار اجرا میشود
در طراحی صف، تلاش مجدد و شکستهای موقت را در نظر بگیرید. عملیات مالی، ارسال پیام و تغییر وضعیت سفارش باید با کنترل تکرار و سازوکارهای مناسب دیتابیس طراحی شوند.
یک معماری پیشنهادی برای پروژه واقعی
برای یک فروشگاه اینترنتی متوسط میتوان از ساختار زیر شروع کرد:
| نوع عملیات | صف پیشنهادی | ملاحظه |
|---|---|---|
| اعلانهای حساس به زمان | critical | ظرفیت مستقل برای کاهش تأخیر |
| ارسال ایمیل معمولی | default | تعداد Worker متناسب با حجم |
| گزارشهای سنگین | reports | محدودیت زمان و حافظه متناسب |
| پردازش فایل و عملیات انبوه | bulk | تقسیم داده به بخشهای کوچک |
این جدول یک الگوی اولیه است، نه یک نسخه ثابت برای تمام پروژهها. پس از راهاندازی، باید زمان انتظار صف، زمان اجرای Jobها و منابع مصرفشده را اندازهگیری کنید و بر اساس داده واقعی ظرفیت Workerها را تغییر دهید.
در پروژههایی که چند نوع پردازش همزمان دارند، جداسازی صفها معمولاً امکان کنترل و عیبیابی بهتری فراهم میکند؛ اما پیچیدگی عملیاتی بیشتری نیز دارد.
جمعبندی
Queue یکی از ابزارهای مهم لاراول برای انتقال عملیات زمانبر به پسزمینه است. با استفاده صحیح از Job، Queue و Worker میتوانید زمان پاسخگویی درخواستها را کاهش دهید و پردازشهای سنگین را قابلکنترلتر کنید.
برای پیادهسازی قابلاعتماد در محیط واقعی، صرفاً Dispatch کردن Job کافی نیست. باید به انتخاب درایور، تقسیم صفها، تعداد Workerها، Timeout، تلاش مجدد، جلوگیری از پردازش تکراری، تراکنش دیتابیس و مانیتورینگ توجه کنید.
اگر پروژه شما هنوز کوچک است، میتوانید با Queue مبتنی بر دیتابیس شروع کنید. با افزایش حجم کار، Redis و ابزارهایی مانند Laravel Horizon نیز میتوانند به مدیریت بهتر صفها کمک کنند.
پیشنهاد عملی: یک عملیات زمانبر و غیرحساس به پاسخ فوری را در پروژه خود انتخاب کنید، آن را به Job تبدیل کنید، Worker را راهاندازی کنید و مدت پاسخگویی درخواست را قبل و بعد از تغییر اندازه بگیرید. این آزمایش ساده کمک میکند تأثیر واقعی Queue را در پروژه خود مشاهده کنید.
سؤالات متداول
Queue در Laravel چه کاربردی دارد؟
Queue برای اجرای وظایف در پسزمینه استفاده میشود؛ بهخصوص عملیاتی که زمانبر هستند یا نیازی نیست پیش از پاسخ HTTP کامل شوند.
تفاوت Job و Queue در لاراول چیست؟
Job کدی است که باید اجرا شود؛ Queue محل نگهداری و مدیریت Jobهای منتظر اجرا است.
برای Queue در Laravel از Redis استفاده کنیم یا Database؟
Database برای شروع و پروژههای ساده مناسب است. Redis در بسیاری از سامانههای پرترافیک گزینه مناسبی است، اما انتخاب نهایی باید بر اساس نیاز، زیرساخت و نتایج اندازهگیری باشد.
چرا Job در Laravel اجرا نمیشود؟
علتهای رایج عبارتاند از فعال نبودن Worker، تنظیم اشتباه QUEUE_CONNECTION، باقی ماندن Job در صف، خطای داخل handle() یا انتقال Job به فهرست Jobهای ناموفق.
چگونه Jobهای ناموفق را دوباره اجرا کنیم؟
ابتدا با php artisan queue:failed آنها را بررسی کنید. سپس با دستور php artisan queue:retry شناسه Job موردنظر یا مقدار all را برای تلاش مجدد مشخص کنید.
آیا Queue باعث میشود عملیات سریعتر انجام شود؟
Queue لزوماً زمان اجرای خود عملیات را کاهش نمیدهد؛ مزیت اصلی آن این است که میتواند عملیات زمانبر را از مسیر پاسخ HTTP جدا کند. زمان تکمیل نهایی عملیات همچنان به منطق برنامه، زیرساخت و حجم کار وابسته است.
منابع و مستندات رسمی
- مستندات Queue در Laravel 12: https://laravel.com/docs/12.x/queues
- مستندات Eloquent: https://laravel.com/docs/12.x/eloquent
- مستندات دیتابیس: https://laravel.com/docs/12.x/database
- مستندات Laravel Horizon: https://laravel.com/docs/12.x/horizon





