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

Как избежать дублирования операций и гарантировать, что операция выполнится только один раз?

3.0 Senior🔥 181 комментариев
#REST API и микросервисы#SOLID и паттерны проектирования#Многопоточность

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

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

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

Ответ

Проблема

Если запрос повторяется из-за сетевой ошибки, операция может выполниться дважды:

Первый запрос → платёж создан ✓
Сеть падает → клиент не получил ответ  
Повторный запрос → платёж создан ЕЩЁ РАЗ ✗

Решение 1: Idempotency Key

Клиент генерирует уникальный ключ для каждой операции и передаёт его сервере:

String idempotencyKey = UUID.randomUUID().toString();

HttpRequest request = HttpRequest.newBuilder()
    .uri(java.net.URI.create("https://api.example.com/payments"))
    .header("Idempotency-Key", idempotencyKey)
    .POST(...)
    .build();

Сервер проверяет по ключу был ли запрос раньше:

@PostMapping("/payments")
public PaymentResponse create(
        @RequestBody PaymentRequest req,
        @RequestHeader("Idempotency-Key") String key) {
    
    // Проверяем кэш
    PaymentResponse cached = idempotencyCache.get(key);
    if (cached != null) return cached; // Уже выполнено
    
    // Выполняем операцию
    Payment payment = paymentRepository.save(new Payment(req));
    PaymentResponse response = new PaymentResponse(payment);
    
    // Сохраняем результат (TTL 24 часа)
    idempotencyCache.put(key, response, Duration.ofHours(24));
    return response;
}

Решение 2: UNIQUE констрейн в БД

Используй натуральный ключ для предотвращения дублей:

CREATE TABLE payments (
    id UUID PRIMARY KEY,
    external_reference_id VARCHAR(255) UNIQUE NOT NULL,
    amount DECIMAL(10, 2)
);
@Transactional
public PaymentResponse create(PaymentRequest req, String refId) {
    try {
        Payment p = new Payment();
        p.setExternalReferenceId(refId);
        return new PaymentResponse(paymentRepository.save(p));
    } catch (DataIntegrityViolationException e) {
        // Платёж уже существует, возвращаем его
        return new PaymentResponse(
            paymentRepository.findByExternalReferenceId(refId).get()
        );
    }
}

Решение 3: Pessimistic Lock

Для критичных операций используй SELECT FOR UPDATE:

@Transactional
public TransferResponse transfer(TransferRequest req) {
    // Блокируем запись на время транзакции
    Transfer existing = transferRepo
        .findByIdempotencyKeyWithLock(req.getIdempotencyKey())
        .orElse(null);
    
    if (existing != null) return new TransferResponse(existing);
    
    Transfer t = new Transfer();
    t.setIdempotencyKey(req.getIdempotencyKey());
    Transfer saved = transferRepository.save(t);
    
    accountService.decreaseBalance(req.getFrom(), req.getAmount());
    accountService.increaseBalance(req.getTo(), req.getAmount());
    
    return new TransferResponse(saved);
}

@Repository
public interface TransferRepository extends JpaRepository<Transfer, UUID> {
    @Query("SELECT t FROM Transfer t WHERE t.idempotencyKey = ?1")
    @Lock(LockModeType.PESSIMISTIC_WRITE) // SELECT FOR UPDATE
    Optional<Transfer> findByIdempotencyKeyWithLock(String key);
}

Сравнение

ПодходНадёжностьПроизводительностьИспользование
Idempotency Key✅✅⚠️ МедленнееПлатежи, API
UNIQUE констрейн✅✅✅ БыстроНатуральные ключи
Pessimistic Lock✅✅✅❌ МедленноКритичные операции

Best Practices

Клиент: Генерируй UUID для каждого запроса

String idempotencyKey = UUID.randomUUID().toString();

Сервер: Проверяй был ли запрос перед выполнением

if (idempotencyCache.contains(key)) return cachedResult;

Сервер: Сохраняй результат с TTL

idempotencyCache.put(key, result, Duration.ofHours(24));

БД: Используй транзакции

@Transactional
public void operation() { ... }

Итог

Для гарантирования выполнения операции только один раз:

  • Простые случаи → UNIQUE констрейн
  • REST API → Idempotency Key (стандарт Stripe, PayPal)
  • Критичные операции → Pessimistic Lock (SELECT FOR UPDATE)
  • Распределённые системы → Event Sourcing или Saga паттерн
Как избежать дублирования операций и гарантировать, что операция выполнится только один раз? | PrepBro