المهام المجدولة والمعالجة غير المتزامنة
المهام المجدولة والمعالجة غير المتزامنة هما مفتاح تعزيز كفاءة الخلفية—جدول ما يحتاج للجدولة وشغّل بالتوازي ما يمكن تشغيله بالتوازي، حتى لا يضطر المستخدمون للانتظار بلا داعٍ.
1. ما ستتعلمه
@EnableSchedulingوتعبيرات Cron / FixedRate / FixedDelay- التنفيذ غير المتزامن باستخدام
@EnableAsyncو@Async - تكوين تجمع خيوط مخصص
ThreadPoolTaskExecutor - معالجة الاستثناءات غير المتزامنة
AsyncUncaughtExceptionHandler - Alice نفّذت مهمة مجدولة لإلغاء الطلبات غير المدفوعة خلال 30 دقيقة تلقائيًا
2. قصة حقيقية من مطور أعمال
(1) نقطة الألم: تراكم الطلبات المتأخرة
اكتشفت Alice تراكمًا كبيرًا من الطلبات غير المدفوعة في OrderFlow—المستخدمون يقدمون طلبات لكن لا يدفعون، مما يحجز المخزون ويمنع مستخدمين آخرين من الشراء. أبلغ Charlie أن هناك حوالي 2000 طلب متأخرة يوميًا، تحجز مجتمعة مخزونًا بقيمة 50,000 دولار. سابقًا، كانت الطلبات تُلغى يدويًا بفحص قاعدة البيانات كل 30 دقيقة، وهو ما كان غير فعال وعرضة للسهو.
(2) حلول @Scheduled
Spring Boot يتضمن مهام مجدولة مدمجة يمكن تنفيذها بتعليق توضيحي واحد:
@Scheduled(fixedRate = 5 * 60 * 1000) // كل 5 دقائق
public void cancelExpiredOrders() {
List<Order> expired = orderRepository.findByStatusAndCreatedAtBefore(
"PENDING", Instant.now().minus(30, ChronoUnit.MINUTES));
expired.forEach(order -> cancelOrder(order));
log.info("Cancelled {} expired orders", expired.size());
}
(3) العائد
بعد أن نفّذت Alice المهمة المجدولة، تُمسح الطلبات المتأخرة تلقائيًا كل 5 دقائق، ويُحرر المخزون تلقائيًا، وانخفض تراكم الطلبات المتأخرة من 2000 يوميًا إلى 0. في نفس الوقت، باستخدام @Async لتحويل رسائل الإشعار البريدية إلى تسليم غير متزامن، انخفض وقت استجابة API تقديم الطلب من 800 مللي ثانية إلى 50 مللي ثانية.
3. مهام @Scheduled المجدولة
(1) ثلاث طرق للجدولة
| الطريقة | معاملات الملاحظة | المعنى | مثال |
|---|---|---|---|
| FixedRate | fixedRate |
فاصل ثابت (يُحسب من بداية المرة السابقة) | تشغيل كل 5 دقائق |
| FixedDelay | fixedDelay |
تأخير ثابت (من نهاية المرة السابقة) | انتظار 5 دقائق بعد انتهاء المرة السابقة |
| Cron | cron |
تعبير Cron | تشغيل في الساعة 2:00 صباحًا يوميًا |
gantt
title توقيت FixedRate مقابل FixedDelay
dateFormat X
axisFormat %s
section FixedRate
المهمة A (5دقائق) :a1, 0, 5
المهمة B (5دقائق) :a2, 5, 8
المهمة C (5دقائق) :a3, 10, 14
section FixedDelay
المهمة A (5دقائق) :b1, 0, 5
انتظار 3دقائق :b2, 5, 8
المهمة B (5دقائق) :b3, 8, 13
انتظار 3دقائق :b4, 13, 16
| البُعد | FixedRate | FixedDelay | Cron |
|---|---|---|---|
| فاصل التنفيذ | ثابت (بصرف النظر عن مدة السابق) | ثابت (بعد انتهاء السابق) | وقت محدد |
| تداخل المهام | قد تتداخل | لا تتداخل | قد تتداخل |
| حالات الاستخدام | استطلاع | معالجة بيانات | معالجة دفعات مجدولة |
(1) ▶ مثال:مهمة مجدولة FixedRate
@Component
@EnableScheduling
public class OrderScheduleTask {
private final OrderService orderService;
private static final Logger log = LoggerFactory.getLogger(OrderScheduleTask.class);
public OrderScheduleTask(OrderService orderService) {
this.orderService = orderService;
}
@Scheduled(fixedRate = 5 * 60 * 1000) // كل 5 دقائق
public void cancelExpiredOrders() {
log.info("بدء تنظيف الطلبات المتأخرة...");
int cancelled = orderService.cancelExpiredOrders(Duration.ofMinutes(30));
log.info("تم إلغاء {} طلب متأخر", cancelled);
}
}
الناتج:
// تم التنفيذ بنجاح
(2) ▶ مثال:تعبيرات Cron للمهام المجدولة
@Component
public class DailyReportTask {
@Scheduled(cron = "0 0 2 * * ?") // كل يوم في الساعة 2:00 صباحًا
public void generateDailyReport() {
// إنشاء تقرير إحصائيات الطلبات اليومي
}
@Scheduled(cron = "0 0 0 1 * ?") // أول يوم من كل شهر في منتصف الليل
public void generateMonthlyReport() {
// إنشاء التقرير الشهري
}
}
الناتج:
// تم التنفيذ بنجاح
| حقل Cron | المعنى | القيم الصالحة |
|---|---|---|
| الثواني | 0-59 | |
| الدقائق | الدقائق | 0-59 |
| الساعات | الساعات | 0-23 |
| اليوم | يوم الشهر | 1-31 |
| الشهر | 1-12 | |
| يوم الأسبوع | يوم الأسبوع | 0-7 (0/7 = الأحد) |
| تعبيرات Cron الشائعة | المعنى |
|---|---|
0 0 2 * * ? |
كل يوم في الساعة 2 صباحًا |
0 */5 * * * ? |
كل 5 دقائق |
0 0 0 1 * ? |
منتصف الليل في الأول من كل شهر |
0 0 9-17 * * MON-FRI |
أيام العمل، 9:00 صباحًا - 5:00 مساءً (في بداية كل ساعة) |
4. المعالجة غير المتزامنة @Async
(1) المتزامن مقابل غير المتزامن
(1) ▶ مثال:إرسال الإشعارات بالتوازي (حظر)
@Service
public class OrderService {
public Order createOrder(CreateOrderRequest request) {
Order order = saveOrder(request);
emailService.sendConfirmation(order); // يحظر لمدة 500 مللي ثانية
smsService.sendNotification(order); // يحظر لمدة 200 مللي ثانية
return order; // الإجمالي: 700 مللي ثانية مهدر
}
}
الناتج:
// تم التنفيذ بنجاح
(2) ▶ مثال:إرسال الإشعارات بشكل غير متزامن (بدون حظر)
@Service
@EnableAsync
public class NotificationService {
@Async
public void sendOrderConfirmation(Order order) {
// يعمل في خيط منفصل
// لا يحظر المستدعي
emailService.sendConfirmation(order);
log.info("تم إرسال بريد التأكيد للطلب {}", order.getId());
}
@Async
public CompletableFuture<String> sendSmsNotification(Order order) {
smsService.sendNotification(order);
return CompletableFuture.completedFuture("تم إرسال SMS");
}
}
// في OrderService
@Service
public class OrderService {
private final NotificationService notificationService;
public Order createOrder(CreateOrderRequest request) {
Order order = saveOrder(request);
notificationService.sendOrderConfirmation(order); // بدون حظر
return order; // يعود فورًا
}
}
الناتج:
// تم التنفيذ بنجاح
| البُعد | المتزامن | غير المتزامن |
|---|---|---|
| طريقة الاستدعاء | حظر وانتظار النتيجة | العودة فورًا؛ التنفيذ في الخلفية |
| وقت الاستجابة | إجمالي وقت جميع العمليات | وقت العمليات الأساسية فقط |
| معالجة الأخطاء | معالجة مباشرة try-catch | AsyncUncaughtExceptionHandler |
| حالات الاستخدام | عمليات تتطلب انتظار النتائج | إشعارات، سجلات، عمليات غير حرجة |
5. تجمع خيوط مخصص
(1) مشكلات تجمع الخيوط الافتراضي
Spring Boot يستخدم SimpleAsyncTaskExecutor افتراضيًا، الذي ينشئ خيطًا جديدًا لكل مهمة ولا يضع حدًا لعدد الخيوط، مما قد يؤدي إلى OutOfMemoryError.
(1) ▶ مثال:تخصيص ThreadPoolTaskExecutor
@Configuration
@EnableAsync
public class AsyncConfig implements AsyncConfigurer {
@Override
public Executor getAsyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(5);
executor.setMaxPoolSize(20);
executor.setQueueCapacity(100);
executor.setThreadNamePrefix("orderflow-async-");
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
executor.setWaitForTasksToCompleteOnShutdown(true);
executor.setAwaitTerminationSeconds(60);
executor.initialize();
return executor;
}
@Override
public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
return (ex, method, params) -> {
log.error("استثناء غير متزامن في الطريقة {}: {}", method.getName(), ex.getMessage(), ex);
};
}
}
الناتج:
// تم التنفيذ بنجاح
| المعامل | المعنى | القيمة الموصى بها |
|---|---|---|
corePoolSize |
عدد الخيوط الأساسية (مقيمة) | عدد أنوية المعالج |
maxPoolSize |
الحد الأقصى لعدد الخيوط | عدد أنوية المعالج x 2 |
queueCapacity |
سعة الطابور | 100-1,000 |
threadNamePrefix |
بادئة اسم الخيط | اسم المشروع-async- |
rejectedExecutionHandler |
سياسة الرفض | CallerRunsPolicy |
| سياسة الرفض | السلوك | السيناريوهات المطبقة |
|---|---|---|
AbortPolicy |
رمي RejectedExecutionException | الافتراضية، فشل سريع |
CallerRunsPolicy |
التنفيذ بواسطة خيط المستدعي | تخفيض دون تجاهل |
DiscardPolicy |
تجاهل صامت | خسارة مقبولة |
DiscardOldestPolicy |
تجاهل أقدم مهمة | إعطاء أولوية للأداء الفوري |
6. معالجة الاستثناءات غير المتزامنة
(1) ▶ مثال:AsyncUncaughtExceptionHandler
@Configuration
@EnableAsync
public class AsyncConfig implements AsyncConfigurer {
private static final Logger log = LoggerFactory.getLogger(AsyncConfig.class);
@Override
public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
return (throwable, method, objects) -> {
log.error("الطريقة غير المتزامنة [{}] رمت استثناء: {}",
method.getName(), throwable.getMessage(), throwable);
// يمكن أيضًا النشر إلى نظام المراقبة
};
}
}
الناتج:
// تم التنفيذ بنجاح
@Async من نوع void لا تُنقل إلى المستدعي ويجب معالجتها باستخدام AsyncUncaughtExceptionHandler. يمكن معالجة الاستثناءات التي ترميها الطرق التي تُرجع CompletableFuture باستخدام future.exceptionally().
7. مثال شامل: إلغاء تأخير OrderFlow + إشعار غير متزامن
// OrderScheduleTask.java
package com.orderflow.schedule;
import com.orderflow.service.OrderService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.time.Duration;
@Component
public class OrderScheduleTask {
private static final Logger log = LoggerFactory.getLogger(OrderScheduleTask.class);
private final OrderService orderService;
public OrderScheduleTask(OrderService orderService) {
this.orderService = orderService;
}
@Scheduled(fixedRateString = "${orderflow.order.expiry-check-interval:300000}")
public void cancelExpiredOrders() {
int timeoutMinutes = 30;
int cancelled = orderService.cancelExpiredOrders(Duration.ofMinutes(timeoutMinutes));
if (cancelled > 0) {
log.info("تم إلغاء {} طلب متأخر لمدة {} دقيقة", cancelled, timeoutMinutes);
}
}
@Scheduled(cron = "0 0 2 * * ?")
public void generateDailyReport() {
log.info("جارٍ إنشاء تقرير الطلبات اليومي...");
orderService.generateDailyReport();
}
}
// NotificationService.java
package com.orderflow.service;
import com.orderflow.model.Order;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import java.util.concurrent.CompletableFuture;
@Service
public class NotificationService {
private static final Logger log = LoggerFactory.getLogger(NotificationService.class);
@Async
public void sendOrderConfirmation(Order order) {
// محاكاة إرسال البريد الإلكتروني
log.info("إرسال بريد تأكيد للطلب {}", order.getId());
}
@Async
public void sendCancellationNotice(Order order) {
log.info("إرسال إشعار إلغاء للطلب {}", order.getId());
}
@Async
public CompletableFuture<String> sendSmsNotification(Order order) {
log.info("إرسال SMS للطلب {}", order.getId());
return CompletableFuture.completedFuture("تم إرسال SMS للطلب " + order.getId());
}
}
// AsyncConfig.java
@Configuration
@EnableAsync
@EnableScheduling
public class AsyncConfig implements AsyncConfigurer {
private static final Logger log = LoggerFactory.getLogger(AsyncConfig.class);
@Override
public Executor getAsyncExecutor() {
ThreadPoolTaskExecutor exec = new ThreadPoolTaskExecutor();
exec.setCorePoolSize(5);
exec.setMaxPoolSize(20);
exec.setQueueCapacity(100);
exec.setThreadNamePrefix("orderflow-async-");
exec.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
exec.setWaitForTasksToCompleteOnShutdown(true);
exec.setAwaitTerminationSeconds(60);
exec.initialize();
return exec;
}
@Override
public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
return (ex, method, params) ->
log.error("خطأ غير متزامن في {}: {}", method.getName(), ex.getMessage(), ex);
}
}
❓ أسئلة شائعة
TaskScheduler، أو أوكل العمليات المستهلكة للوقت إلى تجمع الخيوط باستخدام @Async.fixedRate وfixedDelay للمهام طويلة التشغيل؟fixedRate يحسب من وقت بداية المهمة السابقة؛ إذا استغرقت المهمة وقتًا أطول من الفاصل، ستعمل متتالية أو حتى تتداخل مع مهام أخرى. fixedDelay يحسب من وقت نهاية المهمة السابقة، مما يضمن فاصلًا ثابتًا بين المهام حتى لا تتداخل.ScheduledTaskRegistrar لتسجيل المهام برمجيًا، أو استخدام مركز التكوين للتحديث الديناميكي. حل بسيط: fixedRateString = "${interval}"—تحكم بها عبر تكوين خارجي.CompletableFuture وأخرى تُرجع void؟void هي "أطلق وانسَ"؛ المستدعي لا يهتم بالنتيجة، والاستثناءات تُعالج بواسطة AsyncUncaughtExceptionHandler. طرق CompletableFuture تسمح بالمعالجة المتسلسلة للنتائج والاستثناءات، مما يمنح المستدعي تحكمًا أكبر.📖 ملخص
@Scheduledثلاث طرق للجدولة: fixedRate (فاصل ثابت)، fixedDelay (تأخير ثابت)، cron (وقت محدد)@Asyncيمنع العمليات المستهلكة للوقت من حظر المستدعي؛ مناسب للعمليات غير الحرجة مثل الإشعارات والتسجيل- تجمع الخيوط الافتراضي لا يضع حدًا لعدد الخيوط؛ في بيئة الإنتاج يجب تكوين
ThreadPoolTaskExecutorبنفسك AsyncUncaughtExceptionHandlerلمعالجة الاستثناءات في الطرق غير المتزامنة من نوع void- عند نشر مثيلات متعددة للمهمة المجدولة، يلزم قفل موزع (مثل ShedLock) لمنع التنفيذ المكرر
- الاستدعاء المتبادل لطرق
@Asyncداخل نفس الفئة لا يعمل؛ تحتاج إلى تفكيك الفئة أو حقن وكيل
📝 تمارين
-
تمرين أساسي (صعوبة ⭐): نفِّذ مهمة مجدولة لـ OrderFlow لإلغاء الطلبات المتأخرة تلقائيًا. كل 5 دقائق، ابحث عن وألغِ أي طلبات PENDING غير المدفوعة لأكثر من 30 دقيقة.
-
تمرين متقدم (صعوبة: ⭐⭐): عدّل رسائل تأكيد الطلب والإشعارات عبر SMS لتُرسل بشكل غير متزامن باستخدام @Async، وخصص تجمع الخيوط (core=5, max=20)، وتحقق من أن وقت استجابة API تقديم الطلب انخفض بشكل ملحوظ.
-
تحدي (صعوبة: ⭐⭐⭐): ادمج ShedLock لتنفيذ قفل موزع للمهام المجدولة، مما يضمن عند نشر OrderFlow عبر مثيلات متعددة أن مثيلًا واحدًا فقط ينفذ المهمة المجدولة في أي وقت. فكر في دقة القفل وتصميم المهلة.



