← Назад к вопросам
Реализовывал ли асинхронное взаимодействие через 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);
});
}
}
Итог
- Polling — простой подход, но неэффективный при частых проверках
- Webhooks — быстро, но требует обработки на клиенте
- Message Queue — надёжный и масштабируемый
- Reactive — современный подход, но сложнее
- Выбирайте на основе:
- Задержки приемлемой для вашего случая
- Нагрузки и масштаба системы
- Надёжности требований
- Простоты реализации
В большинстве современных систем я использую комбинацию: Message Queue + Webhooks для надёжности и скорости обратной связи.