一、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是一种强大的架构模式:
- 命令端:专注业务逻辑,保证一致性
- 查询端:专注读取性能,支持灵活查询
- 数据同步:同步或异步,根据场景选择
- 适用场景:读写负载不均、复杂业务、需要高并发
最佳实践:
- 优先考虑简单架构
- 根据实际需求决定是否使用CQRS
- 处理好一致性问题
- 做好监控和告警
个人观点,仅供参考