【架构实战】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. 做好监控和告警

个人观点,仅供参考

相关推荐
技术传感器2 小时前
Hermes为什么开始像基础设施:11万星、RCE修复与生态接入
人工智能·安全·架构·aigc
执于代码2 小时前
智能客服的agent 的架构和作用以及源码分析
架构
AI创界者4 小时前
【独家解析】Ernie-Image-AIO-Rapid一键部署本地运行整合包:深度融合架构如何重塑AI绘图效率?4K超分与硬件适配全指南
人工智能·架构
BullSmall5 小时前
Redis 双机部署 完整方案(两种架构,适配两台机器)
java·redis·架构
SamDeepThinking7 小时前
适合中小型企业的出口入口网关微服务
java·后端·架构
LSL666_8 小时前
微服务架构
微服务·云原生·架构
威迪斯特8 小时前
GoFr框架:加速微服务开发的Go语言利器
开发语言·后端·微服务·架构·golang·命令行框架·gofr框架
小程故事多_809 小时前
AI编码效率革命,Agent Orchestrator如何让多智能体并行开发成为现实
人工智能·架构·智能体
feng14569 小时前
OpenSREClaw - OpenClaw 多 Agent 架构
人工智能·架构