CQRS(Command Query Responsibility Segregation)是一种将读写操作分离的架构模式。在PHP项目中采用这种模式,能够显著提升复杂业务系统的可维护性和性能表现。我曾在多个电商项目中实践过这种架构,今天就来分享下具体的实现方案和踩过的坑。
在传统CRUD架构中,我们通常使用同一个模型处理读写操作。这种设计在小项目中很实用,但当业务复杂度增加时就会暴露问题:
php复制// 传统方式示例
class OrderController {
public function create(Request $request) {
// 同一个模型处理创建和查询
$order = new Order();
$order->create($request->all());
return $order->find($order->id);
}
}
CQRS架构将系统明确划分为两个独立部分:
php复制interface CommandBus {
public function dispatch(object $command): void;
}
class SimpleCommandBus implements CommandBus {
private array $handlers = [];
public function register(string $commandClass, callable $handler): void {
$this->handlers[$commandClass] = $handler;
}
public function dispatch(object $command): void {
$handler = $this->handlers[get_class($command)] ?? null;
if (!$handler) {
throw new \RuntimeException("No handler registered for ".get_class($command));
}
$handler($command);
}
}
php复制interface QueryBus {
public function ask(object $query): mixed;
}
class SimpleQueryBus implements QueryBus {
private array $handlers = [];
public function register(string $queryClass, callable $handler): void {
$this->handlers[$queryClass] = $handler;
}
public function ask(object $query): mixed {
$handler = $this->handlers[get_class($query)] ?? null;
if (!$handler) {
throw new \RuntimeException("No handler registered for ".get_class($query));
}
return $handler($query);
}
}
php复制class Order {
private array $events = [];
private array $items = [];
public static function create(string $customerId): self {
$order = new self();
$order->record(new OrderCreated(Uuid::uuid4(), $customerId));
return $order;
}
public function addItem(OrderItem $item): void {
$this->items[] = $item;
$this->record(new ItemAddedToOrder(
$this->id,
$item->productId,
$item->quantity,
$item->price
));
}
private function record(object $event): void {
$this->events[] = $event;
}
public function releaseEvents(): array {
$events = $this->events;
$this->events = [];
return $events;
}
}
php复制class CreateOrderHandler {
public function __construct(
private OrderRepository $orderRepo,
private EventBus $eventBus
) {}
public function handle(CreateOrderCommand $command): void {
// 验证业务规则
if (count($command->items) === 0) {
throw new \InvalidArgumentException("Order must have at least one item");
}
$order = Order::create($command->customerId);
foreach ($command->items as $item) {
$order->addItem(OrderItem::create(
$item['product_id'],
$item['product_name'],
Money::fromFloat($item['price']),
$item['quantity']
));
}
$this->orderRepo->save($order);
// 发布领域事件
foreach ($order->releaseEvents() as $event) {
$this->eventBus->dispatch($event);
}
}
}
重要提示:命令处理器中应该只包含业务逻辑,不要混入基础设施代码(如直接操作数据库)
读模型应该针对具体查询场景进行优化:
sql复制CREATE TABLE order_read_model (
order_id VARCHAR(36) PRIMARY KEY,
customer_id VARCHAR(36) NOT NULL,
status VARCHAR(20) NOT NULL,
total_amount DECIMAL(10,2) NOT NULL,
created_at DATETIME NOT NULL,
INDEX idx_customer (customer_id),
INDEX idx_created (created_at)
);
CREATE TABLE order_item_read_model (
id INT AUTO_INCREMENT PRIMARY KEY,
order_id VARCHAR(36) NOT NULL,
product_id VARCHAR(36) NOT NULL,
product_name VARCHAR(255) NOT NULL,
quantity INT NOT NULL,
unit_price DECIMAL(10,2) NOT NULL,
INDEX idx_order (order_id),
INDEX idx_product (product_id)
);
php复制class GetOrderDetailsHandler {
public function __construct(private \PDO $readDb) {}
public function handle(GetOrderDetailsQuery $query): array {
$stmt = $this->readDb->prepare(
'SELECT o.*,
JSON_ARRAYAGG(
JSON_OBJECT(
"product_name", oi.product_name,
"quantity", oi.quantity,
"unit_price", oi.unit_price
)
) as items
FROM order_read_model o
LEFT JOIN order_item_read_model oi ON o.order_id = oi.order_id
WHERE o.order_id = ?
GROUP BY o.order_id'
);
$stmt->execute([$query->orderId]);
$result = $stmt->fetch(\PDO::FETCH_ASSOC);
if (!$result) {
throw new \RuntimeException("Order not found");
}
$result['items'] = json_decode($result['items'], true);
return $result;
}
}
php复制class OrderCreatedEventListener {
public function __construct(private \PDO $readDb) {}
public function handle(OrderCreatedEvent $event): void {
$this->readDb->beginTransaction();
try {
// 同步订单主表
$stmt = $this->readDb->prepare(
'INSERT INTO order_read_model
(order_id, customer_id, status, total_amount, created_at)
VALUES (?, ?, ?, ?, ?)'
);
$stmt->execute([
$event->orderId,
$event->customerId,
'pending',
0.00,
$event->occurredOn->format('Y-m-d H:i:s')
]);
$this->readDb->commit();
} catch (\Exception $e) {
$this->readDb->rollBack();
throw $e;
}
}
}
在实际项目中,我们需要考虑以下几种同步策略:
同步处理:在命令处理器中直接更新读模型
异步事件处理:
php复制class EventBus {
private array $listeners = [];
public function subscribe(string $eventClass, callable $listener): void {
$this->listeners[$eventClass][] = $listener;
}
public function dispatch(object $event): void {
foreach ($this->listeners[get_class($event)] ?? [] as $listener) {
// 可以放入消息队列异步处理
$listener($event);
}
}
}
变更数据捕获(CDC):使用数据库binlog或触发器捕获变更
php复制class CachedOrderDetailsHandler implements QueryHandler {
public function __construct(
private QueryHandler $innerHandler,
private CacheInterface $cache
) {}
public function handle(GetOrderDetailsQuery $query): array {
$cacheKey = "order_details_{$query->orderId}";
if ($this->cache->has($cacheKey)) {
return $this->cache->get($cacheKey);
}
$result = $this->innerHandler->handle($query);
$this->cache->set($cacheKey, $result, 3600); // 缓存1小时
return $result;
}
}
对于不要求实时一致性的场景,可以使用延迟队列:
php复制class UpdateReadModelJob {
public function __construct(private object $event) {}
public function handle(): void {
// 这里实现具体的读模型更新逻辑
sleep(5); // 故意延迟5秒
$listener = new OrderCreatedEventListener($this->getReadDb());
$listener->handle($this->event);
}
}
// 在事件总线中
$eventBus->subscribe(OrderCreatedEvent::class, function($event) {
dispatch(new UpdateReadModelJob($event));
});
在CQRS中,跨聚合的事务需要特别注意:
php复制class PlaceOrderHandler {
public function handle(PlaceOrderCommand $command): void {
$this->entityManager->beginTransaction();
try {
// 1. 扣减库存
foreach ($command->items as $item) {
$product = $this->productRepository->find($item['product_id']);
$product->reduceStock($item['quantity']);
$this->productRepository->save($product);
}
// 2. 创建订单
$order = Order::create($command->customerId);
// ...添加订单项...
$this->orderRepository->save($order);
$this->entityManager->commit();
} catch (\Exception $e) {
$this->entityManager->rollBack();
throw $e;
}
}
}
重要提示:尽量避免跨聚合的事务,可以通过Saga模式实现最终一致性
当领域事件结构需要变更时:
php复制class OrderCreatedEvent {
public function __construct(
public string $eventId,
public string $orderId,
public string $customerId,
public DateTimeImmutable $occurredOn,
public int $version = 1
) {}
public static function fromArray(array $data): self {
// 根据version字段决定如何反序列化
if ($data['version'] === 1) {
return new self(
$data['eventId'],
$data['orderId'],
$data['customerId'],
new DateTimeImmutable($data['occurredOn']),
$data['version']
);
}
// 处理其他版本...
}
}
CQRS系统的调试比传统架构更复杂,建议:
为每个命令和查询添加唯一追踪ID
php复制class TraceableCommandBus implements CommandBus {
public function dispatch(object $command): void {
$traceId = Uuid::uuid4();
// 记录日志或发送到APM系统
$this->innerBus->dispatch($command);
}
}
实现事件溯源(Event Sourcing)以便重现问题
php复制class EventStore {
public function append(string $aggregateId, array $events): void {
foreach ($events as $event) {
$this->db->insert('event_store', [
'event_id' => Uuid::uuid4(),
'aggregate_id' => $aggregateId,
'event_type' => get_class($event),
'payload' => json_encode($event),
'occurred_at' => (new DateTimeImmutable())->format('Y-m-d H:i:s')
]);
}
}
}
CQRS不是银弹,最适合以下场景:
读写负载差异大的系统
复杂业务逻辑的领域
需要不同数据视图的应用
不适合的场景:
php复制class OrderAggregate {
private array $events = [];
public static function reconstitute(array $events): self {
$aggregate = new self();
foreach ($events as $event) {
$aggregate->apply($event);
}
return $aggregate;
}
public function create(string $customerId): void {
$this->record(new OrderCreated(Uuid::uuid4(), $customerId));
}
private function apply(OrderCreated $event): void {
$this->id = $event->orderId;
$this->customerId = $event->customerId;
$this->status = 'pending';
}
}
在微服务架构中,CQRS可以这样应用:
php复制class OrderQueryService {
public function __construct(
private \PDO $readDb,
private HttpClient $userServiceClient
) {}
public function getOrderWithUserDetails(string $orderId): array {
$order = $this->getOrderDetails($orderId);
$user = $this->userServiceClient->get("/users/{$order['customer_id']}");
return [
'order' => $order,
'user' => $user
];
}
}
框架集成:
消息总线:
事件溯源:
CQRS框架:
测试工具:
php复制// 使用Prooph实现CQRS的示例
use Prooph\ServiceBus\CommandBus;
$commandBus = new CommandBus();
$commandBus->attach(
CommandBus::PRIORITY_NORMAL,
function (Command $command): void {
// 命令处理逻辑
}
);
在实现CQRS时,我建议先从简单的自定义实现开始,等真正理解了模式的核心思想后,再考虑使用框架。过早引入复杂框架反而会增加学习成本。