【架构实战】CQRS架构模式实战

一、CQRS概述

CQRS(Command Query Responsibility Segregation,命令查询职责分离)是一种架构模式:

核心思想:

  • 命令(Command):修改数据的操作
  • 查询(Query):读取数据的操作
  • 两者使用不同的模型和存储

为什么需要CQRS:

  • 读写负载不均衡
  • 读写数据结构差异大
  • 需要独立的读写优化

二、CQRS核心概念

1. 基本模型

复制代码
┌─────────────────────────────────────────────────────────────┐
│                         CQRS架构                             │
├─────────────────────────────────────────────────────────────┤
│                                                              │
│   ┌──────────────┐              ┌──────────────┐           │
│   │    命令端     │              │    查询端     │           │
│   │  (写入优化)   │──── 同步 ────▶│  (读取优化)   │           │
│   └──────┬───────┘              └──────▲───────┘           │
│          │                              │                     │
│          ▼                              │                     │
│   ┌──────────────┐              ┌──────────────┐           │
│   │  命令数据库   │              │  查询数据库   │           │
│   │ (事务存储)   │              │  (只读副本)   │           │
│   └──────────────┘              └──────────────┘           │
│                                                              │
└─────────────────────────────────────────────────────────────┘

2. 命令端

java 复制代码
// 命令接口
public interface CommandHandler<C extends Command> {
    void handle(C command);
}

// 命令基类
public abstract class Command {
    private final String commandId;
    private final LocalDateTime timestamp;
    
    protected Command() {
        this.commandId = UUID.randomUUID().toString();
        this.timestamp = LocalDateTime.now();
    }
}

// 创建订单命令
public class CreateOrderCommand extends Command {
    private final String customerId;
    private final List<OrderItemData> items;
    
    public CreateOrderCommand(String customerId, List<OrderItemData> items) {
        this.customerId = customerId;
        this.items = items;
    }
}

// 命令处理器
@Service
public class OrderCommandHandler implements CommandHandler<CreateOrderCommand> {
    
    @Autowired
    private OrderRepository orderRepository;
    
    @Autowired
    private EventPublisher eventPublisher;
    
    @Transactional
    @Override
    public void handle(CreateOrderCommand command) {
        // 创建订单聚合
        Order order = Order.create(
            OrderId.generate(),
            CustomerId.of(command.getCustomerId())
        );
        
        // 添加商品
        for (OrderItemData itemData : command.getItems()) {
            Product product = productRepository.findById(ProductId.of(itemData.getProductId()));
            order.addItem(product, itemData.getQuantity());
        }
        
        // 提交订单
        order.submit();
        
        // 保存
        orderRepository.save(order);
        
        // 发布事件
        eventPublisher.publish(new OrderCreatedEvent(order));
    }
}

3. 查询端

java 复制代码
// 查询接口
public interface QueryHandler<Q extends Query, R> {
    R handle(Q query);
}

// 查询基类
public abstract class Query {
    // 查询参数
}

// 订单查询DTO(专为读取优化)
public class OrderQueryDTO {
    private Long orderId;
    private String orderNo;
    private String customerName;    // 可能需要JOIN
    private String statusText;      // 状态转换
    private BigDecimal totalAmount;
    private List<OrderItemQueryDTO> items;
    private String createTimeText;  // 格式化时间
    
    // 允许非常灵活的查询模型
}

// 查询处理器
@Service
public class OrderQueryHandler implements QueryHandler<OrderQuery, List<OrderQueryDTO>> {
    
    @Autowired
    private OrderReadRepository readRepository;
    
    @Override
    public List<OrderQueryDTO> handle(OrderQuery query) {
        return readRepository.findOrders(query);
    }
}

三、数据同步方案

1. 同步复制

复制代码
┌────────────┐     同步写入      ┌────────────┐
│  命令数据库  │ ──────────────▶│  查询数据库  │
│  (OLTP)    │   实时同步      │  (OLAP)    │
└────────────┘                └────────────┘
java 复制代码
// 同步复制实现
@Service
public class SynchronousReplicationService {
    
    @Autowired
    private JdbcTemplate commandJdbcTemplate;
    
    @Autowired
    private JdbcTemplate queryJdbcTemplate;
    
    @Transactional
    public void saveOrder(Order order) {
        // 写入命令数据库
        String sql = "INSERT INTO orders (id, order_no, customer_id, status, total_amount) " +
                     "VALUES (?, ?, ?, ?, ?)";
        
        commandJdbcTemplate.update(sql, 
            order.getId(), order.getOrderNo(), 
            order.getCustomerId(), order.getStatus().name(),
            order.getTotalAmount());
        
        // 同步写入查询数据库
        String querySql = "INSERT INTO v_orders (id, order_no, customer_name, status_text, total_amount) " +
                          "VALUES (?, ?, ?, ?, ?)";
        
        queryJdbcTemplate.update(querySql,
            order.getId(), order.getOrderNo(),
            order.getCustomerName(),  // 查询端需要的字段
            order.getStatus().getText(),
            order.getTotalAmount());
    }
}

2. 事件驱动复制

java 复制代码
// 事件监听同步
@Component
public class OrderEventSynchronizer {
    
    @Autowired
    private OrderReadRepository readRepository;
    
    @KafkaListener(topics = "order-events")
    public void handleOrderEvent(OrderEvent event) {
        if (event instanceof OrderCreatedEvent) {
            OrderCreatedEvent created = (OrderCreatedEvent) event;
            // 转换为查询模型
            OrderQueryModel model = toQueryModel(created.getOrder());
            readRepository.save(model);
        }
        
        if (event instanceof OrderUpdatedEvent) {
            OrderUpdatedEvent updated = (OrderUpdatedEvent) event;
            readRepository.update(toQueryModel(updated.getOrder()));
        }
        
        if (event instanceof OrderCancelledEvent) {
            OrderCancelledEvent cancelled = (OrderCancelledEvent) event;
            readRepository.delete(cancelled.getOrderId());
        }
    }
    
    private OrderQueryModel toQueryModel(Order order) {
        return OrderQueryModel.builder()
            .id(order.getId())
            .orderNo(order.getOrderNo())
            .customerName(getCustomerName(order.getCustomerId()))
            .statusText(order.getStatus().getText())
            .totalAmount(order.getTotalAmount())
            .items(order.getItems().stream()
                .map(this::toItemModel)
                .collect(Collectors.toList()))
            .build();
    }
}

3. 最终一致性

复制代码
┌────────────┐     事件      ┌────────────┐     消费      ┌────────────┐
│  命令端     │ ──────────▶│  消息队列   │ ───────────▶│  查询端     │
│  (聚合根)   │            │  (Kafka)   │            │  (投影)    │
└────────────┘              └────────────┘              └────────────┘
                                  │
                                  ▼
                            ┌────────────┐
                            │  事件存储   │
                            │  (EventStore)│
                            └────────────┘

四、读写分离优化

1. 命令端优化

java 复制代码
// 命令端:事务优先,保证一致性
@Service
public class OrderCommandService {
    
    @Transactional(isolation = Isolation.REPEATABLE_READ)
    public OrderDTO createOrder(CreateOrderCommand command) {
        // 严格的业务校验
        validateBusinessRules(command);
        
        // 创建聚合
        Order order = orderAggregateFactory.create(command);
        
        // 保存到主库
        orderRepository.save(order);
        
        // 发布领域事件
        eventPublisher.publish(order.getDomainEvents());
        
        return toDTO(order);
    }
    
    private void validateBusinessRules(CreateOrderCommand command) {
        // 检查库存
        for (OrderItemData item : command.getItems()) {
            if (!inventoryService.checkStock(item.getProductId(), item.getQuantity())) {
                throw new InsufficientStockException(item.getProductId());
            }
        }
        
        // 检查客户信用
        if (!creditService.checkCredit(command.getCustomerId(), command.getTotalAmount())) {
            throw new InsufficientCreditException(command.getCustomerId());
        }
    }
}

2. 查询端优化

java 复制代码
// 查询端:性能优先,支持各种读取场景
@Service
public class OrderQueryService {
    
    @Autowired
    private JdbcTemplate jdbcTemplate;
    
    // 场景1:订单列表(分页)
    public Page<OrderListDTO> listOrders(OrderListQuery query) {
        String sql = """
            SELECT o.*, c.name as customer_name, 
                   (SELECT COUNT(*) FROM order_items WHERE order_id = o.id) as item_count
            FROM orders o
            LEFT JOIN customers c ON o.customer_id = c.id
            WHERE o.status = ?
            ORDER BY o.create_time DESC
            LIMIT ? OFFSET ?
            """;
        
        // 直接执行优化的查询
        return jdbcTemplate.query(sql, 
            (rs, rowNum) -> toOrderListDTO(rs), 
            query.getStatus(), query.getPageSize(), query.getOffset());
    }
    
    // 场景2:订单详情(JOIN多表)
    public OrderDetailDTO getOrderDetail(Long orderId) {
        String sql = """
            SELECT o.*, c.name as customer_name, c.phone as customer_phone,
                   p.name as payment_name, p.method as payment_method
            FROM orders o
            LEFT JOIN customers c ON o.customer_id = c.id
            LEFT JOIN payments p ON o.id = p.order_id
            WHERE o.id = ?
            """;
        
        return jdbcTemplate.queryForObject(sql, this::toOrderDetailDTO, orderId);
    }
    
    // 场景3:统计报表
    public OrderStatisticsDTO getStatistics(OrderStatisticsQuery query) {
        String sql = """
            SELECT 
                DATE(create_time) as date,
                COUNT(*) as order_count,
                SUM(total_amount) as total_amount,
                AVG(total_amount) as avg_amount
            FROM orders
            WHERE create_time BETWEEN ? AND ?
            GROUP BY DATE(create_time)
            """;
        
        return jdbcTemplate.query(sql, 
            (rs, rowNum) -> toStatisticsDTO(rs),
            query.getStartDate(), query.getEndDate()).stream()
            .collect(Collectors.groupingBy(OrderStatisticsDTO::getDate))
            .values().stream().findFirst().orElse(new OrderStatisticsDTO());
    }
}

五、视图模型设计

1. 查询数据库表设计

sql 复制代码
-- 命令端:规范化设计
CREATE TABLE orders (
    id BIGINT PRIMARY KEY,
    order_no VARCHAR(32),
    customer_id BIGINT,
    status VARCHAR(20),
    total_amount DECIMAL(12,2),
    create_time TIMESTAMP
);

CREATE TABLE order_items (
    id BIGINT PRIMARY KEY,
    order_id BIGINT,
    product_id BIGINT,
    quantity INT,
    price DECIMAL(10,2)
);

-- 查询端:反规范化设计,冗余常用字段
CREATE TABLE v_orders (
    id BIGINT PRIMARY KEY,
    order_no VARCHAR(32),
    
    -- 冗余的客户信息(避免JOIN)
    customer_id BIGINT,
    customer_name VARCHAR(100),
    customer_phone VARCHAR(20),
    customer_address VARCHAR(200),
    
    -- 状态文本(避免转换)
    status VARCHAR(20),
    status_text VARCHAR(50),
    status_color VARCHAR(20),
    
    -- 预计算的金额
    total_amount DECIMAL(12,2),
    discount_amount DECIMAL(12,2),
    final_amount DECIMAL(12,2),
    
    -- 预格式化的时间
    create_time TIMESTAMP,
    create_time_text VARCHAR(50),
    create_time_date DATE,
    
    -- 冗余的商品数量(避免子查询)
    item_count INT,
    item_names TEXT,
    
    INDEX idx_customer (customer_id),
    INDEX idx_status (status),
    INDEX idx_create_time (create_time_date)
);

2. ES查询模型

java 复制代码
// Elasticsearch视图模型
@Document(indexName = "orders")
public class OrderIndexModel {
    
    @Id
    private String id;
    
    @Field(type = FieldType.Keyword)
    private String orderNo;
    
    @Field(type = FieldType.Long)
    private Long customerId;
    
    @Field(type = FieldType.Text, analyzer = "ik_max_word")
    private String customerName;
    
    @Field(type = FieldType.Keyword)
    private String status;
    
    @Field(type = FieldType.Text)
    private String statusText;
    
    @Field(type = FieldType.Double)
    private BigDecimal totalAmount;
    
    @Field(type = FieldType.Nested)
    private List<OrderItemIndex> items;
    
    @Field(type = FieldType.Date)
    private LocalDateTime createTime;
    
    @Field(type = FieldType.Text)
    private String createTimeText;
    
    // 支持全文搜索
    @Field(type = FieldType.Text, analyzer = "ik_max_word")
    private String searchText; // orderNo + customerName + productNames
}

六、CQRS实现框架

1. Axon Framework

java 复制代码
// Axon Framework实现CQRS
@SpringBootApplication
@EnableAxonFramework
public class OrderApplication {
}

@Aggregate
public class OrderAggregate {
    
    @AggregateIdentifier
    private String orderId;
    
    @CommandHandler
    public OrderAggregate(CreateOrderCommand command) {
        apply(new OrderCreatedEvent(command.getOrderId(), command.getCustomerId()));
    }
    
    @EventSourcingHandler
    public void on(OrderCreatedEvent event) {
        this.orderId = event.getOrderId();
    }
    
    @CommandHandler
    public void handle(AddItemCommand command) {
        apply(new ItemAddedEvent(orderId, command.getProductId(), command.getQuantity()));
    }
}

@Component
public class OrderEventHandler {
    
    @EventHandler
    public void on(OrderCreatedEvent event) {
        // 更新查询端
        OrderProjection projection = OrderProjection.builder()
            .orderId(event.getOrderId())
            .status("CREATED")
            .build();
        
        orderProjectionRepository.save(projection);
    }
}

2. Spring CQRS示例

java 复制代码
// 命令端
@Service
@RequiredArgsConstructor
public class OrderCommandService {
    
    private final CommandGateway commandGateway;
    
    public String createOrder(CreateOrderCommand command) {
        return commandGateway.send(command);
    }
}

// 查询端
@Service
@RequiredArgsConstructor
public class OrderQueryService {
    
    private final JdbcTemplate jdbcTemplate;
    
    public List<OrderDTO> listOrders(Long customerId) {
        return jdbcTemplate.query(
            "SELECT * FROM orders WHERE customer_id = ?",
            (rs, rowNum) -> toDTO(rs),
            customerId
        );
    }
}

七、CQRS最佳实践

1. 何时使用CQRS

场景 建议
简单CRUD 不需要CQRS
读写负载差异大 考虑CQRS
复杂业务逻辑 考虑CQRS
需要高并发读取 适合CQRS
报表和分析需求 非常适合CQRS

2. 注意事项

复制代码
1. 避免过度设计
   - 小型应用不需要CQRS
   
2. 处理好最终一致性
   - 命令端和查询端可能短暂不一致
   
3. 选择合适的数据同步方式
   - 同步:延迟低,但影响写入性能
   - 异步:写入快,但存在延迟
   
4. 保持命令和查询的独立性
   - 不要在命令端直接查询

3. 与其他模式结合

复制代码
CQRS + DDD:
- 命令端使用DDD设计聚合根
- 查询端使用投影构建视图模型

CQRS + Event Sourcing:
- 命令端存储事件
- 查询端订阅事件构建投影

CQRS + 微服务:
- 每个服务独立使用CQRS
- 通过事件总线同步数据

八、总结

CQRS是一种强大的架构模式:

  • 命令端:专注业务逻辑,保证一致性
  • 查询端:专注读取性能,支持灵活查询
  • 数据同步:同步或异步,根据场景选择
  • 适用场景:读写负载不均、复杂业务、需要高并发

最佳实践:

  1. 优先考虑简单架构
  2. 根据实际需求决定是否使用CQRS
  3. 处理好一致性问题
  4. 做好监控和告警

个人观点,仅供参考

相关推荐
郑寿昌5 小时前
边缘AI传感:架构革命与智能跃迁
架构
IPHWT 零软网络6 小时前
从 SIP 软交换到国密加密:OM1000‑A‑UC 国产化 IPPBX 的架构与实战价值
架构·信息与通信·信创·国产化·ippbx
2601_957786776 小时前
短视频矩阵全链路自动化系统的技术架构与性能实测
矩阵·架构·自动化
青天喵喵10 小时前
Linux WiFi 架构解析:连接流程(基础篇二)
linux·运维·架构·嵌入式·wi-fi·sta·ap
heimeiyingwang10 小时前
【架构实战】RPC框架Dubbo3.0:高性能Java通信之道
java·rpc·架构
万岳科技系统开发10 小时前
直播电商APP搭建如何支持多门店与多主播模式
小程序·架构
TheRouter11 小时前
把 ClaudeCode 换成DeepSeek V4:两行配置,成本立省80%(含 Anthropic 兼容接口)
网络·架构
数字化顾问12 小时前
(122页PPT)企业数字化IT架构蓝图规划设计方案(附下载方式)
java·运维·架构
有才不一定有德12 小时前
动态生成 vs 静态预制:复杂 Agent 系统的 Sub-Agent 架构选择
架构·agent
一切皆是因缘际会12 小时前
本地大模型轻量化部署
大数据·人工智能·机器学习·架构