跳到主要内容

CQRS 架构

CQRS(Command Query Responsibility Segregation,命令查询职责分离)是一种将读操作和写操作分离的架构模式,通过使用不同的模型来处理命令(写)和查询(读),从而优化系统性能和可扩展性。

什么是 CQRS?

CQRS 的核心思想是将系统的读操作和写操作分离到不同的模型中:

  • 命令端(Command Side):处理数据的创建、更新、删除操作
  • 查询端(Query Side):处理数据的读取操作
┌─────────────────────────────────────────────────────────────────────────────┐
│ CQRS 架构示意图 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ 命令端 (Write Side) 查询端 (Read Side) │
│ ┌─────────────┐ ┌─────────────┐ │
│ │ Command │ │ Query │ │
│ │ Controller │ │ Controller │ │
│ └──────┬──────┘ └──────┬──────┘ │
│ │ │ │
│ ▼ ▼ │
│ ┌─────────────┐ ┌─────────────┐ │
│ │ Command │ │ Query │ │
│ │ Handler │ │ Handler │ │
│ └──────┬──────┘ └──────┬──────┘ │
│ │ │ │
│ ▼ ▼ │
│ ┌─────────────┐ 事件同步 ┌─────────────┐ │
│ │ Write DB │ ──────────────────────> │ Read DB │ │
│ │ (关系型DB) │ (Event/消息队列) │ (优化读取) │ │
│ └─────────────┘ └─────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘

为什么使用 CQRS?

传统方式的局限

在传统的 CRUD 架构中,同一个模型既要处理写操作又要处理读操作:

// 传统方式:同一个实体处理读写
@Entity
public class Order {
@Id
private Long id;
private String orderNumber;
private Long userId;
private BigDecimal totalAmount;
private OrderStatus status;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;

// 写操作需要的复杂业务逻辑
// 读操作需要的各种关联查询
// 导致实体变得臃肿
}

问题:

  • 读操作和写操作的优化目标冲突
  • 复杂的查询需要关联多张表
  • 领域模型被查询需求污染

CQRS 的优势

优势说明
性能优化读模型可以针对查询优化,写模型针对事务优化
可扩展性读写可以独立扩展
模型简化命令模型和查询模型各司其职,更加专注
灵活性可以使用不同的技术栈处理读写
安全性读写分离天然提供了安全边界

CQRS 实现模式

模式一:单数据库 CQRS

最简单的 CQRS 实现,使用同一个数据库,但使用不同的模型。

// 命令模型 - 关注业务规则
@Entity
@Table(name = "orders")
public class Order {
@Id
private String id;
private String userId;
private BigDecimal totalAmount;
private OrderStatus status;

// 业务方法
public void confirm() {
if (status != OrderStatus.PENDING) {
throw new IllegalStateException("只能确认待处理订单");
}
this.status = OrderStatus.CONFIRMED;
}
}

// 查询模型 - 针对读取优化
public class OrderSummary {
private String orderId;
private String userName; // 直接包含用户名,避免关联查询
private BigDecimal totalAmount;
private String status;
private LocalDateTime createdAt;
private int itemCount; // 直接包含统计信息

// 只有 getter,没有业务逻辑
}

// 查询仓库使用自定义 SQL 优化读取
@Repository
public interface OrderQueryRepository extends JpaRepository<OrderSummary, String> {

@Query("""
SELECT new com.example.OrderSummary(
o.id, u.username, o.totalAmount, o.status, o.createdAt, COUNT(oi)
)
FROM Order o
JOIN User u ON o.userId = u.id
LEFT JOIN OrderItem oi ON o.id = oi.orderId
WHERE o.status = :status
GROUP BY o.id
""")
List<OrderSummary> findByStatusWithSummary(@Param("status") String status);
}

模式二:事件溯源 CQRS

使用事件溯源作为写模型,投影到优化的读模型。

// 写模型 - 事件溯源
public class Order extends EventSourcedAggregate {
private String userId;
private List<OrderItem> items;
private OrderStatus status;

public static Order create(String userId, List<OrderItem> items) {
Order order = new Order();
order.raiseEvent(new OrderCreatedEvent(UUID.randomUUID().toString(),
userId, items));
return order;
}

public void confirm() {
if (status != OrderStatus.PENDING) {
throw new IllegalStateException("只能确认待处理订单");
}
raiseEvent(new OrderConfirmedEvent(this.id));
}

@Override
protected void handleEvent(DomainEvent event) {
switch (event.getEventType()) {
case "ORDER_CREATED" -> handleOrderCreated((OrderCreatedEvent) event);
case "ORDER_CONFIRMED" -> handleOrderConfirmed((OrderConfirmedEvent) event);
}
}
}

// 读模型 - 针对查询优化的投影
@Document(indexName = "orders")
public class OrderDocument {
@Id
private String orderId;
private String userId;
private String userName;
private List<OrderItemView> items;
private BigDecimal totalAmount;
private String status;
private LocalDateTime createdAt;
private List<String> tags;
private Map<String, Object> metadata;

// 为搜索优化的字段
private String searchText;
}

// 投影处理器
@Component
public class OrderProjectionHandler {

private final OrderReadRepository readRepository;
private final UserService userService;

@EventListener
public void onOrderCreated(OrderCreatedEvent event) {
UserDTO user = userService.getUser(event.getUserId());

OrderDocument document = new OrderDocument();
document.setOrderId(event.getOrderId());
document.setUserId(event.getUserId());
document.setUserName(user.getName());
document.setItems(event.getItems().stream()
.map(this::toItemView)
.collect(Collectors.toList()));
document.setTotalAmount(calculateTotal(event.getItems()));
document.setStatus("PENDING");
document.setCreatedAt(event.getOccurredOn());
document.setSearchText(buildSearchText(event, user));

readRepository.save(document);
}

@EventListener
public void onOrderConfirmed(OrderConfirmedEvent event) {
readRepository.updateStatus(event.getOrderId(), "CONFIRMED");
}
}

模式三:分离数据库 CQRS

读写使用完全不同的数据库技术。

// 写端 - 使用关系型数据库保证事务
@Service
@Transactional
public class OrderCommandService {

private final OrderRepository orderRepository;
private final EventPublisher eventPublisher;

public String createOrder(CreateOrderCommand command) {
Order order = Order.create(command.getUserId(), command.getItems());
orderRepository.save(order);

// 发布事件通知读端更新
eventPublisher.publish(new OrderCreatedEvent(order));

return order.getId();
}
}

// 读端 - 使用 Elasticsearch 优化搜索
@Service