Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

4 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Gozon Shop Microservices

Проект демонстрирует реализацию надежного асинхронного взаимодействия между сервисами с гарантиями доставки сообщений и целостности данных. В основе архитектуры лежат паттерны Transactional Outbox, Transactional Inbox и Idempotent Consumer, обеспечивающие семантику обработки exactly-once для финансовых операций.


Структура проекта

.
├── common/                  # Общие C++ компоненты (DB, RabbitMQ, DTO)
│   ├── db_conn.hpp          # Пул подключений к PostgreSQL (libpqxx)
│   ├── dto.hpp              # Структуры данных и JSON-сериализация
│   └── rabbitmq.hpp         # Обертка над SimpleAmqpClient
├── frontend/                # SPA приложение (Клиентская часть)
│   ├── Dockerfile           # Nginx контейнер
│   ├── app.js               # Логика взаимодействия с REST API
│   └── index.html           # UI
├── schemas/                 # SQL
│   └── init.sql             # Схемы таблиц (Orders, Accounts, Outbox/Inbox)
├── services/                # Исходный код микросервисов
│   ├── order-service/       # Сервис заказов (REST API + Consumers)
│   │   ├── src/
│   │   │   ├── main.cpp     # Точка входа, потоки обработки, HTTP сервер
│   │   │   └── repository.hpp # Бизнес-логика и SQL запросы
│   │   └── CMakeLists.txt
│   └── payment-service/     # Сервис оплаты (REST API + Consumers)
│       ├── src/
│       │   ├── main.cpp
│       │   └── repository.hpp
│       └── CMakeLists.txt
├── tests/                   # Интеграционные тесты
│   └── GozonShop_Tests.postman_collection.json
├── CMakeLists.txt           # Корневая конфигурация сборки
├── Dockerfile               # Full build для C++ сервисов
└── docker-compose.yml       # Конфигурация (App, DB, Broker, UI)


Технологический стек

  • Язык: C++17
  • Web Framework: Crow (Asynchronous REST API)
  • Database: PostgreSQL 15 (libpqxx driver)
  • Message Broker: RabbitMQ (SimpleAmqpClient)
  • Frontend: HTML, JavaScript, Nginx
  • Containerization: Docker, Docker Compose

Установка и Запуск

Для запуска требуется установленный Docker и Docker Compose.

1. Запуск системы

Выполните команду в корне проекта:

docker compose up --build

2. Доступ к интерфейсам

После запуска доступны следующие точки входа:

Сервис URL Описание
Frontend UI http://localhost:3000 Веб-интерфейс для управления счетами и заказами
Order API http://localhost:8081 REST API заказов
Payment API http://localhost:8082 REST API счетов
RabbitMQ UI http://localhost:15672 Админ-панель брокера сообщений (login: user, pass: password)

3. Тестирование (Postman)

В директории tests/ находится коллекция GozonShop_Tests.postman_collection.json.

Шаг 1. Импорт

  1. Откройте Postman -> Import.
  2. Перетащите файл tests/GozonShop_Tests.postman_collection.json.

Шаг 2. Запуск тестов

  1. Нажмите на название коллекции "Gozon Shop Integration Tests" в списке слева.
  2. Нажмите кнопку Run.
  3. В разделе Run configuration задайте Delay 500ms (необходимо для завершения асинхронной обработки заказа через очередь RabbitMQ).
  4. Нажмите кнопку Gozon Shop Integration Tests.

Тесты автоматически проверят:

  • Создание и пополнение счетов.
  • Успешную оплату (баланс уменьшается, статус PAID).
  • Отказ в оплате при нехватке средств (баланс не меняется, статус FAILED).
  • Обработку несуществующих пользователей.

Архитектурные решения и Реализация

Архитектура системы построена на принципах слабой связности (Low Coupling) и высокой надежности данных. Бизнес-логика разделена на два независимых микросервиса: Order Service и Payment Service, каждый из которых владеет собственной базой данных PostgreSQL.

1. Стратегия маршрутизации

В данной архитектуре было принято решение отказаться от выделенного API Gateway в пользу Infrastructure-based Routing.

  • Снижение задержки: Прямое взаимодействие клиента с микросервисами через выделенные порты Docker исключает лишний сетевой узел, уменьшая время отклика системы.
  • Принцип KISS (Keep It Simple, Stupid): Для текущего масштаба системы внедрение программного прокси добавляет избыточную сложность конфигурации без существенного выигрыша в функциональности.
  • Изолированное тестирование: Разделение точек входа (порты 8081 и 8082) позволяет разрабатывать и отлаживать API каждого сервиса независимо друг от друга.

2. Асинхронный транспорт и Гарантии доставки

В качестве транспортного слоя между сервисами используется брокер сообщений RabbitMQ. Система брокера сообщений спроектирована с учетом того, что сеть ненадежна, поэтому он гарантирует доставку по модели at-least-once (минимум один раз).

Это означает, что сообщения могут дублироваться. Чтобы обеспечить корректность финансовых операций, реализована семантика exactly-once на уровне приложения, используя комбинацию идемпотентности и транзакционных паттернов.

3. Паттерн Transactional Outbox (Order Service)

Чтобы решить проблему "двойной записи", когда нужно атомарно сохранить заказ в БД и отправить событие в очередь, мы используем таблицу order_outbox.

  • Атомарное сохранение: Метод OrderRepository::create_order в рамках одной SQL-транзакции сохраняет сам заказ и запись о событии ORDER_CREATED в таблицу Outbox. Это гарантирует, что событие не потеряется, даже если процесс упадет сразу после записи в БД.
  • Фоновая отправка: Функция run_outbox_processor в фоновом режиме вычитывает события из Outbox и публикует их в RabbitMQ. Использование механизма SKIP LOCKED позволяет запускать несколько экземпляров сервиса без конкуренции за события.

4. Паттерн Transactional Inbox и Идемпотентность (Payment Service)

Для защиты от дубликатов сообщений (которые возможны при at-least-once доставке) на стороне сервиса оплат реализован паттерн Transactional Inbox.

  • Избавление от дубликатов: В начале обработки метод PaymentRepository::process_payment пытается вставить уникальный идентификатор сообщения (связанный с ID заказа) в таблицу payment_inbox. Если запись уже существует (конфликт уникальности), обработка прерывается, так как сообщение уже было обработано.
  • Гарантированная обработка: Если сообщение новое, сервис переходит к бизнес-логике списания средств.

5. Атомарность бизнес-операций и Обратная связь

Списание средств и отправка результата также защищены транзакционно.

  • Списание средств: Внутри той же транзакции, где фиксируется запись в Inbox, происходит обновление баланса пользователя (метод PaymentRepository::process_payment). Это обеспечивает строгую согласованность: баланс меняется тогда и только тогда, когда сообщение успешно учтено.
  • Transactional Outbox для ответа: Результат операции (успех или отказ из-за нехватки средств) не отправляется в сеть сразу, а сохраняется в таблицу payment_outbox.
  • Отправка результата: Отдельный поток run_payment_outbox доставляет статус обработки обратно в Order Service через очередь результатов, замыкая цикл асинхронного взаимодействия.

Пользовательские сценарии: Жизненный цикл заказа

Ключевой сценарий системы описывает процесс создания заказа и его автоматической оплаты через асинхронное взаимодействие микросервисов. Процесс разделен на этапы, обеспечивающие гарантии целостности данных.

1. Инициация заказа (Transactional Outbox — Часть 1)

Пользователь отправляет запрос на создание нового заказа. Order Service принимает запрос и в рамках одной атомарной транзакции базы данных сохраняет сущность нового заказа и задачу на его оплату (запись в таблицу order_outbox). Это гарантирует, что заказ не может быть создан без инициирования процесса оплаты.

  • Исполняющий метод: OrderRepository::create_order

2. Асинхронная публикация (Transactional Outbox — Часть 2)

Фоновый процесс Order Service асинхронно вычитывает задачу из базы данных и отправляет соответствующее сообщение в брокер сообщений. Использование механизма подтверждения гарантирует, что сообщение попадет в очередь.

  • Исполняющий метод: run_outbox_processor (использует process_next_outbox_event)

3. Прием и Обработка дубликатов (Transactional Inbox — Часть 1)

Payment Service вычитывает задачу на оплату из очереди. Перед выполнением сервис сохраняет идентификатор сообщения в свою базу данных (таблица payment_inbox). Если запись с таким ID уже существует, обработка прекращается, что обеспечивает идемпотентность и защиту от дублей.

  • Исполняющий метод: run_payment_consumer (вызывает PaymentRepository::process_payment)

4. Исполнение транзакции (Inbox ч.2 + Outbox ч.1)

Payment Service выполняет бизнес-логику оплаты. В рамках единой транзакции происходит:

  1. Проверка наличия счета пользователя и достаточности средств.
  2. Атомарное списание средств (если условия выполнены) или фиксация ошибки.
  3. Запись результата (PAID или FAILED) в таблицу payment_outbox для последующей отправки.
  • Исполняющий метод: PaymentRepository::process_payment

5. Завершение цикла (Асинхронный ответ)

Процесс завершается двумя параллельными действиями:

  1. Отправка статуса: Фоновый процесс Payment Service вычитывает результат из своей базы данных и отправляет событие об успехе или неуспехе операции в очередь результатов.
  • Исполняющий метод: run_payment_outbox
  1. Обновление заказа: Order Service ожидает входящее событие из очереди результатов. Получив его, сервис обновляет статус соответствующего заказа в базе данных на финальный (PAID, FAILED).
  • Исполняющий метод: run_result_consumer (вызывает OrderRepository::update_order_status)

Взаимодействие Frontend и API (CORS)

Для обеспечения работы SPA (порт 3000) с API микросервисов (порты 8081, 8082), в C++ коде реализован CORSHandler:

// main.cpp
void after_handle(crow::request& req, crow::response& res, context& ctx) {
    res.add_header("Access-Control-Allow-Origin", "*");
    res.add_header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
    // ...
}

Это позволяет браузеру выполнять запросы напрямую к микросервисам, минуя необходимость в API Gateway.


Работа с Веб-интерфейсом

В проект включен полноценный Frontend-клиент (SPA), работающий в отдельном Docker-контейнере на базе Nginx. Приложение взаимодействует с микросервисами напрямую через REST API.

Сценарий тестирования через браузер (http://localhost:3000):

  1. Регистрация и Касса:
    • В блоке "Счет" введите произвольный User ID (например, 67) и нажмите "Создать".
    • Пополните баланс этого пользователя (например, на 6767 у.е.).
  2. Покупка (Витрина):
    • В блоке "Магазин" введите тот же User ID, стоимость товара и описание.
    • Нажмите "КУПИТЬ".
  3. Визуализация асинхронности:
    • Интерфейс покажет Order ID и начальный статус NEW.
    • Через секунду (после обработки очереди сообщений) статус автоматически обновится на PAID (зеленый) или FAILED (красный, если средств недостаточно).
  4. История заказов:
    • В нижней части экрана можно загрузить полный список всех операций пользователя, чтобы убедиться в корректности статусов всех сделанных заказов.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages