PHP 怎么CQRS

wen PHP项目 3

PHP 架构进化论:从混乱到有序,一文读懂 CQRS 模式实战指南**

PHP 怎么CQRS


📚 目录导读

  1. 为什么你的 PHP 项目越来越“笨重”? —— 传统 CRUD 的痛点剖析
  2. CQRS 是什么? —— 打破“读写不分家”的魔法咒语
  3. PHP 实现 CQRS 的核心组件 —— Command Bus 与 Query Bus 的协奏曲
  4. 实战演练:手写一个轻量级 PHP CQRS 示例(无框架依赖)
  5. CQRS 的黄金搭档:Event Sourcing(事件溯源)浅析
  6. PHP 生态中的 CQRS 武器库 —— Laravel 与 Symfony 解决方案
  7. 高频问答:解开你对 CQRS 的最后疑虑
  8. 何时该用,何时该弃?

为什么你的 PHP 项目越来越“笨重”?

在传统的 PHP 业务系统中,我们习惯将数据的读取(查询)和写入(命令)糅合在同一个 Service 或 Model 中,一个 UserService 既要有 getUserProfile()(读),又要有 updateUserProfile()(写),这种 CRUD(增删改查)模式在业务简单时效率极高,但当系统演进到复杂领域(如电商订单、金融交易)时,问题开始暴露:

  • 性能瓶颈:高频的读操作(如列表展示)和低频的写操作(如状态变更)共享同一数据模型和数据库连接池,导致资源争抢。
  • 锁竞争:写操作需要加行锁保证一致性,但频繁的读查询会被阻塞,反之亦然。
  • 逻辑膨胀:读模型往往为了界面适配需要 JOIN 十几张表,而写模型只需要关注单表变化,两者耦合在一个方法里,代码变得难以维护。

这就好比一家餐厅,既要做堂食(读),又要做外卖(写),却共用同一个厨房和同一批厨师,高峰期必然混乱。

CQRS 是什么?

CQRS(Command Query Responsibility Segregation,命令查询职责分离) 是一种架构模式,其核心理念非常朴素:将“修改状态的命令”和“读取数据的查询”完全分离

  • Command(命令):意图是“执行一个动作”,如 PlaceOrderUpdateStock,它不返回业务数据,只表示“我干了这个事”,结果是改变状态。
  • Query(查询):意图是“获取数据”,如 GetOrderByIdSearchProducts,它不改变状态,只返回只读的 DTO(数据传输对象)。

在 PHP 中实施 CQRS,意味着你的服务层不再是一个“万能类”,而是拆分为 Command Bus(命令总线)和 Query Bus(查询总线)。

PHP 实现 CQRS 的核心组件

要落地 CQRS,你至少需要三个关键角色:

  1. Command/Query 对象:一个简单的 PHP 类,用于承载数据,如 class CreateUserCommand { public $name; public $email; }
  2. Handler(处理器):专门处理“做什么”的逻辑,如 class CreateUserHandler { public function handle(CreateUserCommand $cmd) { /* 写库逻辑 */ } }
  3. Bus(总线):负责将 Command 分发到对应的 Handler,它像一个调度中心,你无需在 Controller 里手动 new Handler,而是丢给 Bus 去处理。

重要区分:写操作的 Handler 不返回任何值,或者只返回一个状态码;读操作的 Handler 必须返回数组或对象。

实战演练:手写一个轻量级 PHP CQRS 示例

我们抛开框架,用纯 PHP 演示核心思路(假设环境 PHP 8.1+)。

<?php
// 1. 定义查询对象
class GetUserQuery {
    public function __construct(
        public readonly int $userId
    ) {}
}
// 2. 定义查询处理器
class GetUserQueryHandler {
    public function __invoke(GetUserQuery $query): array {
        // 模拟从专用的读库(可以是缓存或副本)查询
        return [
            'id' => $query->userId,
            'name' => 'Alice',
            'created_at' => '2024-01-01'
        ];
    }
}
// 3. 定义一个极简的查询总线(容器)
class SimpleQueryBus {
    public function dispatch(object $query): mixed {
        // 约定Handler类名:类名 + 'Handler'
        $handlerClass = get_class($query) . 'Handler';
        $handler = new $handlerClass();
        return $handler($query);
    }
}
// 4. 使用
$bus = new SimpleQueryBus();
$result = $bus->dispatch(new GetUserQuery(1));
var_dump($result); // 输出数组

同样的逻辑,对于 Command 侧,dispatch 方法则不需要 return,而是执行 $handler->handle($command) 后静默结束。

注意:真实项目中,你会使用依赖注入容器(如 PHP-DI)来解析 Handler 的依赖,而绝不仅仅是用new来创建 Handler,否则你只是做了类名拆分,并没有解耦。

CQRS 的黄金搭档:Event Sourcing(事件溯源)

单独的 CQRS 只是解决了读写不平衡的问题,但它并没有解决数据一致性来源的问题,当你把读库和写库分开时,写库产生的变更如何同步到读库?

Event Sourcing(事件溯源) 登场,它不是保存“当前状态”,而是保存“状态变化的事件流”,不用保存“订单已发货”,而是保存一个事件 OrderShipped,当需要读数据时,通过重放这些事件来构建读模型。

  • 优势:完美的审计日志,可回溯任意时间点的状态。
  • 代价:事件存储复杂,事件版本管理困难,学习曲线陡峭。

在 PHP 项目中,我强烈的建议是:先上 CQRS 解决读写性能问题,暂不引入 Event Sourcing,只有当你的业务有强审计需求或复杂状态流转时,再考虑引入事件溯源。

PHP 生态中的 CQRS 武器库

如果你不想从零造轮子,以下两个主流方案值得关注:

  • 针对 Laravel:推荐 laravel-actions 配合队列使用,或者使用更严谨的 ecotone/ecotone 框架,尤其 ecotone 提供了完整的 CQRS + Event Sourcing + 消息队列支持,直接与 Symfony Messenger 或 Laravel Queue 集成。
  • 针对 Symfony:官方推荐的 symfony/messenger 组件,它是业界教科书级别的实现,支持异步 Command 处理、中间件队列、事务包装。

实战技巧:利用消息队列(如 RabbitMQ)作为 Command Bus 的底层传输,可以将写操作异步化,极大提升系统吞吐量。

高频问答:解开你对 CQRS 的最后疑虑

Q1:CQRS 是不是一定要把读库和写库物理分开(不同的数据库)? A: 不一定。逻辑分离才是核心,你可以只写一套代码,但内部使用不同的 Model 和 Repository 去访问同一个 MySQL 实例的不同表(读表和写表),只有遇到大数据量或高并发时,才物理拆分到 Elasticsearch 或 Redis 副本中。

Q2:使用 CQRS 后,代码量岂不是翻倍了? A: 是的,会有很多 CreateXCommandCreateXHandlerGetXQuery 这样的类。这是一种刻意的“冗余”,换取的是未来的可维护性和扩展性,对于小项目,这种代价是负资产;对于复杂项目,这是必要的投资。

Q3:在 PHP 中,如何保证 Command 的幂等性? A: 这是核心难点,建议在 Command Bus 处理前,生成一个全局唯一 ID(UUID),并将该 ID 存储在数据库的 processed_messages 表中做唯一约束,如果重复消费同一个Command,数据库会拒绝插入,从而避免重复扣款或重复下单。

Q4:CQRS 能提升安全性吗? A: 间接提升,因为读写模型分离后,你可以对 Query 层做严格的字段过滤,只暴露需要的 DTO,避免将 password_hash 这样的字段意外传给前端,Command 层可以进行更严格的权限校验。

何时该用,何时该弃?

不建议使用 CQRS 的场景

  • 简单的后台管理(增删改查报表)。
  • 团队人数少于 5 人,且对 DDD 不熟悉。
  • 没有专职的架构师持续维护 Bus 层。

强烈建议使用 CQRS 的场景

  • 数据读取量和写入量差距巨大(典型:读写比 > 10:1)。
  • 团队在多个子域协作(如订单域与库存域)。
  • 系统未来必然要拆分为微服务。

最后一句忠告:CQRS 不是银弹,它提升的是架构优雅度,牺牲的是实现复杂度,在 PHP 项目里,请先从战术层面(命令处理器 + 查询模型)入手,而不是直接上战略层面的全套事件溯源,从今天起,试着在你的 Service 里加上后缀 CommandQuery,这就是你迈向 CQRS 的第一步。

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