Представьте: ваше приложение на Go похоже на перегруженного работой официанта в ресторане, отмеченном звездой Мишлен. Оно принимает заказы (записи), подаёт блюда (чтения), подливает напитки (обновления) и разбирается с «хочу поговорить с менеджером» Кэрри — всё это в неудобных туфлях. На помощь приходит CQRS — архитектурный аналог найма команды шеф-поваров и сомелье. Давайте создадим масштабируемое решение!
Почему вашему коду нужна терапия (и CQRS)
Традиционный CRUD подобен использованию швейцарского армейского ножа для проведения нейрохирургии — возможно, но грязно. CQRS (Command Query Responsibility Segregation) разделяет ваше приложение на:
- Сторона команд: «пишущие» повара, управляющие изменениями состояния.
- Сторона запросов: «читающий» обслуживающий персонал, подающий данные в качестве закуски.
Это разделение позволяет оптимизировать каждую сторону независимо — как если бы у вас были спортивные автомобили с турбонаддувом для записи и роскошные лимузины для чтения.
Реализация CQRS в Go: пошаговый рецепт
Шаг 1: Командуйте на кухне
Создадим структуры команд. Думайте о них как о рецептурных карточках для изменения состояния:
type CreateOrderCommand struct {
OrderID uuid.UUID
UserID uuid.UUID
Items []Item
SpecialNote string `json:"special_note" validate:"omitempty,max=500"`
}
type CancelOrderCommand struct {
OrderID uuid.UUID
Reason string `json:"reason" validate:"required"`
CancelledBy uuid.UUID
}
Наш обработчик команд действует как шеф-повар:
type OrderCommandHandler struct {
eventProducer EventProducer
}
func (h *OrderCommandHandler) Handle(ctx context.Context, cmd interface{}) error {
switch command := cmd.(type) {
case CreateOrderCommand:
if err := validate.Struct(command); err != nil {
return fmt.Errorf("invalid order: %w", err)
}
event := OrderCreatedEvent{
OrderID: command.OrderID,
Timestamp: time.Now().UTC(),
MenuItems: command.Items,
SpecialNote: command.SpecialNote,
}
return h.eventProducer.Publish(ctx, event)
// Обработка других команд...
}
return ErrUnknownCommand
}
Шаг 2: Kafka как ваш помощник по работе с событиями
Настроим наш Kafka-продюсер для трансляции событий заказа:
type KafkaEventProducer struct {
producer sarama.SyncProducer
topic string
}
func (k *KafkaEventProducer) Publish(ctx context.Context, event Event) error {
jsonData, _ := json.Marshal(event)
msg := &sarama.ProducerMessage{
Topic: k.topic,
Value: sarama.ByteEncoder(jsonData),
}
_, _, err := k.producer.SendMessage(msg)
if err != nil {
return fmt.Errorf("failed to send Kafka message: %w", err)
}
return nil
}
Совет: добавьте идентификаторы корреляции к вашим событиям — они похожи на номера отслеживания рецептов для ваших распределённых транзакций.
Шаг 3: Модели запросов — стол для гастрономических критиков
Наша модель чтения использует материализованные представления, оптимизированные для конкретных запросов:
type OrderProjection struct {
db *sqlx.DB
}
func (p *OrderProjection) HandleOrderCreated(event OrderCreatedEvent) error {
return p.db.Exec(`
INSERT INTO active_orders
(id, user_id, items, special_notes, status)
VALUES ($1, $2, $3, $4, 'PENDING')`,
event.OrderID,
event.UserID,
event.Items,
event.SpecialNote,
)
}
func (p *OrderProjection) GetActiveOrders(userID uuid.UUID) ([]OrderSummary, error) {
var orders []OrderSummary
err := p.db.Select(&orders, `
SELECT id, items, status
FROM active_orders
WHERE user_id = $1`, userID)
return orders, err
}
Распространённые проблемы на кухне (и как их устранить)
- Драма с согласованностью по итогу:
- Используйте проверки версий в командах.
- Реализуйте уведомления для пользователей о «выполнении операции».
- Добавьте компенсирующие действия для критических путей.
- Кошмар с штормом событий:
// Плохое событие — слишком расплывчато type OrderUpdatedEvent struct{} // Хорошее событие — конкретное изменение состояния type OrderLocationUpdatedEvent struct { NewLat float64 NewLong float64 UpdatedBy uuid.UUID }
- Паника из-за отставания Kafka-потребителя:
- Эффективно используйте группы потребителей.
- Мониторьте отставание с помощью метрик Prometheus.
- Реализуйте очереди мёртвых писем для проблемных сообщений.
Секретный соус: когда добавлять эту приправу
CQRS эффективен, когда:
- Вам нужна разная масштабируемость для чтения и записи.
- Сложная бизнес-логика требует аудита.
- Несколько команд работают над одним доменом.
- Вы хотите экспериментировать с разными хранилищами данных. Но помните: как и трюфельное масло, этой приправы нужно немного. Не используйте её для простых CRUD-приложений — это всё равно что принести огнемет на романтический ужин при свечах.
Напоследок: мудрость (и шутки отца)
Реализация CQRS похожа на обучение вашего кода танго — требуется практика, но когда всё синхронизируется, это красиво. Помните:
- «Исторический источник событий» — это не просто паттерн, это образ жизни.
- Потоки Kafka — это реки истины в вашей системе.
- Хороший идентификатор корреляции стоит своего веса в журналах отладки. Как мы говорим в распределённых системах: «Если с первого раза у вас не получилось, уничтожьте все доказательства своих попыток». Подождите, нет — это не так. Лучше придерживаться «согласованности по итогу»!
Теперь идите и создавайте системы, настолько устойчивые, что даже тараканы будут завидовать. Ваше приложение скажет вам спасибо — вероятно, через поток событий.