Gymterview
middle

Что такое идемпотентность и почему она важна в микросервисах?

Идемпотентность — это свойство операции, при котором многократное выполнение даёт тот же результат, что и однократное. В микросервисах это гарантирует, что повторная отправка запроса не приводит к дублированию побочных эффектов.

Аналогия из жизни: нажатие кнопки «Вызвать лифт» — сколько бы раз вы ни нажали, лифт приедет один раз. Повторное нажатие не вызывает второй лифт.

Почему это критично

  • Сетевые сбои: клиент отправил запрос, не получил ответ (таймаут) и повторяет.
  • Retry-механизмы: автоматический retry при ошибке может отправить запрос повторно.
  • Kafka: при ребалансировке потребитель может обработать сообщение дважды (at-least-once delivery).

Идемпотентность HTTP-методов

Метод Идемпотентный? Пояснение
GET Да Чтение не меняет состояние
PUT Да Полная замена ресурса
DELETE Да Повторное удаление = нет эффекта
POST Нет Создание ресурса — требует дополнительных мер
Реализация через Idempotency Key
// Клиент передаёт уникальный ключ идемпотентности
@PostMapping("/api/payments")
public ResponseEntity<PaymentResponse> createPayment(
        @RequestHeader("Idempotency-Key") String idempotencyKey,
        @RequestBody PaymentRequest request) {
    return paymentService.processPayment(idempotencyKey, request);
}

@Service
@RequiredArgsConstructor
public class PaymentService {
    private final PaymentRepository paymentRepository;
    private final IdempotencyStore idempotencyStore; // Redis

    @Transactional
    public ResponseEntity<PaymentResponse> processPayment(
            String idempotencyKey, PaymentRequest request) {

        // 1. Проверяем, не обрабатывался ли уже этот запрос
        Optional<PaymentResponse> existing = idempotencyStore.get(idempotencyKey);
        if (existing.isPresent()) {
            return ResponseEntity.ok(existing.get());
        }

        // 2. Обрабатываем платёж
        Payment payment = executePayment(request);
        PaymentResponse response = toResponse(payment);

        // 3. Сохраняем результат с TTL
        idempotencyStore.save(idempotencyKey, response, Duration.ofHours(24));

        return ResponseEntity.status(HttpStatus.CREATED).body(response);
    }
}
Идемпотентность Kafka-консьюмера
@KafkaListener(topics = "payment-events")
public void handlePaymentEvent(PaymentEvent event) {
    // Проверяем, обрабатывали ли уже это событие
    if (processedEventRepository.existsByEventId(event.getEventId())) {
        log.info("Событие {} уже обработано, пропускаем", event.getEventId());
        return;
    }

    // Обрабатываем и помечаем как обработанное в одной транзакции
    processEvent(event);
    processedEventRepository.save(new ProcessedEvent(event.getEventId()));
}

На собеседовании: объясните через конкретный пример: «если POST /payments выполняется дважды — деньги не должны списаться дважды». Покажите решение через Idempotency Key + Redis. Частая ошибка — забыть про идемпотентность Kafka-консьюмеров.