← Назад к вопросам

Сколько операций в день по отправке уведомлений происходит в приложении онлайн школы?

1.3 Junior🔥 41 комментариев
#REST API и микросервисы#Soft Skills и карьера

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI23 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Масштабирование уведомлений в приложении онлайн школы

Это отличный вопрос о системном проектировании. Кол-во операций зависит от размера школы, но давайте посчитаем для реальной онлайн школы.

Сценарий: школа с 10,000 студентов

Типы уведомлений:

1. Напоминание о уроке (за 15 минут)
   - 200 уроков в день × 10,000 студентов = 2,000,000 уведомлений
   - Но только 30% слушают = 600,000 реальных

2. Домашнее задание выложено
   - 500 заданий × 10,000 = 5,000,000 потенциальных
   - Но только 40% активных студентов = 2,000,000

3. Оценка выставлена
   - 1,000 оценок × 10,000 = 10,000,000 потенциальных
   - Реально: 500,000 (только когда выставляют)

4. Сообщение от учителя
   - 50 учителей × 100 сообщений в день × 10,000 = 50,000,000
   - Реально: 500,000

5. Системные уведомления
   - Важные события: ~100,000 в день

ИТОГО В ДЕНЬ: 3,600,000 уведомлений

В пике (во время занятий):

Пиковое время: 9 AM - 5 PM (8 часов)

3,600,000 / 8 часов = 450,000 в час
450,000 / 60 минут = 7,500 в минуту
7,500 / 60 секунд = 125 в секунду

Пиковой нагрузки:
- Экспоненциально выше в начале занятия (напоминания)
- Максимум: 500-1000 в секунду в самый пик

Архитектура для масштабирования

❌ Неправильно: синхронно прямо в HTTP

@RestController
@PostMapping("/lessons/{id}/start")
public void startLesson(@PathVariable Long id) {
    lesson.start();
    
    // Отправляем уведомления 10,000 студентам
    for (Student student : lesson.getStudents()) {
        sendNotification(student);  // ← Синхронно!
    }
    // Request висит 30+ секунд!
}

// Проблемы:
// - Client ждёт 30 сек
// - Если упадёт отправка, упадёт весь endpoint
// - Один request блокирует thread

✅ Правильно: асинхронная очередь (Message Queue)

@RestController
@PostMapping("/lessons/{id}/start")
public ResponseEntity<LessonResponse> startLesson(@PathVariable Long id) {
    Lesson lesson = lessonService.start(id);
    
    // Кладём задачу в очередь
    notificationQueue.push(
        new NotificationJob(
            "lesson_started",
            lesson.getId(),
            lesson.getStudentIds()
        )
    );
    
    // Сразу возвращаем ответ
    return ResponseEntity.ok(new LessonResponse(lesson));
}

// Workers обрабатывают очередь асинхронно
@Service
public class NotificationWorker {
    
    @Transactional
    public void processNotificationJob(NotificationJob job) {
        // Worker #1 отправляет 1000 уведомлений
        // Worker #2 отправляет 1000 уведомлений
        // Worker #3 отправляет 1000 уведомлений
        // ... в параллели
        
        for (Long studentId : job.getStudentIds()) {
            sendNotificationAsync(studentId, job);
        }
    }
}

Диаграмма системы

┌─────────────┐
│ REST API    │
│ (sync)      │
└──────┬──────┘
       │
       ├─→ Save to DB
       │
       └─→ Push to Queue (RabbitMQ, Kafka)
            │
            ├─→ Job: "send_lesson_notification"
            │   studentIds: [1,2,3,...,10000]
            │   lessonId: 123
            │
            └─→ Queue (fast, in-memory)
                 ↓
         ┌──────────────────┐
         │ Worker Pool (10) │
         │                  │
         │ Worker #1  ─┐
         │ Worker #2  ├─→ Send Email
         │ Worker #3  │   Send SMS
         │ Worker #4  │   Send In-App
         │ ...        │   Send Push
         │ Worker #10 ─┐
         └──────────────────┘
                │
                └─→ External Services
                    ├─ SendGrid (Email)
                    ├─ Twilio (SMS)
                    ├─ Firebase (Push)
                    └─ Custom (In-App)

Реализация: RabbitMQ + Workers

// 1. Конфигурация
@Configuration
public class RabbitConfig {
    
    public static final String NOTIFICATION_QUEUE = "notifications";
    public static final String NOTIFICATION_EXCHANGE = "notifications_exchange";
    
    @Bean
    public Queue notificationQueue() {
        return new Queue(NOTIFICATION_QUEUE, true);  // durable
    }
    
    @Bean
    public DirectExchange notificationExchange() {
        return new DirectExchange(NOTIFICATION_EXCHANGE);
    }
    
    @Bean
    public Binding notificationBinding(Queue queue, DirectExchange exchange) {
        return BindingBuilder.bind(queue)
            .to(exchange)
            .with("notification.#");
    }
}

// 2. Издатель (Push в очередь)
@Service
public class NotificationPublisher {
    
    @Autowired
    private RabbitTemplate rabbitTemplate;
    
    public void publishLessonStarted(Long lessonId, List<Long> studentIds) {
        NotificationMessage message = new NotificationMessage();
        message.setType("lesson_started");
        message.setLessonId(lessonId);
        message.setStudentIds(studentIds);
        message.setTimestamp(LocalDateTime.now());
        
        // Быстро кладём в очередь
        rabbitTemplate.convertAndSend(
            NOTIFICATION_EXCHANGE,
            "notification.lesson_started",
            message
        );
    }
}

// 3. Consumesr (Workers, обрабатывают очередь)
@Service
public class NotificationConsumer {
    
    @Autowired
    private EmailService emailService;
    
    @Autowired
    private PushNotificationService pushService;
    
    @RabbitListener(queues = NOTIFICATION_QUEUE, concurrency = "10")
    public void processNotification(NotificationMessage message) {
        try {
            switch (message.getType()) {
                case "lesson_started":
                    sendLessonReminders(message);
                    break;
                case "homework_assigned":
                    sendHomeworkNotifications(message);
                    break;
                case "grade_posted":
                    sendGradeNotifications(message);
                    break;
            }
        } catch (Exception e) {
            log.error("Failed to send notification", e);
            // Dead letter queue для retry
        }
    }
    
    private void sendLessonReminders(NotificationMessage message) {
        List<Student> students = studentRepository.findByIds(message.getStudentIds());
        
        students.parallelStream()  // ← параллельно!
            .forEach(student -> {
                // Email
                emailService.send(
                    student.getEmail(),
                    "Lesson Reminder",
                    "Your lesson starts in 15 minutes"
                );
                
                // Push
                if (student.hasNotificationsEnabled()) {
                    pushService.send(
                        student.getDeviceToken(),
                        "Lesson Reminder"
                    );
                }
                
                // In-App
                inAppNotificationService.create(student.getId(), message);
            });
    }
}

Оптимизация: батчевая обработка

// ❌ Неэффективно: для каждого студента отдельный запрос
for (Student student : students) {
    emailService.send(student.getEmail(), message);
    // 10,000 отдельных HTTP запросов к SendGrid
    // Медленно!
}

// ✅ Эффективно: батчи
public void sendBatch(List<Student> students, String message) {
    // Разделяем на батчи по 100
    List<List<Student>> batches = Lists.partition(students, 100);
    
    for (List<Student> batch : batches) {
        // Один HTTP запрос к SendGrid за 100 писем
        SendGridRequest request = new SendGridRequest();
        
        for (Student student : batch) {
            request.addRecipient(
                student.getEmail(),
                student.getName()
            );
        }
        
        sendGridClient.sendBatch(request);
    }
    // 100 HTTP запросов вместо 10,000!
}

Масштабирование Workers

Есть 3,600,000 уведомлений в день
Пиковая нагрузка: 1000 в секунду

Если один worker обрабатывает 10 уведомлений в сек:

1000 уведомлений/сек ÷ 10 уведомлений/worker/сек = 100 workers

Конфигурация:
┌─────────────────┬──────────────┐
│ Сценарий        │ Workers      │
├─────────────────┼──────────────┤
│ Нормальная      │ 10           │
│ Пиковая нагрузка│ 100          │
│ Emergency       │ 200 (auto)   │
└─────────────────┴──────────────┘

Spring конфигурация:

# application.properties
spring.rabbitmq.listener.simple.concurrency=10
spring.rabbitmq.listener.simple.max-concurrency=200
spring.rabbitmq.listener.simple.prefetch=1

Мониторинг и алерты

@Component
public class NotificationMetrics {
    
    @Autowired
    private MeterRegistry meterRegistry;
    
    public void recordNotificationSent(String type) {
        meterRegistry.counter(
            "notifications.sent",
            "type", type
        ).increment();
    }
    
    public void recordNotificationFailed(String type, Exception e) {
        meterRegistry.counter(
            "notifications.failed",
            "type", type,
            "error", e.getClass().getSimpleName()
        ).increment();
    }
}

// Prometheus alert
ALERT NotificationQueueBacklog
  IF rabbitmq_queue_messages_ready > 10000
  FOR 5m
  THEN SEND EMAIL (срочно добавить workers)

Estimated resources

Для 3,600,000 уведомлений в день:

┌────────────────────┬──────────┐
│ Компонент          │ Ресурсы  │
├────────────────────┼──────────┤
│ RabbitMQ           │ 8GB RAM  │
│ Worker Pool        │ 4 CPU    │
│ Database           │ 16GB RAM │
│ Email Service      │ 2 CPU    │
│ Cache (Redis)      │ 4GB RAM  │
│ Monitoring         │ 2 CPU    │
└────────────────────┴──────────┘

Вывод

Для онлайн школы с 10,000 студентов:

  • Кол-во операций: ~3,600,000 уведомлений в день
  • Пиковая нагрузка: 1000+ в секунду
  • Архитектура: Message Queue + Worker Pool
  • Message Broker: RabbitMQ, Kafka, или AWS SQS
  • Workers: 10 в норме, 100+ в пике (auto-scaling)
  • Ключ: асинхронность, батчи, параллелизм

Никогда не отправляй 10,000 уведомлений синхронно из одного HTTP request!

Сколько операций в день по отправке уведомлений происходит в приложении онлайн школы? | PrepBro