404 Not Found

404 Not Found


nginx

المهام المجدولة والمعالجة غير المتزامنة

المهام المجدولة والمعالجة غير المتزامنة هما مفتاح تعزيز كفاءة الخلفية—جدول ما يحتاج للجدولة وشغّل بالتوازي ما يمكن تشغيله بالتوازي، حتى لا يضطر المستخدمون للانتظار بلا داعٍ.

1. ما ستتعلمه


2. قصة حقيقية من مطور أعمال

(1) نقطة الألم: تراكم الطلبات المتأخرة

اكتشفت Alice تراكمًا كبيرًا من الطلبات غير المدفوعة في OrderFlow—المستخدمون يقدمون طلبات لكن لا يدفعون، مما يحجز المخزون ويمنع مستخدمين آخرين من الشراء. أبلغ Charlie أن هناك حوالي 2000 طلب متأخرة يوميًا، تحجز مجتمعة مخزونًا بقيمة 50,000 دولار. سابقًا، كانت الطلبات تُلغى يدويًا بفحص قاعدة البيانات كل 30 دقيقة، وهو ما كان غير فعال وعرضة للسهو.

(2) حلول @Scheduled

Spring Boot يتضمن مهام مجدولة مدمجة يمكن تنفيذها بتعليق توضيحي واحد:

JAVA
@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 صباحًا يوميًا
100%
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

JAVA
@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);
    }
}

الناتج:

TEXT
// تم التنفيذ بنجاح

(2) ▶ مثال:تعبيرات Cron للمهام المجدولة

JAVA
@Component
public class DailyReportTask {

    @Scheduled(cron = "0 0 2 * * ?")  // كل يوم في الساعة 2:00 صباحًا
    public void generateDailyReport() {
        // إنشاء تقرير إحصائيات الطلبات اليومي
    }

    @Scheduled(cron = "0 0 0 1 * ?")  // أول يوم من كل شهر في منتصف الليل
    public void generateMonthlyReport() {
        // إنشاء التقرير الشهري
    }
}

الناتج:

TEXT
// تم التنفيذ بنجاح
حقل 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) ▶ مثال:إرسال الإشعارات بالتوازي (حظر)

JAVA
@Service
public class OrderService {

    public Order createOrder(CreateOrderRequest request) {
        Order order = saveOrder(request);
        emailService.sendConfirmation(order);   // يحظر لمدة 500 مللي ثانية
        smsService.sendNotification(order);       // يحظر لمدة 200 مللي ثانية
        return order;  // الإجمالي: 700 مللي ثانية مهدر
    }
}

الناتج:

TEXT
// تم التنفيذ بنجاح

(2) ▶ مثال:إرسال الإشعارات بشكل غير متزامن (بدون حظر)

JAVA
@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;  // يعود فورًا
    }
}

الناتج:

TEXT
// تم التنفيذ بنجاح
البُعد المتزامن غير المتزامن
طريقة الاستدعاء حظر وانتظار النتيجة العودة فورًا؛ التنفيذ في الخلفية
وقت الاستجابة إجمالي وقت جميع العمليات وقت العمليات الأساسية فقط
معالجة الأخطاء معالجة مباشرة try-catch AsyncUncaughtExceptionHandler
حالات الاستخدام عمليات تتطلب انتظار النتائج إشعارات، سجلات، عمليات غير حرجة

5. تجمع خيوط مخصص

(1) مشكلات تجمع الخيوط الافتراضي

Spring Boot يستخدم SimpleAsyncTaskExecutor افتراضيًا، الذي ينشئ خيطًا جديدًا لكل مهمة ولا يضع حدًا لعدد الخيوط، مما قد يؤدي إلى OutOfMemoryError.

(1) ▶ مثال:تخصيص ThreadPoolTaskExecutor

JAVA
@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);
        };
    }
}

الناتج:

TEXT
// تم التنفيذ بنجاح
المعامل المعنى القيمة الموصى بها
corePoolSize عدد الخيوط الأساسية (مقيمة) عدد أنوية المعالج
maxPoolSize الحد الأقصى لعدد الخيوط عدد أنوية المعالج x 2
queueCapacity سعة الطابور 100-1,000
threadNamePrefix بادئة اسم الخيط اسم المشروع-async-
rejectedExecutionHandler سياسة الرفض CallerRunsPolicy
سياسة الرفض السلوك السيناريوهات المطبقة
AbortPolicy رمي RejectedExecutionException الافتراضية، فشل سريع
CallerRunsPolicy التنفيذ بواسطة خيط المستدعي تخفيض دون تجاهل
DiscardPolicy تجاهل صامت خسارة مقبولة
DiscardOldestPolicy تجاهل أقدم مهمة إعطاء أولوية للأداء الفوري

6. معالجة الاستثناءات غير المتزامنة

(1) ▶ مثال:AsyncUncaughtExceptionHandler

JAVA
@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);
            // يمكن أيضًا النشر إلى نظام المراقبة
        };
    }
}

الناتج:

TEXT
// تم التنفيذ بنجاح
📌 نقطة رئيسية: الاستثناءات التي ترميها طرق @Async من نوع void لا تُنقل إلى المستدعي ويجب معالجتها باستخدام AsyncUncaughtExceptionHandler. يمكن معالجة الاستثناءات التي ترميها الطرق التي تُرجع CompletableFuture باستخدام future.exceptionally().


7. مثال شامل: إلغاء تأخير OrderFlow + إشعار غير متزامن

JAVA
// 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);
    }
}

❓ أسئلة شائعة

س هل يمكن تنفيذ طرق @Scheduled في خيط واحد فقط؟
ج افتراضيًا، نعم. طرق @Scheduled المتعددة تشترك في جدول خيط واحد، لذا المهام طويلة التشغيل ستحظر مهام أخرى. الحل: كوّن تجمع خيوط TaskScheduler، أو أوكل العمليات المستهلكة للوقت إلى تجمع الخيوط باستخدام @Async.
س هل يعمل @Async عند الاستدعاء داخل نفس الفئة؟
ج لا. مثل @Transactional، يعتمد @Async على وكيل AOP، والاستدعاءات بين طرق نفس الفئة تتجاوز الوكيل. الحلول: 1) فصلها إلى خدمات مختلفة؛ 2) حقن الوكيل الخاص بك.
س ما الفرق بين fixedRate وfixedDelay للمهام طويلة التشغيل؟
ج fixedRate يحسب من وقت بداية المهمة السابقة؛ إذا استغرقت المهمة وقتًا أطول من الفاصل، ستعمل متتالية أو حتى تتداخل مع مهام أخرى. fixedDelay يحسب من وقت نهاية المهمة السابقة، مما يضمن فاصلًا ثابتًا بين المهام حتى لا تتداخل.
س كيف نمنع المهام المجدولة من العمل عدة مرات عبر مثيلات مختلفة في بيئة الإنتاج؟
ج 1) أقفال صفوف قاعدة البيانات (SELECT FOR UPDATE)؛ 2) أقفال موزعة (Redis/ShedLock)؛ 3) تفعيل الجدولة على مثيل واحد فقط. نوصي بمكتبة ShedLock المصممة خصيصًا لمعالجة هذه المشكلة.
س كيف يمكنني تغيير وقت المهمة المجدولة ديناميكيًا؟
ج يمكنك استخدام ScheduledTaskRegistrar لتسجيل المهام برمجيًا، أو استخدام مركز التكوين للتحديث الديناميكي. حل بسيط: fixedRateString = "${interval}"—تحكم بها عبر تكوين خارجي.
س ما الفرق بين طريقة غير متزامنة تُرجع CompletableFuture وأخرى تُرجع void؟
ج طريقة void هي "أطلق وانسَ"؛ المستدعي لا يهتم بالنتيجة، والاستثناءات تُعالج بواسطة AsyncUncaughtExceptionHandler. طرق CompletableFuture تسمح بالمعالجة المتسلسلة للنتائج والاستثناءات، مما يمنح المستدعي تحكمًا أكبر.

📖 ملخص


📝 تمارين

  1. تمرين أساسي (صعوبة ⭐): نفِّذ مهمة مجدولة لـ OrderFlow لإلغاء الطلبات المتأخرة تلقائيًا. كل 5 دقائق، ابحث عن وألغِ أي طلبات PENDING غير المدفوعة لأكثر من 30 دقيقة.

  2. تمرين متقدم (صعوبة: ⭐⭐): عدّل رسائل تأكيد الطلب والإشعارات عبر SMS لتُرسل بشكل غير متزامن باستخدام @Async، وخصص تجمع الخيوط (core=5, max=20)، وتحقق من أن وقت استجابة API تقديم الطلب انخفض بشكل ملحوظ.

  3. تحدي (صعوبة: ⭐⭐⭐): ادمج ShedLock لتنفيذ قفل موزع للمهام المجدولة، مما يضمن عند نشر OrderFlow عبر مثيلات متعددة أن مثيلًا واحدًا فقط ينفذ المهمة المجدولة في أي وقت. فكر في دقة القفل وتصميم المهلة.

Web-Tutorial.com

فريق Web-Tutorial التقني

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

100%