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

Как обеспечить синхронное взаимодействие на сервере через внешнюю систему

1.7 Middle🔥 161 комментариев
#REST API и микросервисы#Брокеры сообщений

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

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

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

# Синхронное взаимодействие через внешнюю систему

Задача

Тебе нужно ожидать результат от внешней системы синхронно (блокирующе), но ответ может прийти через какое-то время. Типичный пример:

  1. Твой сервер отправляет запрос на микросервис оплаты
  2. Сервис оплаты обрабатывает заказ (это может занять несколько секунд)
  3. Твой сервер должен дождаться ответа перед отправкой 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НизкаяВысокаяОчень высокаяВысокая

Рекомендации

  1. Используй вебхуки если внешняя система их поддерживает
  2. Добавь сигнатуру вебхука для безопасности (HMAC SHA-256)
  3. Обработай idempotentность — вебхук может прийти дважды
  4. Логируй все платежи в БД для отладки
  5. Установи таймаут не больше 30 секунд для HTTP запроса
  6. Используй транзакции — избегай 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);
}

Этот подход обеспечивает синхронное взаимодействие без излишней нагрузки на системы.