CQRS命令查询职责分离

一个报表查询的灾难

2021年,我们电商系统的订单报表页面加载需要 30 秒。

SELECT o.id, o.user_id, u.name, u.email,
       GROUP_CONCAT(p.name) as products,
       o.status, o.amount, o.created_at,
       s.name as shipping_status,
       (SELECT COUNT(*) FROM order_comments c WHERE c.order_id = o.id) as comment_count
FROM orders o
JOIN users u ON o.user_id = u.id
JOIN order_items oi ON o.id = oi.order_id
JOIN products p ON oi.product_id = p.id
LEFT JOIN shipping s ON o.id = s.order_id
WHERE o.created_at BETWEEN ? AND ?
GROUP BY o.id
ORDER BY o.created_at DESC
LIMIT 20 OFFSET 2000;

这个查询涉及 6 个表 JOIN,对于千万级订单表,每次翻页都要全表扫描。

CQRS 的思路:为什么读和写要用同一个数据模型?


二、CQRS 核心🔴

2.1 核心思想

传统架构:
  Command(增删改) → 同一个数据库 ← Query(查询)

CQRS:
  Command → 写模型(优化写入)→ 存储

              同步/异步

  Query  ← 读模型(优化读取)← 视图

2.2 简单实现

// 写模型:聚合
class OrderAggregate {
    public void addItem(Product product, int quantity) {
        // 写入优化:校验、状态管理
        if (this.status != OrderStatus.DRAFT) {
            throw new OrderCannotModifyException();
        }
        this.items.add(new OrderItem(product, quantity));
        this.totalAmount = calculateTotal();
    }
}

// 读模型:DTO
record OrderListDTO(
    Long orderId,
    String userName,
    String status,
    BigDecimal amount,
    int itemCount,
    LocalDateTime createdAt
) {}

record OrderDetailDTO(
    Long orderId,
    String userName,
    String userEmail,
    String userPhone,
    String status,
    BigDecimal amount,
    List<OrderItemDTO> items,
    ShippingInfoDTO shipping,
    List<CommentDTO> comments,
    LocalDateTime createdAt,
    LocalDateTime paidAt
) {}

2.3 同步机制

// 方式一:事件驱动同步
@EventListener
class OrderProjectionUpdater {
    @Autowired
    private JdbcTemplate jdbcTemplate;

    @Async
    @EventListener
    public void handle(OrderCreatedEvent event) {
        jdbcTemplate.update(
            "INSERT INTO order_read_model (id, user_id, status, amount, created_at) " +
            "VALUES (?, ?, ?, ?, ?)",
            event.getOrderId(), event.getUserId(), event.getStatus(),
            event.getAmount(), event.getCreatedAt()
        );
    }
}

// 方式二:同步写入
class CreateOrderUseCase {
    public Order execute(CreateOrderCommand cmd) {
        Order order = new Order(cmd.getUserId(), cmd.getItems());
        orderRepository.save(order);

        // 同步写入读模型
        OrderListDTO dto = toDTO(order);
        readModelRepository.save(dto);

        return order;
    }
}

三、读写分离的进阶设计🟡

3.1 物化视图

-- 创建物化视图
CREATE MATERIALIZED VIEW order_summary_mv AS
SELECT
    o.id as order_id,
    o.user_id,
    u.name as user_name,
    o.status,
    o.amount,
    COUNT(oi.id) as item_count,
    o.created_at
FROM orders o
JOIN users u ON o.user_id = u.id
LEFT JOIN order_items oi ON o.id = oi.order_id
GROUP BY o.id;

-- 刷新视图
REFRESH MATERIALIZED VIEW order_summary_mv;

3.2 数据库双写

@Service
class OrderService {
    @Autowired
    private JdbcTemplate writeDbTemplate;  // 写库
    @Autowired
    private JdbcTemplate readDbTemplate;   // 读库

    public void createOrder(Order order) {
        // 写库
        writeDbTemplate.update(
            "INSERT INTO orders (...) VALUES (...)",
            order.getId(), order.getAmount()
        );

        // 读库(可能有延迟)
        readDbTemplate.update(
            "INSERT INTO order_read_model (...) VALUES (...)",
            order.getId(), order.getAmount()
        );
    }
}

【架构权衡】 CQRS 的代价是数据一致性问题(读模型可能有延迟)和系统复杂度增加。只在确实有复杂查询或高并发读场景时才使用。


四、面试总结

级别期望回答
P5能说出 CQRS 的基本概念
P6能分析读写分离的同步机制
P7能设计完整的 CQRS 方案