PHP CQRS 架构实战:从理论到代码的完整示例解析
目录导读
- 什么是 CQRS?为什么 PHP 项目需要它?
- CQRS 的核心概念:Command 与 Query 的彻底分离
- PHP 实现 CQRS 的基础环境搭建
- PHP CQRS 实战示例:构建一个订单管理系统
- 1 定义 Command 与 Command Handler
- 2 定义 Query 与 Query Handler
- 3 命令总线(Command Bus)与查询总线(Query Bus)的实现
- CQRS 与数据库读写分离的整合策略
- 常见坑与性能优化建议
- Q&A 高频问题解答
什么是 CQRS?为什么 PHP 项目需要它?
CQRS(Command Query Responsibility Segregation,命令查询职责分离)是一种将系统的“写操作”(Command,命令)和“读操作”(Query,查询)彻底分离的架构模式,在传统 MVC 中,我们通常使用同一个模型(Model)来处理数据的增删改查,但一旦业务逻辑复杂化,这种“一刀切”的方式会导致代码臃肿、数据库锁竞争激烈、查询性能低下。

PHP 项目引入 CQRS 的核心价值:
- 性能优化:读操作可以针对查询场景设计独立的数据库索引,甚至使用 NoSQL 或缓存(如 Redis),而写操作则专注于事务一致性。
- 代码清晰度:写操作关注业务规则(如订单状态校验),读操作关注数据展示(如列表分页),两者解耦后,团队可并行开发。
- 可扩展性:在读多写少的场景(如电商商品详情页),可以单独对 Query 侧做水平扩展。
CQRS 的核心概念:Command 与 Query 的彻底分离
- Command(命令):以动词命名(如
CreateOrderCommand),表示“用户想做什么”,它是意图性的,不返回数据,只改变系统状态,通常会经历验证、业务逻辑执行、持久化三个步骤。 - Query(查询):以名词或描述性命名(如
GetOrderDetailsQuery),表示“用户想看什么”,它不修改数据,只从数据库或缓存中读取数据并返回 DTO(数据传输对象)。
关键原则:不要在一个类中同时处理 Command 和 Query 逻辑,你甚至可以将读写数据库拆分到不同的物理数据库(如 MySQL 主从分离),但 CQRS 的难点在于 最终一致性 的处理(写入后读取可能短暂存在延迟)。
PHP 实现 CQRS 的基础环境搭建
我们使用纯 PHP + Composer 构建,不依赖重型框架,方便理解核心机制。
composer require symfony/messenger # 作为消息总线(实现 Command/Query 分发)
目录结构建议:
src/
├── Command/ # 存放所有 Command 类
├── CommandHandler/ # 存放对应的处理器
├── Query/ # 存放所有 Query 类
├── QueryHandler/ # 存放查询处理器
├── Bus/ # 自定义总线接口(或直接用 Symfony Messenger)
└── Model/ # 实体与 DTO
PHP CQRS 实战示例:构建一个订单管理系统
1 定义 Command 与 Command Handler
// src/Command/CreateOrderCommand.php
final class CreateOrderCommand
{
public function __construct(
public readonly string $orderId,
public readonly int $userId,
public readonly float $totalAmount,
public readonly array $items // 商品项
) {}
}
// src/CommandHandler/CreateOrderCommandHandler.php
final class CreateOrderCommandHandler
{
public function __construct(private OrderRepositoryInterface $orderRepo) {}
public function __invoke(CreateOrderCommand $command): void
{
// 1. 业务逻辑校验(如库存检查)
// 2. 创建订单实体
$order = new Order($command->orderId, $command->userId, $command->totalAmount, $command->items);
// 3. 保存到写数据库(主库)
$this->orderRepo->save($order);
// 4. 触发事件(如发送邮件通知,可通过异步处理)
}
}
2 定义 Query 与 Query Handler
// src/Query/GetOrderDetailsQuery.php
final class GetOrderDetailsQuery
{
public function __construct(public readonly string $orderId) {}
}
// src/QueryHandler/GetOrderDetailsQueryHandler.php
final class GetOrderDetailsQueryHandler
{
public function __construct(private OrderReadModelInterface $readModel) {}
public function __invoke(GetOrderDetailsQuery $query): OrderDetailsDTO
{
// 直接从只读副本(从库或缓存)读取
return $this->readModel->getOrderDetails($query->orderId);
}
}
3 命令总线与查询总线的实现
使用 Symfony Messenger 简化:
// 在 services.yaml 中配置
framework:
messenger:
buses:
command.bus: ~
query.bus: ~
routing:
'App\Command\CommandInterface': command.bus
'App\Query\QueryInterface': query.bus
调用方式:
// 写操作
$this->commandBus->dispatch(new CreateOrderCommand(...));
// 读操作
$orderDetails = $this->queryBus->dispatch(new GetOrderDetailsQuery('order_123'))->getResult();
CQRS 与数据库读写分离的整合策略
- 写库:使用 MySQL InnoDB 引擎,保证 ACID 事务,所有 Command Handler 通过主库连接(如
PdoWriteAdapter)操作。 - 读库:可以使用 MySQL 从库、Redis 缓存或 Elasticsearch,对于订单查询,推荐使用 Redis 哈希结构存储高频访问的订单摘要,降低数据库压力。
- 同步策略:在 Command Handler 中,保存数据到主库后,通过异步消息(如 RabbitMQ)将变更同步到缓存或从库,注意 最终一致性:如果同步失败,要提供重试机制。
常见坑与性能优化建议
- 坑 1:过度设计,CRUD 简单应用强制使用 CQRS,会降低开发效率,建议只在复杂业务模块(如订单、支付)中使用。
- 坑 2:忽略查询优化,单纯分离读写而不优化查询,性能依旧差,务必为 Query 侧设计专用 DTO,避免
SELECT *。 - 坑 3:命令执行的幂等性,网络超时可能导致客户端重试,同一命令可能被执行两次,建议在 Command 中增加唯一请求 ID,并在 Handler 中判断是否已处理。
优化建议:
- 使用 PHP 8.1+ readonly 属性 确保 Command 不可变。
- 为 Command Bus 添加 中间件(Middleware),用于记录日志、开启数据库事务、执行权限校验。
- 对高频 Query 使用 长时缓存,并设置合理的 TTL。
Q&A 高频问题解答
问:CQRS 和事件溯源(Event Sourcing)是一回事吗? 答:不是,CQRS 关注读写分离,事件溯源关注如何存储状态(通过事件序列),两者常结合使用,但可以独立实施。
问:在 PHP 中,CQRS 适合与哪些框架搭配?
答:Laravel 的 laravel-query-bus 包或 Symfony Messenger 都是不错的选择,Laravel 惯例推荐使用 Action 类,但 CQRS 更强调 CommandBus 的显式分发。
问:如果读模型和写模型的数据不一致怎么办? 答:这是分布式的经典难题,建议:
- 主库写入后,立即将变更发送到消息队列,从库异步消费更新。
- 为读接口设置较短缓存时间(如 5 秒),容忍短暂延迟。
- 对强一致性的操作(如订单支付后查询)可走
READ_UNCOMMITTED直接查主库。
问:复杂的 JOIN 查询在 CQRS 中如何处理? 答:不要在 Command 侧做复杂 JOIN,在 Query 侧可以创建冗余的读模型表(如订单统计表),或者使用 ES 进行聚合搜索。
CQRS 不是银弹,但它为 PHP 应用提供了清晰的业务边界和高性能潜力,通过本文示例,你可以快速在自己的项目中落地这一模式,从简单开始,逐步演进,避免一开始就引入复杂的分布式事务。