Проект демонстрирует реализацию надежного асинхронного взаимодействия между сервисами с гарантиями доставки сообщений и целостности данных. В основе архитектуры лежат паттерны 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.
Выполните команду в корне проекта:
docker compose up --build
После запуска доступны следующие точки входа:
| Сервис | 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) |
В директории tests/ находится коллекция GozonShop_Tests.postman_collection.json.
Шаг 1. Импорт
- Откройте Postman -> Import.
- Перетащите файл
tests/GozonShop_Tests.postman_collection.json.
Шаг 2. Запуск тестов
- Нажмите на название коллекции "Gozon Shop Integration Tests" в списке слева.
- Нажмите кнопку Run.
- В разделе Run configuration задайте Delay 500ms (необходимо для завершения асинхронной обработки заказа через очередь RabbitMQ).
- Нажмите кнопку Gozon Shop Integration Tests.
Тесты автоматически проверят:
- Создание и пополнение счетов.
- Успешную оплату (баланс уменьшается, статус PAID).
- Отказ в оплате при нехватке средств (баланс не меняется, статус FAILED).
- Обработку несуществующих пользователей.
Архитектура системы построена на принципах слабой связности (Low Coupling) и высокой надежности данных. Бизнес-логика разделена на два независимых микросервиса: Order Service и Payment Service, каждый из которых владеет собственной базой данных PostgreSQL.
В данной архитектуре было принято решение отказаться от выделенного API Gateway в пользу Infrastructure-based Routing.
- Снижение задержки: Прямое взаимодействие клиента с микросервисами через выделенные порты Docker исключает лишний сетевой узел, уменьшая время отклика системы.
- Принцип KISS (Keep It Simple, Stupid): Для текущего масштаба системы внедрение программного прокси добавляет избыточную сложность конфигурации без существенного выигрыша в функциональности.
- Изолированное тестирование: Разделение точек входа (порты 8081 и 8082) позволяет разрабатывать и отлаживать API каждого сервиса независимо друг от друга.
В качестве транспортного слоя между сервисами используется брокер сообщений RabbitMQ. Система брокера сообщений спроектирована с учетом того, что сеть ненадежна, поэтому он гарантирует доставку по модели at-least-once (минимум один раз).
Это означает, что сообщения могут дублироваться. Чтобы обеспечить корректность финансовых операций, реализована семантика exactly-once на уровне приложения, используя комбинацию идемпотентности и транзакционных паттернов.
Чтобы решить проблему "двойной записи", когда нужно атомарно сохранить заказ в БД и отправить событие в очередь, мы используем таблицу order_outbox.
- Атомарное сохранение: Метод
OrderRepository::create_orderв рамках одной SQL-транзакции сохраняет сам заказ и запись о событииORDER_CREATEDв таблицу Outbox. Это гарантирует, что событие не потеряется, даже если процесс упадет сразу после записи в БД. - Фоновая отправка: Функция
run_outbox_processorв фоновом режиме вычитывает события из Outbox и публикует их в RabbitMQ. Использование механизмаSKIP LOCKEDпозволяет запускать несколько экземпляров сервиса без конкуренции за события.
Для защиты от дубликатов сообщений (которые возможны при at-least-once доставке) на стороне сервиса оплат реализован паттерн Transactional Inbox.
- Избавление от дубликатов: В начале обработки метод
PaymentRepository::process_paymentпытается вставить уникальный идентификатор сообщения (связанный с ID заказа) в таблицуpayment_inbox. Если запись уже существует (конфликт уникальности), обработка прерывается, так как сообщение уже было обработано. - Гарантированная обработка: Если сообщение новое, сервис переходит к бизнес-логике списания средств.
Списание средств и отправка результата также защищены транзакционно.
- Списание средств: Внутри той же транзакции, где фиксируется запись в Inbox, происходит обновление баланса пользователя (метод
PaymentRepository::process_payment). Это обеспечивает строгую согласованность: баланс меняется тогда и только тогда, когда сообщение успешно учтено. - Transactional Outbox для ответа: Результат операции (успех или отказ из-за нехватки средств) не отправляется в сеть сразу, а сохраняется в таблицу
payment_outbox. - Отправка результата: Отдельный поток
run_payment_outboxдоставляет статус обработки обратно в Order Service через очередь результатов, замыкая цикл асинхронного взаимодействия.
Ключевой сценарий системы описывает процесс создания заказа и его автоматической оплаты через асинхронное взаимодействие микросервисов. Процесс разделен на этапы, обеспечивающие гарантии целостности данных.
Пользователь отправляет запрос на создание нового заказа.
Order Service принимает запрос и в рамках одной атомарной транзакции базы данных сохраняет сущность нового заказа и задачу на его оплату (запись в таблицу order_outbox). Это гарантирует, что заказ не может быть создан без инициирования процесса оплаты.
- Исполняющий метод:
OrderRepository::create_order
Фоновый процесс Order Service асинхронно вычитывает задачу из базы данных и отправляет соответствующее сообщение в брокер сообщений. Использование механизма подтверждения гарантирует, что сообщение попадет в очередь.
- Исполняющий метод:
run_outbox_processor(используетprocess_next_outbox_event)
Payment Service вычитывает задачу на оплату из очереди. Перед выполнением сервис сохраняет идентификатор сообщения в свою базу данных (таблица payment_inbox). Если запись с таким ID уже существует, обработка прекращается, что обеспечивает идемпотентность и защиту от дублей.
- Исполняющий метод:
run_payment_consumer(вызываетPaymentRepository::process_payment)
Payment Service выполняет бизнес-логику оплаты. В рамках единой транзакции происходит:
- Проверка наличия счета пользователя и достаточности средств.
- Атомарное списание средств (если условия выполнены) или фиксация ошибки.
- Запись результата (
PAIDилиFAILED) в таблицуpayment_outboxдля последующей отправки.
- Исполняющий метод:
PaymentRepository::process_payment
Процесс завершается двумя параллельными действиями:
- Отправка статуса: Фоновый процесс Payment Service вычитывает результат из своей базы данных и отправляет событие об успехе или неуспехе операции в очередь результатов.
- Исполняющий метод:
run_payment_outbox
- Обновление заказа: Order Service ожидает входящее событие из очереди результатов. Получив его, сервис обновляет статус соответствующего заказа в базе данных на финальный (
PAID,FAILED).
- Исполняющий метод:
run_result_consumer(вызываетOrderRepository::update_order_status)
Для обеспечения работы 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):
- Регистрация и Касса:
- В блоке "Счет" введите произвольный
User ID(например,67) и нажмите "Создать". - Пополните баланс этого пользователя (например, на
6767у.е.).
- В блоке "Счет" введите произвольный
- Покупка (Витрина):
- В блоке "Магазин" введите тот же
User ID, стоимость товара и описание. - Нажмите "КУПИТЬ".
- В блоке "Магазин" введите тот же
- Визуализация асинхронности:
- Интерфейс покажет
Order IDи начальный статусNEW. - Через секунду (после обработки очереди сообщений) статус автоматически обновится на
PAID(зеленый) илиFAILED(красный, если средств недостаточно).
- Интерфейс покажет
- История заказов:
- В нижней части экрана можно загрузить полный список всех операций пользователя, чтобы убедиться в корректности статусов всех сделанных заказов.