System Design - Система учета покупок (Purchase Tracking System)
Цель: Научиться проектировать систему для обработки финансовых транзакций с акцентом на данные, консистентность и аналитику, используя Python-стек.
Длительность: 1.5 - 2 часа
Часть 1: Введение и бизнес-контекст (10 минут)
Проблема: Компания (ритейл, маркетплейс, SaaS) нуждается в системе для отслеживания всех покупок пользователей с возможностями аналитики, отчетности и интеграции.
Бизнес-требования:
- Учет всех транзакций с гарантией сохранности.
- Отслеживание статусов заказов (создан, оплачен, доставлен, отменен).
- Аналитика: топ товаров, LTV пользователя, сезонные тренды.
- Интеграция с платежными системами и ERP.
- Персонализация (рекомендации, кэшбэк).
Ключевые отличия от URL shortener:
- Акцент на данные и их консистентность, а не на низкую задержку.
- Сложные запросы (агрегации, JOIN) вместо простых key-value lookups.
- Строгие требования к надежности (финансовые данные).
- Необходимость обработки как онлайн-трафика, так и офлайн-аналитики.
Часть 2: Разбор кейса: Маркетплейс с 10 млн пользователей (70 минут)
Шаг 1: Требования и оценки (15 мин)
-
Функциональные требования:
- Регистрация покупки (пользователь, товары, сумма, время).
- Обновление статуса заказа.
- Получение истории покупок пользователя.
- Расчет и начисление кэшбэка/баллов.
- Формирование отчетов (выручка за период, топ товаров).
- Интеграционные вебхуки для оплаты.
-
Нефункциональные требования:
- Консистентность данных (главное!): Сумма списанных средств = сумме заказа. Недопустимо потерять транзакцию.
- Доступность: Система приема платежей должна быть высокодоступной.
- Масштабируемость: Пиковые нагрузки (Черная пятница, распродажи).
- Аудит и откат: Возможность отследить цепочку событий и отменить ошибочную операцию.
-
Оценки масштаба:
- Активных пользователей: 10 млн.
- Средний чек: 5000 руб.
- Среднее количество покупок на пользователя в месяц: 2.
- Пиковый RPS на запись (оформление заказа): (10M * 2 / 30 дней / 86400 сек) * 10 (пиковый коэффициент) ≈ 80 RPS.
- Пиковый RPS на чтение (история заказов): 80 RPS * 50 ≈ 4000 RPS.
- Объем данных за год (только заголовки заказов): 10M * 2 * 12 мес * 2 КБ ≈ 480 ГБ/год.
- Объем данных о товарах в заказах (детализация): в 5-10 раз больше.
Шаг 2: Высокоуровневая архитектура и выбор технологий (20 мин)
Пользователь -> [API Gateway / Load Balancer] -> [Микросервисы] -> [Базы данных и очереди]
Core-сервисы (Python):
- Order Service: Ядро системы. Создание заказа, управление статусом.
- Технологии: FastAPI/Django (REST), SQLAlchemy/asyncpg.
- База данных: PostgreSQL. Причины: транзакции (ACID), сложные запросы по истории, джойны с пользователями/товарами. Используем схемы и партиционирование по дате.
- Payment Service: Взаимодействие с эквайрингом (ЮKassa, Stripe, Tinkoff).
- Технологии: aiohttp/httpx для асинхронных вызовов к API банков.
- База данных: PostgreSQL. Требует строгой согласованности.
- Analytics Service: Сбор и предрасчет метрик.
- Технологии: Celery для периодических задач, Pandas/Polars для обработки, ClickHouse/PostgreSQL + TimescaleDB для хранения агрегатов.
- Notification Service: Отправка email (через SendGrid) и push о смене статуса.
- Технологии: FastAPI, Celery/RQ для фоновых отправок.
Коммуникация между сервисами:
- Синхронная (HTTP/REST/gRPC): Когда нужен immediate response (создание заказа -> проверка наличия товара).
- Асинхронная (очереди сообщений): Для декoupling и фоновых задач. Используем RabbitMQ/Kafka.
# Пример: Отправка события в RabbitMQ (pika) import pika connection = pika.BlockingConnection(pika.ConnectionParameters('localhost')) channel = connection.channel() channel.queue_declare(queue='order_created') channel.basic_publish(exchange='', routing_key='order_created', body='{"order_id": 123, "user_id": 456}')
Шаг 3: Детальное проектирование. Обработка потока заказа (15 мин)
# order_service/api.py (упрощенно)
from fastapi import FastAPI, BackgroundTasks
from pydantic import BaseModel
from datetime import datetime
app = FastAPI()
class OrderCreate(BaseModel):
user_id: int
items: list[dict]
total: float
@app.post("/orders")
async def create_order(order_data: OrderCreate, background_tasks: BackgroundTasks):
# 1. НАЧАЛО ТРАНЗАКЦИИ в PostgreSQL
# 2. Проверка данных (наличие товара, баланс пользователя - запрос к другим сервисам)
# 3. Создание записи Order в статусе "PENDING"
new_order_id = save_order_to_db(order_data)
# 4. ПОЛУЧЕНИЕ ТРАНЗАКЦИИ. Фиксация факта создания заказа.
# 5. АСИНХРОННО: Отправка события в очередь для Payment Service
background_tasks.add_task(publish_to_message_queue, 'order.created', {'order_id': new_order_id})
# 6. АСИНХРОННО: Запуск задачи на предрасчет рекомендаций
background_tasks.add_task(celery_app.send_task, 'update_user_profile', args=[order_data.user_id])
return {"order_id": new_order_id, "status": "processing"}
# payment_service/consumer.py
# Слушает очередь 'order.created', вызывает API банка, обновляет статус заказа.
# Важно: идемпотентность. Повторное сообщение не должно списывать деньги дважды.
Ключевые моменты:
- Транзакционность: Используем with session.begin(): в SQLAlchemy для atomic операций.
- Идемпотентность API: Генерация idempotency_key на клиенте для повторной безопасной отправки запроса.
- SAGA-паттерн: Для распределенных транзакций между Order и Payment сервисами. Компенсирующие действия при ошибках (например, отмена заказа, если платеж не прошел).
Шаг 4: Масштабирование и работа с данными (20 мин)
-
Шардирование БД заказов:
- По user_id: Все заказы пользователя в одном шарде. Упрощает получение истории. Проблема: горячие шарды для VIP-клиентов.
- По дате (партиционирование): ORDER BY created_at работает быстро. Упрощает архивацию.
-- PostgreSQL пример партиционирования CREATE TABLE orders ( id BIGSERIAL, user_id INT, created_at TIMESTAMP DEFAULT now(), total DECIMAL ) PARTITION BY RANGE (created_at); CREATE TABLE orders_2024_05 PARTITION OF orders FOR VALUES FROM ('2024-05-01') TO ('2024-06-01'); -
Кэширование:
- Redis для горячих данных: История последних 10 заказов пользователя, часто запрашиваемые товары. Инвалидация при обновлении статуса.
# Кэширование с помощью aiocache (асинхронный) from aiocache import Cache cache = Cache(Cache.REDIS, endpoint="localhost", port=6379, namespace="orders") async def get_user_orders(user_id: int): cached = await cache.get(f"last_orders:{user_id}") if cached: return cached orders = await db.fetch_user_orders(user_id) # Долгий запрос await cache.set(f"last_orders:{user_id}", orders, ttl=300) return orders -
Аналитика и отчетность (отдельный контур данных):
- Проблема: Сложные аналитические запросы убивают OLTP-базу.
- Решение (CDC - Change Data Capture): Делаем снимок данных из PostgreSQL и загружаем в ClickHouse (для быстрых агрегаций) или в S3 + AWS Athena (для ad-hoc-аналитики).
- Инструменты: Debezium (Kafka Connect) для захвата изменений или собственный механизм на Python, публикующий события в Kafka.
-
Резервное копирование и аудит:
- Все финансовые события (создание заказа, списание, отмена) пишутся в неизменяемый лог (Kafka с long retention) или в append-only таблицу в БД. Это источник истины для сверок.
Часть 3: Паттерны и антипаттерны для финансовых систем на Python (15 минут)
Паттерны:
- Repository/Unit of Work (SQLAlchemy Session): Для управления транзакциями.
- SAGA/Outbox Pattern: Для координации обновлений между микросервисами. Реализация через таблицу outbox_messages в той же транзакции, что и основное изменение.
- CQRS (Command Query Responsibility Segregation): Разделение модели записи (PostgreSQL) и модели чтения (кэш, предрассчитанные материализованные представления).
- Retry with Exponential Backoff: Для вызовов внешних платежных API (tenacity библиотека).
- Circuit Breaker: Защита от сбоев внешних сервисов.
Антипаттерны:
- Прямые JOIN между микросервисами через БД: Нарушает инкапсуляцию. Общайтесь только через API.
- Игнорирование идемпотентности: Приводит к дублированию списаний.
- Блокирующие вызовы в цикле событий: Вызов requests.get() в асинхронном платежном вебхуке заблокирует все другие запросы.
- Хранение денежных сумм в float: Использовать Decimal с фиксированной точностью.
- Отсутствие метрик и алертов: Не знать о росте времени ответа БД или количестве неудачных платежей.
Часть 4: Практическое задание (5 минут)
Задание: Спроектировать систему лояльности с кэшбэком в рамках маркетплейса.
- Требования:
- Начисление баллов (1% от суммы покупки).
- Списание баллов при следующей покупке.
- История начислений/списаний.
- TTL для баллов (сгорание через 1 год).
- Real-time отображение баланса в личном кабинете.
- Вопросы для проектирования:
- Как гарантировать консистентность между списанием денег и начислением баллов? (Транзакция? SAGA?).
- Как рассчитать баланс пользователя быстро? (Кэш, отдельная таблица с текущим балансом).
- Как реализовать TTL для баллов? (Фоновая задача Celery, которая пересчитывает балансы).
- Какая БД лучше подходит для хранения событий лояльности? (PostgreSQL для транзакций, Redis для горячего баланса).
Резюме и ключевые выводы:
- Консистентность важнее latency в финансовых системах.
- Python + PostgreSQL — отличная база для бизнес-логики и данных, но нужны правильные архитектурные решения (партиционирование, индексы).
- Микросервисы + очереди помогают масштабировать и изолировать отказы.
- Разделяй online (OLTP) и offline (OLAP) нагрузку. Используй разные хранилища.
- Пиши идемпотентный код, логируй все финансовые события, имей план отката.
Дополнительные материалы:
- Книга: "Designing Data-Intensive Applications" Martin Kleppmann.
- Статьи: Реальные кейсы от Uber (Schemaless), Airbnb (Airflow), Spotify (Python микросервисы).
- Библиотеки Python для production: FastAPI, SQLAlchemy, Celery, pydantic, structlog, prometheus-client.