← Назад к вопросам
Сколько операций в день по отправке уведомлений происходит в приложении онлайн школы?
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!