PHP 怎么PHP CQRS 示例

wen PHP项目 2

PHP CQRS 架构实战:从理论到代码的完整示例解析


目录导读

  1. 什么是 CQRS?为什么 PHP 项目需要它?
  2. CQRS 的核心概念:Command 与 Query 的彻底分离
  3. PHP 实现 CQRS 的基础环境搭建
  4. PHP CQRS 实战示例:构建一个订单管理系统
    • 1 定义 Command 与 Command Handler
    • 2 定义 Query 与 Query Handler
    • 3 命令总线(Command Bus)与查询总线(Query Bus)的实现
  5. CQRS 与数据库读写分离的整合策略
  6. 常见坑与性能优化建议
  7. Q&A 高频问题解答

什么是 CQRS?为什么 PHP 项目需要它?

CQRS(Command Query Responsibility Segregation,命令查询职责分离)是一种将系统的“写操作”(Command,命令)和“读操作”(Query,查询)彻底分离的架构模式,在传统 MVC 中,我们通常使用同一个模型(Model)来处理数据的增删改查,但一旦业务逻辑复杂化,这种“一刀切”的方式会导致代码臃肿、数据库锁竞争激烈、查询性能低下。

PHP 怎么PHP CQRS 示例

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 应用提供了清晰的业务边界和高性能潜力,通过本文示例,你可以快速在自己的项目中落地这一模式,从简单开始,逐步演进,避免一开始就引入复杂的分布式事务。

抱歉,评论功能暂时关闭!