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

Реализовывал ли асинхронное взаимодействие через REST

1.7 Middle🔥 131 комментариев
#REST API и микросервисы

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

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

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

Ответ: Реализовывал ли асинхронное взаимодействие через REST

Прямой ответ

Да, я реализовывал асинхронное взаимодействие через REST не один раз на разных проектах. Это один из наиболее часто встречающихся паттернов в современных распределённых системах. Расскажу о подходах и примерах из реальных проектов.

Когда нужно асинхронное взаимодействие?

Синхронное REST (проблема):
Клиент                       Сервер A          Сервер B
   │                            │                  │
   ├──────POST request──────────►│                  │
   │                            ├──── POST ────────►│
   │                            │   (ожидание)      │
   │                            │                  ├─ обработка...
   │                            │◄──── 200 OK ─────┤
   │◄──────────200 OK───────────┤                  │
   │                                               │
   └── Если это долгая операция (10+ сек) ────────┘
   └── Клиент зависает, timeout, плохой UX

Асинхронное REST (решение):
Клиент                       Сервер A          Queue/Broker
   │                            │                  │
   ├──────POST request──────────►│                  │
   │◄──────202 ACCEPTED──────────┤                  │
   │(с Location и/или job-id)    ├─ Поставить ────►│
   │                            │ (быстро)          │
   │                                               │
   (later)
   │──────GET /jobs/123──────────►│                  │
   │◄──────200 + status──────────┤                  │

Подход 1: Polling (регулярный опрос)

@RestController
@RequestMapping("/api/v1/jobs")
public class JobController {
    
    @Autowired
    private JobService jobService;
    
    @Autowired
    private JobRepository jobRepository;
    
    // Шаг 1: Клиент отправляет запрос
    @PostMapping("/process")
    public ResponseEntity<?> submitJob(@RequestBody JobRequest request) {
        // Быстро создаём запись и возвращаемся
        Job job = jobService.createJob(request);
        
        // Асинхронное выполнение через @Async
        jobService.processJobAsync(job.getId());
        
        // Возвращаем 202 Accepted + location для polling
        return ResponseEntity
            .status(HttpStatus.ACCEPTED)
            .header("Location", "/api/v1/jobs/" + job.getId())
            .body(Map.of("job_id", job.getId()));
    }
    
    // Шаг 2: Клиент регулярно проверяет статус
    @GetMapping("/{jobId}")
    public ResponseEntity<?> getJobStatus(@PathVariable Long jobId) {
        Job job = jobRepository.findById(jobId)
            .orElseThrow(() -> new ResourceNotFoundException("Job not found"));
        
        return ResponseEntity.ok(Map.of(
            "job_id", job.getId(),
            "status", job.getStatus(),  // PENDING, PROCESSING, COMPLETED, FAILED
            "progress", job.getProgress(),
            "result", job.getResult()
        ));
    }
}

@Service
public class JobService {
    
    @Autowired
    private JobRepository jobRepository;
    
    @Autowired
    private ExternalApiClient apiClient;
    
    // Асинхронное выполнение
    @Async
    public void processJobAsync(Long jobId) {
        try {
            Job job = jobRepository.findById(jobId).get();
            job.setStatus("PROCESSING");
            jobRepository.save(job);
            
            // Долгая операция (например, вызов внешнего API)
            String result = apiClient.processData(job.getData());
            
            job.setStatus("COMPLETED");
            job.setResult(result);
            jobRepository.save(job);
        } catch (Exception e) {
            Job job = jobRepository.findById(jobId).get();
            job.setStatus("FAILED");
            job.setError(e.getMessage());
            jobRepository.save(job);
        }
    }
}

Пример использования (клиент):

public class AsyncRestClient {
    
    @Autowired
    private RestTemplate restTemplate;
    
    public void submitAndPoll() {
        // 1. Отправляем запрос
        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.APPLICATION_JSON);
        
        HttpEntity<JobRequest> entity = new HttpEntity<>(
            new JobRequest("data"),
            headers
        );
        
        ResponseEntity<Map> response = restTemplate.postForEntity(
            "http://api.example.com/api/v1/jobs/process",
            entity,
            Map.class
        );
        
        String jobId = (String) response.getBody().get("job_id");
        System.out.println("Job submitted with ID: " + jobId);
        
        // 2. Регулярно проверяем статус
        String status = "PENDING";
        while (!status.equals("COMPLETED") && !status.equals("FAILED")) {
            try {
                Thread.sleep(2000);  // Ждём 2 секунды
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
            
            ResponseEntity<Map> statusResponse = restTemplate.getForEntity(
                "http://api.example.com/api/v1/jobs/" + jobId,
                Map.class
            );
            
            status = (String) statusResponse.getBody().get("status");
            System.out.println("Job status: " + status);
        }
        
        System.out.println("Job finished!");
    }
}

Подход 2: Webhooks (обратные вызовы)

@RestController
@RequestMapping("/api/v1/jobs")
public class JobController {
    
    @Autowired
    private JobService jobService;
    
    @PostMapping("/process")
    public ResponseEntity<?> submitJobWithWebhook(
            @RequestBody JobRequest request,
            @RequestParam(required = false) String webhookUrl) {
        
        Job job = jobService.createJob(request);
        
        // Сохраняем webhook URL для обратного вызова
        if (webhookUrl != null) {
            job.setWebhookUrl(webhookUrl);
        }
        jobRepository.save(job);
        
        // Асинхронное выполнение
        jobService.processJobAsyncWithWebhook(job.getId());
        
        return ResponseEntity
            .status(HttpStatus.ACCEPTED)
            .body(Map.of("job_id", job.getId()));
    }
}

@Service
public class JobService {
    
    @Autowired
    private RestTemplate restTemplate;
    
    @Async
    public void processJobAsyncWithWebhook(Long jobId) {
        Job job = jobRepository.findById(jobId).get();
        
        try {
            job.setStatus("PROCESSING");
            jobRepository.save(job);
            
            // Долгая операция
            String result = processData(job.getData());
            
            job.setStatus("COMPLETED");
            job.setResult(result);
            jobRepository.save(job);
            
            // Шаг 3: Отправляем webhook
            if (job.getWebhookUrl() != null) {
                sendWebhook(job);
            }
        } catch (Exception e) {
            job.setStatus("FAILED");
            job.setError(e.getMessage());
            jobRepository.save(job);
            
            if (job.getWebhookUrl() != null) {
                sendWebhook(job);
            }
        }
    }
    
    private void sendWebhook(Job job) {
        try {
            HttpHeaders headers = new HttpHeaders();
            headers.setContentType(MediaType.APPLICATION_JSON);
            headers.set("X-Signature", generateSignature(job));
            
            HttpEntity<Map> entity = new HttpEntity<>(
                Map.of(
                    "job_id", job.getId(),
                    "status", job.getStatus(),
                    "result", job.getResult()
                ),
                headers
            );
            
            restTemplate.postForObject(
                job.getWebhookUrl(),
                entity,
                String.class
            );
        } catch (Exception e) {
            // Логируем ошибку webhook
            System.err.println("Failed to send webhook: " + e.getMessage());
            // Можно добавить retry logic
        }
    }
    
    private String generateSignature(Job job) {
        // Подпись для безопасности
        return "...";
    }
}

Пример webhook на клиенте:

@RestController
@RequestMapping("/webhooks")
public class WebhookController {
    
    @PostMapping("/job-completed")
    public ResponseEntity<?> handleJobCompleted(@RequestBody JobCompletionEvent event) {
        System.out.println("Job " + event.getJobId() + " completed");
        System.out.println("Result: " + event.getResult());
        
        // Обработка результата
        processResult(event);
        
        return ResponseEntity.ok("Received");
    }
}

Подход 3: Message Queue (очередь сообщений)

@RestController
@RequestMapping("/api/v1/orders")
public class OrderController {
    
    @Autowired
    private OrderService orderService;
    
    @Autowired
    private KafkaTemplate<String, OrderEvent> kafkaTemplate;
    
    @PostMapping("/")
    public ResponseEntity<?> createOrder(@RequestBody OrderRequest request) {
        // Быстро создаём заказ в БД
        Order order = orderService.createOrder(request);
        
        // Отправляем событие в Kafka
        kafkaTemplate.send("orders-topic",
            new OrderEvent(order.getId(), "ORDER_CREATED", order));
        
        return ResponseEntity
            .status(HttpStatus.ACCEPTED)
            .body(Map.of(
                "order_id", order.getId(),
                "status", "PENDING"
            ));
    }
}

// Асинхронный обработчик
@Component
public class OrderEventListener {
    
    @KafkaListener(topics = "orders-topic")
    public void handleOrderEvent(OrderEvent event) {
        if ("ORDER_CREATED".equals(event.getType())) {
            // Обработка заказа (может быть долгой)
            processOrder(event.getOrder());
        }
    }
    
    private void processOrder(Order order) {
        // 1. Проверка запасов
        // 2. Обработка платежа
        // 3. Отправка уведомления
        // 4. Планирование доставки
        // Всё это может быть долгим, но клиент уже получил ответ!
    }
}

Подход 4: Reactive / Non-blocking (Spring WebFlux)

@RestController
@RequestMapping("/api/v1/data")
public class DataController {
    
    @Autowired
    private DataService dataService;
    
    // Возвращает Mono (Single value) в future
    @GetMapping("/{id}")
    public Mono<ResponseEntity<DataResponse>> getData(@PathVariable Long id) {
        return dataService.fetchDataAsync(id)
            .map(data -> ResponseEntity.ok(new DataResponse(data)))
            .onErrorResume(error -> 
                Mono.just(ResponseEntity.status(500).build()));
    }
    
    // Server-Sent Events (SSE) для streaming
    @GetMapping("/stream/{id}")
    public Flux<ServerSentEvent<String>> streamData(@PathVariable Long id) {
        return dataService.streamDataAsync(id)
            .map(data -> ServerSentEvent.<String>builder()
                .data(data)
                .build())
            .doOnError(error -> System.err.println("Stream error: " + error));
    }
}

@Service
public class DataService {
    
    @Autowired
    private WebClient webClient;
    
    public Mono<String> fetchDataAsync(Long id) {
        return webClient.get()
            .uri("https://external-api.com/data/" + id)
            .retrieve()
            .bodyToMono(String.class)
            .timeout(Duration.ofSeconds(30))
            .retry(2);
    }
    
    public Flux<String> streamDataAsync(Long id) {
        return Flux.interval(Duration.ofSeconds(1))
            .flatMap(i -> fetchDataAsync(id))
            .take(10);  // 10 элементов
    }
}

Сравнение подходов

┌─────────────┬──────────┬──────────┬──────────┬─────────────┐
│   Подход    │ Простота │ Задержка │ Scaling  │ Надёжность  │
├─────────────┼──────────┼──────────┼──────────┼─────────────┤
│ Polling     │   ✅    │   ❌    │   ❌    │    ✅      │
│ Webhooks    │  ⚠️     │   ✅    │   ✅    │   ⚠️      │
│ Queue (MQ)  │  ⚠️     │   ✅    │   ✅    │    ✅      │
│ Reactive    │  ❌     │   ✅    │   ✅    │    ✅      │
└─────────────┴──────────┴──────────┴──────────┴─────────────┘

Реальный пример из моего опыта

// Проект: Sistema обработки видео (2020-2021)
// Проблема: Загрузка и конвертация видео занимает минуты

@RestController
@RequestMapping("/api/v1/videos")
public class VideoController {
    
    @PostMapping("/upload")
    public ResponseEntity<?> uploadVideo(@RequestParam("file") MultipartFile file,
                                        @RequestParam("webhookUrl") String webhookUrl) {
        // 1. Быстро сохраняем файл
        Video video = new Video();
        video.setOriginalFilename(file.getOriginalFilename());
        video.setStatus("UPLOADING");
        videoRepository.save(video);
        
        // 2. Отправляем в фоновую очередь
        rabbitTemplate.convertAndSend("video-processing",
            new VideoProcessingTask(video.getId(), file, webhookUrl));
        
        return ResponseEntity.accepted()
            .body(Map.of(
                "video_id", video.getId(),
                "status", "PROCESSING_QUEUED"
            ));
    }
}

// Обработчик в отдельном сервисе
@Component
public class VideoProcessor {
    
    @RabbitListener(queues = "video-processing")
    public void processVideo(VideoProcessingTask task) throws IOException {
        Video video = videoRepository.findById(task.getVideoId()).get();
        
        try {
            video.setStatus("PROCESSING");
            videoRepository.save(video);
            
            // Конвертация видео (долгая операция)
            String convertedPath = convertVideo(task.getFile());
            
            // Загрузка на CDN
            String cdnUrl = uploadToCDN(convertedPath);
            
            video.setStatus("COMPLETED");
            video.setConvertedUrl(cdnUrl);
            videoRepository.save(video);
            
            // Webhook уведомление
            notifyWebhook(task.getWebhookUrl(), video);
        } catch (Exception e) {
            video.setStatus("FAILED");
            video.setError(e.getMessage());
            videoRepository.save(video);
        }
    }
}

Best Practices

// 1. Идемпотентность
@Service
public class JobService {
    
    // Ключ идемпотентности для retry
    @Async
    public void processJobIdempotent(Long jobId, String idempotencyKey) {
        // Проверяем, не обрабатывали ли уже с этим ключом
        if (idempotencyService.isProcessed(idempotencyKey)) {
            return;
        }
        
        // Обработка
        Job job = jobRepository.findById(jobId).get();
        processData(job);
        
        // Отмечаем как обработанные
        idempotencyService.markAsProcessed(idempotencyKey);
    }
}

// 2. Таймауты и retry
@Service
public class ReliableApiClient {
    
    @Autowired
    private RestTemplate restTemplate;
    
    public void callExternalApi(String url) {
        RetryTemplate retryTemplate = new RetryTemplate();
        
        // Retry 3 раза с exponential backoff
        retryTemplate.setRetryPolicy(
            new ExponentialBackOffPolicy()  // 1s, 2s, 4s
        );
        
        retryTemplate.execute(context -> {
            return restTemplate.postForObject(url, data, String.class);
        });
    }
}

Итог

  1. Polling — простой подход, но неэффективный при частых проверках
  2. Webhooks — быстро, но требует обработки на клиенте
  3. Message Queue — надёжный и масштабируемый
  4. Reactive — современный подход, но сложнее
  5. Выбирайте на основе:
    • Задержки приемлемой для вашего случая
    • Нагрузки и масштаба системы
    • Надёжности требований
    • Простоты реализации

В большинстве современных систем я использую комбинацию: Message Queue + Webhooks для надёжности и скорости обратной связи.

Реализовывал ли асинхронное взаимодействие через REST | PrepBro