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-консьюмеров.