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

Каким должен быть сервис, чтобы избежать потерь?

1.8 Middle🔥 201 комментариев
#REST API и микросервисы

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

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

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

Каким должен быть сервис, чтобы избежать потерь?

Введение

Этот вопрос касается построения надёжных (reliable), отказоустойчивых (resilient) систем, которые не теряют данные и не доставляют неожиданные результаты. Критично для систем, обрабатывающих финансы, данные и операции.

1. Надёжность (Reliability)

Определение: Система работает корректно под нормальными и ненормальными условиями.

Уменьшение потерь данных

// ❌ ОПАСНО: потеря данных
public void processPayment(Payment payment) {
    // Платёж обработан в памяти
    processInMemory(payment);
    
    // Но если сервер упадёт, платёж потеряется!
    // Неповторно: клиент заплатил, но деньги на счёте не пришли
}

// ✅ ПРАВИЛЬНО: стойкость к потерям
@Transactional(isolation = Isolation.SERIALIZABLE)
public void processPayment(Payment payment) {
    // 1. Сохранить платёж в БД ПЕРЕД обработкой
    Payment saved = paymentRepository.save(payment);
    
    try {
        // 2. Обработать платёж
        boolean success = processWithGateway(saved);
        
        // 3. Обновить статус в БД
        saved.setStatus(success ? PaymentStatus.SUCCESS : PaymentStatus.FAILED);
        paymentRepository.save(saved);
        
    } catch (Exception e) {
        // 4. При ошибке: сохранить состояние и отправить уведомление
        saved.setStatus(PaymentStatus.ERROR);
        saved.setErrorMessage(e.getMessage());
        paymentRepository.save(saved);
        
        // 5. Отправить алерт для ручного разбора
        alertService.sendAlert("Payment processing failed: " + saved.getId());
        throw e;
    }
}

Идемпотентность (Idempotency)

// Критично для платежей и финансов!
// Если клиент случайно кликнул дважды -> должен быть только один платёж

public class Payment {
    @Id
    private Long id;
    
    @Column(unique = true)  // Idempotency Key
    private String idempotencyKey;  // UUID от клиента
    
    private BigDecimal amount;
    private PaymentStatus status;
}

@Service
public class PaymentService {
    
    public PaymentResponse processPayment(
            CreatePaymentRequest request,
            String idempotencyKey) {  // Важно!
        
        // ШАГ 1: Проверить, не обработан ли уже
        Optional<Payment> existing = paymentRepository
            .findByIdempotencyKey(idempotencyKey);
        
        if (existing.isPresent()) {
            // Платёж уже обработан, вернуть результат
            return new PaymentResponse(existing.get());
        }
        
        // ШАГ 2: Первый раз - обработать
        Payment payment = new Payment();
        payment.setIdempotencyKey(idempotencyKey);
        payment.setAmount(request.getAmount());
        payment.setStatus(PaymentStatus.PENDING);
        
        Payment saved = paymentRepository.save(payment);
        
        try {
            boolean success = chargeCard(request);
            saved.setStatus(success ? APPROVED : REJECTED);
        } catch (Exception e) {
            saved.setStatus(FAILED);
        }
        
        paymentRepository.save(saved);
        return new PaymentResponse(saved);
    }
}

// На клиенте: каждый запрос имеет UUID
UUID idempotencyKey = UUID.randomUUID();
paymentClient.pay(paymentRequest, idempotencyKey);
// Если клиент потеряет соединение -> перешлёт тот же запрос
// Сервер вернёт тот же результат (не создаст новый платёж)

2. Отказоустойчивость (Fault Tolerance)

Определение: Система продолжает работать даже при отказах отдельных компонентов.

Обработка ошибок и Retry Logic

// ❌ ПЛОХО: нет retry
public void callExternalService() throws Exception {
    try {
        externalApi.call();  // Если API недоступен -> ошибка
    } catch (Exception e) {
        throw e;  // Клиент получает ошибку
    }
}

// ✅ ХОРОШО: с retry
@Service
public class PaymentGatewayService {
    
    @Retryable(
        value = { ConnectionException.class, TimeoutException.class },
        maxAttempts = 3,
        delay = 1000,
        multiplier = 2.0  // exponential backoff: 1s, 2s, 4s
    )
    public PaymentResponse chargeCard(Payment payment) {
        try {
            return paymentGateway.charge(payment);
        } catch (ConnectionException e) {
            // Retry после 1s, 2s, 4s
            throw e;
        }
    }
    
    @Recover
    public PaymentResponse recover(PaymentException e, Payment payment) {
        // После 3 попыток: отправить на manual review
        paymentQueueService.enqueueForManualReview(payment);
        return new PaymentResponse(PaymentStatus.PENDING_MANUAL_REVIEW);
    }
}

Circuit Breaker Pattern

// Защита от cascading failures
// Если внешний сервис упал -> не送 ему дальше запросы

@Service
public class PaymentGatewayService {
    
    @CircuitBreaker(
        name = "paymentGateway",
        failureThreshold = 50,      // открыть при 50% ошибок
        delay = 30000,              // проверить через 30 секунд
        successThreshold = 2        // нужно 2 успеха чтобы закрыть
    )
    @Timeout(name = "paymentGateway", duration = "5000ms")
    public PaymentResponse charge(Payment payment) {
        return paymentGateway.charge(payment);
    }
}

// Статусы Circuit Breaker:
// CLOSED: всё хорошо, пропускаем запросы
// OPEN: слишком много ошибок, блокируем запросы
// HALF_OPEN: пробуем, может ли сервис восстановиться

3. Persistence и Durability

Определение: Данные сохраняются надёжно и выживают при сбоях.

Write-Ahead Logging (WAL)

// БД подерживает WAL — перед изменением записывается в лог
// Гарантирует ACID свойства

@Entity
public class Payment {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    private BigDecimal amount;
    
    @Enumerated(EnumType.STRING)
    private PaymentStatus status;
    
    // Всё это автоматически сохраняется в БД транзакционно
}

// PostgreSQL например:
// 1. Записать в WAL: "INSERT INTO payments ..."
// 2. Записать в основную таблицу
// 3. Коммит = гарантия, что данные на диске

Репликация данных

Инфраструктура должна быть:

┌─────────────────────────────────────┐
│ Master (Primary) Database           │
│ ├─ Всё пишется здесь              │
│ └─ Write-Ahead Log                 │
└────────┬────────┬────────────────────┘
         │        │
    Replication (synchronous)
         │        │
    ┌────v─┐  ┌───v─┐
    │ Slave│  │Slave│  (Read-Only)
    │  DB1 │  │ DB2 │
    └──────┘  └─────┘

Если Master упадёт -> Slave1 становится Master
Данные всегда есть в нескольких копиях

4. Атомарность операций (Atomicity)

Критично для финансовых операций!

// ❌ ОПАСНО: не-атомарно
public void transferMoney(Account from, Account to, BigDecimal amount) {
    from.setBalance(from.getBalance().subtract(amount));
    accountRepository.save(from);  // БД обновлена
    
    // СБОЙ? Деньги ушли, но не пришли!
    
    to.setBalance(to.getBalance().add(amount));
    accountRepository.save(to);    // БД обновлена
}

// ✅ ПРАВИЛЬНО: атомарно (всё или ничего)
@Transactional(isolation = Isolation.SERIALIZABLE)
public void transferMoney(Account from, Account to, BigDecimal amount) {
    // ВСЯ операция в одной транзакции
    from.setBalance(from.getBalance().subtract(amount));
    to.setBalance(to.getBalance().add(amount));
    
    accountRepository.save(from);
    accountRepository.save(to);
    
    // Коммит = всё успешно ИЛИ откат = ничего не произойдёт
    // Невозможно потерять деньги!
}

Saga Pattern для распределённых транзакций

// Для систем с множеством микросервисов
// Нельзя использовать обычную транзакцию БД

@Service
public class OrderProcessingSaga {
    
    @Transactional
    public void processOrder(Order order) {
        try {
            // Шаг 1: Резервировать инвентарь
            inventoryService.reserve(order);
            
            // Шаг 2: Обработать платёж
            paymentService.charge(order);
            
            // Шаг 3: Отправить на доставку
            shippingService.ship(order);
            
            // Если все успешно
            order.setStatus(OrderStatus.CONFIRMED);
            orderRepository.save(order);
            
        } catch (PaymentException e) {
            // Откат (compensating transaction)
            inventoryService.release(order);  // вернуть инвентарь
            order.setStatus(OrderStatus.FAILED);
            orderRepository.save(order);
            throw e;
        }
    }
}

5. Мониторинг и Alerting

Как узнать, что что-то не так?

// Метрики на всё
@Service
public class PaymentMetricsService {
    private final MeterRegistry meterRegistry;
    
    public void recordPayment(Payment payment, long durationMs) {
        // Количество
        meterRegistry.counter(
            "payment.processed",
            "status", payment.getStatus().toString(),
            "gateway", payment.getGateway()
        ).increment();
        
        // Время выполнения
        meterRegistry.timer(
            "payment.processing.time"
        ).record(durationMs, TimeUnit.MILLISECONDS);
        
        // Размер платежа
        meterRegistry.gauge(
            "payment.amount",
            payment.getAmount().doubleValue()
        );
    }
}

// Алерты
// - Payment success rate < 99% -> пейджи
// - Payment processing latency > 5 seconds -> warning
// - Failed payments > 100/hour -> критично
// - Gateway timeout > 10% -> расследование

6. Логирование и Трейсинг

// Структурированное логирование (не просто print)

@Slf4j
@Service
public class PaymentService {
    
    public void processPayment(Payment payment) {
        MDC.put("payment_id", payment.getId().toString());  // context
        MDC.put("user_id", payment.getUserId().toString());
        
        try {
            log.info("Processing payment", 
                "amount", payment.getAmount(),
                "gateway", payment.getGateway());
            
            PaymentResponse response = gateway.charge(payment);
            
            log.info("Payment successful",
                "response_code", response.getCode());
            
        } catch (Exception e) {
            log.error("Payment failed",
                "error", e.getMessage(),
                "stack_trace", ExceptionUtils.getStackTrace(e));
            throw e;
        } finally {
            MDC.clear();
        }
    }
}

// ELK Stack для анализа логов
// Elasticsearch -> Logstash -> Kibana
// Можно быстро найти все платежи пользователя, ошибки и т.д.

7. Backup и Disaster Recovery

План восстановления:

1. Резервные копии (Backups)
   ├─ Daily full backups
   ├─ Hourly incremental backups
   └─ Tested recovery (раз в месяц)

2. RPO (Recovery Point Objective)
   └─ Максимум данных, которые можно потерять
   └─ Платежи: 0 минут (real-time replication)
   └─ Логи: может быть несколько минут

3. RTO (Recovery Time Objective)
   └─ Время до восстановления
   └─ Платежи: < 5 минут (критично!)
   └─ Рекомендации: может быть часы

4. Geo-Redundancy
   └─ Данные в разных географических местоположениях
   └─ Если дата-центр упадёт -> есть другой

8. Тестирование надёжности

// Chaos Engineering: намеренно ломаем систему

@Test
public void testPaymentWhenDatabaseUnavailable() {
    // Отключаем БД
    database.shutdown();
    
    try {
        paymentService.processPayment(payment);
        fail("Should throw exception");
    } catch (DatabaseUnavailableException e) {
        // Ожидаемо
    }
    
    // Включаем БД
    database.startup();
    
    // Платёж всё ещё в очереди? Или потерялся?
    assertThat(queue.contains(payment)).isTrue();
    
    // Повторить обработку
    paymentService.retryPendingPayments();
    
    // Платёж должен быть обработан
    assertThat(paymentRepository.findById(payment.getId())
        .getStatus()).isEqualTo(APPROVED);
}

@Test
public void testConcurrentPayments() {
    // 1000 потоков пытаются заплатить одновременно
    ExecutorService executor = Executors.newFixedThreadPool(1000);
    
    for (int i = 0; i < 1000; i++) {
        executor.submit(() -> {
            paymentService.processPayment(createPayment());
        });
    }
    
    executor.shutdown();
    executor.awaitTermination(5, TimeUnit.MINUTES);
    
    // Все платежи обработаны корректно?
    assertThat(paymentRepository.count()).isEqualTo(1000);
    assertThat(paymentRepository.countByStatus(APPROVED)).isEqualTo(1000);
}

Чек-лист надёжного сервиса

✅ Persistence
   ☑ Все важные данные сохраняются в БД
   ☑ Используется ACID транзакции
   ☑ Есть Write-Ahead Logging
   ☑ Есть резервные копии

✅ Idempotency
   ☑ Одинаковый запрос дважды = одинаковый результат
   ☑ Есть Idempotency Key
   ☑ Операции идемпотентны

✅ Fault Tolerance
   ☑ Retry logic с exponential backoff
   ☑ Circuit breaker для внешних сервисов
   ☑ Timeout на все операции
   ☑ Graceful degradation

✅ Monitoring
   ☑ Метрики на все операции
   ☑ Алерты для аномалий
   ☑ Dashboards в Grafana
   ☑ Логирование всех ошибок

✅ Testing
   ☑ Unit тесты на критичную логику
   ☑ Integration тесты с БД
   ☑ E2E тесты полного workflow
   ☑ Chaos tests (падения сервисов)
   ☑ Load tests (высокая нагрузка)

✅ Documentation
   ☑ API задокументирован
   ☑ Есть runbook для операций
   ☑ Есть disaster recovery plan
   ☑ Есть incident postmortems

Вывод

Надёжный сервис должен:

  1. Сохранять данные — все критичные данные в персистентное хранилище
  2. Быть идемпотентным — одинаковый запрос дважды = одинаковый результат
  3. Переживать сбои — retry logic, circuit breaker, graceful degradation
  4. Быть мониторируемым — метрики, логи, алерты
  5. Быть протестированным — unit, integration, E2E, chaos, load тесты
  6. Быть задокументированным — для операций и неотложных ситуаций

Без этого система будет терять данные, деньги и доверие пользователей.