← Назад к вопросам
Как обеспечить синхронное взаимодействие на сервере через внешнюю систему
1.7 Middle🔥 161 комментариев
#REST API и микросервисы#Брокеры сообщений
Комментарии (1)
🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
# Синхронное взаимодействие через внешнюю систему
Задача
Тебе нужно ожидать результат от внешней системы синхронно (блокирующе), но ответ может прийти через какое-то время. Типичный пример:
- Твой сервер отправляет запрос на микросервис оплаты
- Сервис оплаты обрабатывает заказ (это может занять несколько секунд)
- Твой сервер должен дождаться ответа перед отправкой HTTP ответа клиенту
Проблема
// Неправильно: блокируем основной поток на длительное время
@PostMapping("/checkout")
public OrderResponse checkout(@RequestBody OrderRequest request) {
PaymentResult result = paymentService.processPayment(request);
// Если processPayment блокируется на 10 секунд, клиент ждет 10 секунд
return new OrderResponse(result);
}
Решение 1: Polling (опрашивание)
Отправляем запрос и периодически проверяем статус:
@Service
public class PaymentService {
@Autowired
private RestTemplate restTemplate;
public PaymentResult waitForPaymentCompletion(String paymentId, int timeoutSeconds)
throws TimeoutException, InterruptedException {
long startTime = System.currentTimeMillis();
long timeoutMs = timeoutSeconds * 1000L;
while (true) {
try {
// Проверяем статус у внешней системы
String statusUrl = "https://payment-api.com/status/" + paymentId;
PaymentStatus status = restTemplate.getForObject(statusUrl, PaymentStatus.class);
if (status.isCompleted()) {
return new PaymentResult(status.getResult());
}
if (status.isFailed()) {
throw new PaymentException("Payment failed: " + status.getError());
}
} catch (RestClientException e) {
// Сервис недоступен, повторим позже
}
// Проверяем таймаут
if (System.currentTimeMillis() - startTime > timeoutMs) {
throw new TimeoutException("Payment processing timeout");
}
// Ждём перед следующей попыткой
Thread.sleep(1000); // Ждём 1 секунду перед повторной проверкой
}
}
}
@RestController
@RequestMapping("/api/orders")
public class OrderController {
@Autowired
private PaymentService paymentService;
@PostMapping("/checkout")
public OrderResponse checkout(@RequestBody OrderRequest request)
throws TimeoutException, InterruptedException {
// 1. Инициируем платёж
String paymentId = paymentService.initiatePayment(request);
// 2. Ждём результата (будет блокировка на макс 30 секунд)
PaymentResult result = paymentService.waitForPaymentCompletion(paymentId, 30);
// 3. Возвращаем результат
return new OrderResponse(result);
}
}
Минусы: много ненужных HTTP запросов, нагрузка на внешнюю систему.
Решение 2: Webhook (обратный вызов)
Внешняя система уведомляет нас когда готов результат:
// ===== НА НАШЕМ СЕРВЕРЕ =====
@Service
public class PaymentService {
// Map для хранения результатов: paymentId -> CompletableFuture
private final Map<String, CompletableFuture<PaymentResult>> pendingPayments
= new ConcurrentHashMap<>();
public PaymentResult waitForPaymentSync(String paymentId, int timeoutSeconds)
throws TimeoutException, InterruptedException, ExecutionException {
// Создаём Future для этого платежа
CompletableFuture<PaymentResult> future = new CompletableFuture<>();
pendingPayments.put(paymentId, future);
try {
// Блокируемся и ждём результат (максимум timeoutSeconds)
return future.get(timeoutSeconds, TimeUnit.SECONDS);
} finally {
// Очищаем после завершения
pendingPayments.remove(paymentId);
}
}
// Вебхук — вызывается внешней системой
public void handlePaymentWebhook(PaymentWebhookRequest webhook) {
String paymentId = webhook.getPaymentId();
CompletableFuture<PaymentResult> future = pendingPayments.get(paymentId);
if (future != null) {
PaymentResult result = new PaymentResult(
webhook.getSuccess(),
webhook.getTransactionId(),
webhook.getAmount()
);
if (webhook.getSuccess()) {
future.complete(result);
} else {
future.completeExceptionally(
new PaymentException(webhook.getErrorMessage())
);
}
}
}
}
@RestController
@RequestMapping("/api")
public class WebhookController {
@Autowired
private PaymentService paymentService;
// Вебхук от платёжной системы
@PostMapping("/webhooks/payment")
public void paymentWebhook(@RequestBody PaymentWebhookRequest webhook) {
paymentService.handlePaymentWebhook(webhook);
}
}
@RestController
@RequestMapping("/api/orders")
public class OrderController {
@Autowired
private PaymentService paymentService;
@PostMapping("/checkout")
public OrderResponse checkout(@RequestBody OrderRequest request)
throws TimeoutException, InterruptedException, ExecutionException {
// 1. Инициируем платёж
String paymentId = paymentService.initiatePayment(request);
// 2. Ждём вебхука от платёжной системы (максимум 30 секунд)
PaymentResult result = paymentService.waitForPaymentSync(paymentId, 30);
// 3. Возвращаем результат клиенту
return new OrderResponse(result);
}
}
Как это работает:
1. Клиент отправляет POST /checkout
2. Сервер инициирует платёж в внешней системе
3. Сервер вызывает future.get(30, TimeUnit.SECONDS) — БЛОКИРУЕТСЯ
4. Внешняя система обрабатывает платёж в фоне
5. Внешняя система отправляет вебхук на https://наш-сервер.com/webhooks/payment
6. Сервер вызывает future.complete(result)
7. Ожидание в пункте 3 завершается
8. Сервер возвращает ответ клиенту
Решение 3: Более сложный вариант с базой данных
Для отказоустойчивости хранишь статус в БД:
@Entity
@Table(name = "payments")
public class Payment {
@Id
private String id;
@Enumerated(EnumType.STRING)
private PaymentStatus status; // PENDING, COMPLETED, FAILED
private String externalTransactionId;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
}
@Service
public class PaymentService {
@Autowired
private PaymentRepository paymentRepository;
private final Map<String, CompletableFuture<PaymentResult>> futures = new ConcurrentHashMap<>();
@Transactional
public PaymentResult waitForPaymentSync(String paymentId, int timeoutSeconds)
throws TimeoutException, ExecutionException, InterruptedException {
Payment payment = paymentRepository.findById(paymentId)
.orElseThrow(() -> new PaymentNotFoundException(paymentId));
// Если уже завершен, не ждём
if (payment.getStatus() == PaymentStatus.COMPLETED) {
return new PaymentResult(true, payment.getExternalTransactionId());
}
if (payment.getStatus() == PaymentStatus.FAILED) {
throw new PaymentException("Payment already failed");
}
// Создаём Future и ждём вебхука
CompletableFuture<PaymentResult> future = new CompletableFuture<>();
futures.put(paymentId, future);
try {
return future.get(timeoutSeconds, TimeUnit.SECONDS);
} finally {
futures.remove(paymentId);
}
}
@Transactional
public void handlePaymentWebhook(PaymentWebhookRequest webhook) {
String paymentId = webhook.getPaymentId();
Payment payment = paymentRepository.findById(paymentId).orElseReturn();
if (webhook.getSuccess()) {
payment.setStatus(PaymentStatus.COMPLETED);
payment.setExternalTransactionId(webhook.getTransactionId());
} else {
payment.setStatus(PaymentStatus.FAILED);
}
payment.setUpdatedAt(LocalDateTime.now());
paymentRepository.save(payment);
// Уведомляем ожидающий поток
CompletableFuture<PaymentResult> future = futures.get(paymentId);
if (future != null) {
if (webhook.getSuccess()) {
future.complete(new PaymentResult(true, webhook.getTransactionId()));
} else {
future.completeExceptionally(new PaymentException(webhook.getError()));
}
}
}
}
Сравнение подходов
| Способ | Нагрузка на систему | Скорость ответа | Надёжность | Сложность |
|---|---|---|---|---|
| Polling | Высокая (много запросов) | Низкая (зависит от интервала) | Средняя | Низкая |
| Webhook | Низкая (один ответ) | Высокая (без задержки) | Высокая | Средняя |
| Polling + DB | Средняя | Средняя | Высокая | Средняя |
| Webhook + DB | Низкая | Высокая | Очень высокая | Высокая |
Рекомендации
- Используй вебхуки если внешняя система их поддерживает
- Добавь сигнатуру вебхука для безопасности (HMAC SHA-256)
- Обработай idempotentность — вебхук может прийти дважды
- Логируй все платежи в БД для отладки
- Установи таймаут не больше 30 секунд для HTTP запроса
- Используй транзакции — избегай race conditions
Безопасность вебхука
@Service
public class WebhookSecurityService {
private static final String WEBHOOK_SECRET = System.getenv("WEBHOOK_SECRET");
public boolean verifyWebhookSignature(String body, String signature) {
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(WEBHOOK_SECRET.getBytes(), "HmacSHA256"));
byte[] computed = mac.doFinal(body.getBytes(StandardCharsets.UTF_8));
String computedSignature = Base64.getEncoder().encodeToString(computed);
return computedSignature.equals(signature);
}
}
@PostMapping("/webhooks/payment")
public void paymentWebhook(
@RequestBody String body,
@RequestHeader("X-Signature") String signature) {
if (!webhookSecurityService.verifyWebhookSignature(body, signature)) {
throw new WebhookSecurityException("Invalid signature");
}
PaymentWebhookRequest webhook = mapper.readValue(body, PaymentWebhookRequest.class);
paymentService.handlePaymentWebhook(webhook);
}
Этот подход обеспечивает синхронное взаимодействие без излишней нагрузки на системы.