1. 为什么我们需要CQRS模式?
在传统的CRUD架构中,我们通常使用同一个数据模型来处理读写操作。这种看似简单直接的方式,随着业务复杂度提升会暴露出诸多问题。我经历过一个电商系统,在促销活动时查询订单状态的API响应时间从200ms飙升到2s以上,而写入操作也因此受到严重影响。
CQRS(Command Query Responsibility Segregation)的核心思想是将读写操作分离为两个独立的模型:
- 命令(Command):负责创建、更新和删除操作,改变系统状态
- 查询(Query):只负责读取数据,不改变系统状态
这种分离带来的直接好处是:
- 读写负载可以独立扩展
- 查询模型可以根据展示需求专门优化
- 命令模型可以专注于业务规则和验证
- 两个模型可以分别使用最适合的技术实现
2. Hyperf框架下的CQRS实现方案
2.1 基础架构设计
在Hyperf中实现CQRS,我们需要建立清晰的边界。以下是一个典型的分层结构:
code复制app/
├── Command/
│ ├── Application/
│ ├── Domain/
│ └── Infrastructure/
├── Query/
│ ├── Application/
│ └── Infrastructure/
└── Shared/
├── DTO/
└── Events/
命令侧采用典型的领域驱动设计分层:
- Application层:处理命令,协调领域对象
- Domain层:包含业务逻辑和规则
- Infrastructure层:实现持久化等基础设施
查询侧则相对简单:
- Application层:处理查询请求
- Infrastructure层:实现数据获取逻辑
2.2 命令实现细节
让我们以一个用户注册命令为例:
php复制// app/Command/Application/User/RegisterUserHandler.php
class RegisterUserHandler
{
public function __construct(
private UserRepository $repository,
private EventDispatcher $dispatcher
) {}
public function __invoke(RegisterUserCommand $command)
{
$user = User::register(
$command->username,
$command->email,
$command->password
);
$this->repository->save($user);
$this->dispatcher->dispatch(
new UserRegistered($user->getId())
);
}
}
对应的命令对象:
php复制// app/Command/Application/User/RegisterUserCommand.php
class RegisterUserCommand
{
public function __construct(
public readonly string $username,
public readonly string $email,
public readonly string $password
) {}
}
2.3 查询实现方案
查询侧可以使用更灵活的数据获取方式。对于性能要求高的查询,我们可以绕过领域模型直接访问数据:
php复制// app/Query/Application/User/GetUserProfileHandler.php
class GetUserProfileHandler
{
public function __construct(
private UserProfileQuery $query
) {}
public function __invoke(GetUserProfileQuery $query): UserProfileDto
{
return $this->query->execute($query->userId);
}
}
查询服务可以直接使用原生SQL或更高效的查询方式:
php复制// app/Query/Infrastructure/User/DoctrineUserProfileQuery.php
class DoctrineUserProfileQuery implements UserProfileQuery
{
public function execute(string $userId): UserProfileDto
{
$sql = "SELECT u.username, p.avatar, p.bio
FROM users u
LEFT JOIN profiles p ON u.id = p.user_id
WHERE u.id = :userId";
$stmt = $this->connection->prepare($sql);
$stmt->execute(['userId' => $userId]);
return new UserProfileDto(...$stmt->fetch());
}
}
3. Hyperf特有功能在CQRS中的应用
3.1 利用AOP实现横切关注点
Hyperf强大的AOP功能可以帮助我们优雅地处理CQRS中的公共逻辑。例如,我们可以为所有命令处理器添加事务支持:
php复制// app/Command/Infrastructure/Aspect/TransactionalAspect.php
#[Aspect]
class TransactionalAspect
{
#[Inject]
private StdoutLoggerInterface $logger;
#[Around("execution(app\Command\Application\*Handler->__invoke())")]
public function process(ProceedingJoinPoint $proceedingJoinPoint)
{
return Context::run(function () use ($proceedingJoinPoint) {
$this->logger->info("Starting transaction for command");
try {
$result = $proceedingJoinPoint->process();
$this->logger->info("Command executed successfully");
return $result;
} catch (\Throwable $e) {
$this->logger->error("Command failed: ".$e->getMessage());
throw $e;
}
});
}
}
3.2 进程管理与worker重启策略
在CQRS架构中,我们可以针对不同操作类型配置不同的worker进程。例如,在hyperf.php配置文件中:
php复制return [
'settings' => [
'command_workers' => [
'handler' => CommandWorker::class,
'count' => cpu_count() * 2,
'options' => [
'max_requests' => 100000
]
],
'query_workers' => [
'handler' => QueryWorker::class,
'count' => cpu_count() * 4,
'options' => [
'max_requests' => 50000
]
]
]
];
这种配置允许:
- 命令worker数量较少但更稳定(处理复杂业务逻辑)
- 查询worker数量较多但重启更频繁(处理大量简单请求)
4. 性能优化与实战技巧
4.1 查询侧的性能优化
在实现CQRS时,查询侧的优化空间最大。以下是一些实测有效的技巧:
- 专门优化的读模型:
php复制// 专门为首页设计的读模型
class HomePageProjection
{
public function getRecentActivities(int $limit): array
{
return $this->connection->createQueryBuilder()
->select('a.*', 'u.username')
->from('activities', 'a')
->join('a', 'users', 'u', 'a.user_id = u.id')
->orderBy('a.created_at', 'DESC')
->setMaxResults($limit)
->execute()
->fetchAll();
}
}
- 使用Redis缓存热门查询:
php复制#[Inject]
private RedisProxy $redis;
public function getDashboardStats(): array
{
$cacheKey = 'dashboard_stats';
if ($this->redis->exists($cacheKey)) {
return json_decode($this->redis->get($cacheKey), true);
}
$stats = $this->calculateDashboardStats();
$this->redis->set($cacheKey, json_encode($stats), 'EX', 300);
return $stats;
}
- 实现分页查询的最佳实践:
php复制public function paginateProducts(int $page, int $perPage): Paginator
{
$query = $this->connection->createQueryBuilder()
->select('p.*', 'c.name as category_name')
->from('products', 'p')
->join('p', 'categories', 'c', 'p.category_id = c.id')
->where('p.is_active = 1')
->orderBy('p.created_at', 'DESC');
return new Paginator($query, $page, $perPage);
}
4.2 命令侧的可靠性保障
命令处理需要更强的可靠性和一致性保障:
- 实现幂等命令处理器:
php复制class UpdateUserEmailHandler
{
#[Inject]
private IdempotencyChecker $checker;
public function __invoke(UpdateUserEmailCommand $command)
{
if ($this->checker->isProcessed($command->idempotencyKey)) {
return;
}
// 处理逻辑
$this->checker->markAsProcessed($command->idempotencyKey);
}
}
- 使用事件溯源增强可追溯性:
php复制class OrderCommandHandler
{
public function __invoke(CreateOrderCommand $command)
{
$events = [
new OrderCreated($command->orderId, $command->userId),
new OrderItemsAdded($command->orderId, $command->items),
new OrderStatusChanged($command->orderId, 'pending')
];
foreach ($events as $event) {
$this->eventStore->append($event);
$this->dispatcher->dispatch($event);
}
}
}
- 实现补偿事务模式:
php复制class PaymentProcessingHandler
{
public function __invoke(ProcessPaymentCommand $command)
{
try {
$this->paymentGateway->charge($command->amount, $command->token);
$this->orderService->markAsPaid($command->orderId);
} catch (PaymentFailedException $e) {
$this->orderService->cancel($command->orderId);
throw $e;
}
}
}
5. 监控与维护策略
5.1 关键指标监控
在CQRS架构中,我们需要监控的关键指标包括:
- 命令处理延迟:
php复制// 在命令处理器中添加监控
$this->statsd->timing(
'command.'.$commandName.'.duration',
$durationInMs
);
- 查询响应时间:
php复制// 在查询处理器中添加监控
$this->statsd->timing(
'query.'.$queryName.'.duration',
$durationInMs
);
- 事件处理延迟:
php复制// 在事件处理器中添加监控
$this->statsd->timing(
'event.'.$eventName.'.processing_time',
$processingTimeMs
);
5.2 日志记录策略
合理的日志记录对于问题排查至关重要:
- 结构化日志记录:
php复制$this->logger->info('Command executed', [
'command' => get_class($command),
'params' => $command->toArray(),
'duration' => $duration,
'user_id' => $currentUserId
]);
- 关键操作审计日志:
php复制class AuditLogListener
{
public function process(object $event)
{
$this->auditLogRepository->log(
get_class($event),
$event->aggregateId(),
$this->getCurrentUserId(),
$event->occurredOn(),
$event->toArray()
);
}
}
- 异常集中处理:
php复制class ExceptionListener
{
public function process(Throwable $exception)
{
$this->logger->error($exception->getMessage(), [
'exception' => $exception,
'trace' => $exception->getTraceAsString()
]);
$this->errorTracking->captureException($exception);
}
}
6. 常见问题与解决方案
6.1 数据一致性问题
CQRS架构中最大的挑战是保持读写两端的数据一致性。以下是几种解决方案:
- 最终一致性模式:
php复制// 事件处理器中更新读模型
class OrderPlacedListener
{
public function __invoke(OrderPlaced $event)
{
$this->orderProjection->updateOrderStatus(
$event->orderId,
'placed'
);
}
}
- 定期同步机制:
php复制// 定时任务同步命令和查询模型
class SyncReadModelCommand
{
public function __invoke()
{
$unsyncedEvents = $this->eventStore->getUnprocessedEvents();
foreach ($unsyncedEvents as $event) {
$this->projector->project($event);
$this->eventStore->markAsProcessed($event->getId());
}
}
}
- 使用物化视图:
sql复制CREATE MATERIALIZED VIEW user_orders_view AS
SELECT u.id as user_id, u.username, o.id as order_id, o.total_amount
FROM users u
JOIN orders o ON u.id = o.user_id
WHERE o.status = 'completed'
WITH DATA;
REFRESH MATERIALIZED VIEW user_orders_view;
6.2 性能调优经验
经过多个项目实践,我总结了以下性能调优要点:
- 命令批处理:
php复制class BatchCommandHandler
{
public function __invoke(BatchCommand $command)
{
$this->connection->beginTransaction();
try {
foreach ($command->getCommands() as $subCommand) {
$this->commandBus->dispatch($subCommand);
}
$this->connection->commit();
} catch (\Throwable $e) {
$this->connection->rollBack();
throw $e;
}
}
}
- 查询结果缓存:
php复制class CachedUserQuery implements UserQuery
{
public function findById(string $userId): ?UserDto
{
$cacheKey = "user_{$userId}";
return $this->cache->remember($cacheKey, 3600, function() use ($userId) {
return $this->innerQuery->findById($userId);
});
}
}
- 连接池优化:
php复制// database.php配置
return [
'pool' => [
'min_connections' => 1,
'max_connections' => 100,
'connect_timeout' => 10.0,
'wait_timeout' => 3.0,
'heartbeat' => -1,
'max_idle_time' => 60.0,
]
];
7. 项目演进与扩展思路
7.1 从单体到微服务的演进
当系统规模扩大时,CQRS架构可以平滑演进为微服务架构:
- 服务拆分策略:
php复制// 将用户相关命令和查询拆分为独立服务
class UserCommandService
{
public function register(RegisterUserCommand $command)
{
// 处理注册逻辑
}
}
class UserQueryService
{
public function getUserProfile(GetUserProfileQuery $query)
{
// 处理查询逻辑
}
}
- 事件驱动的服务通信:
php复制class OrderCreatedListener
{
public function __invoke(OrderCreated $event)
{
$this->notificationService->sendOrderConfirmation(
$event->orderId,
$event->userId
);
$this->inventoryService->reserveItems(
$event->orderId,
$event->items
);
}
}
- API网关整合:
php复制class ApiGatewayController
{
public function getUserProfile()
{
$query = new GetUserProfileQuery(
$this->request->input('user_id')
);
return $this->queryBus->dispatch($query);
}
public function updateProfile()
{
$command = new UpdateProfileCommand(
$this->request->input('user_id'),
$this->request->input('profile_data')
);
$this->commandBus->dispatch($command);
return response()->json(['success' => true]);
}
}
7.2 前端适配CQRS架构
前端架构也需要相应调整以适应后端CQRS模式:
- 命令和查询分离的API设计:
javascript复制// 命令API
const response = await fetch('/api/commands/register-user', {
method: 'POST',
body: JSON.stringify(command)
});
// 查询API
const response = await fetch('/api/queries/user-profile?userId=123');
- 乐观更新模式:
javascript复制// 在前端先更新UI,然后发送命令
function updateProfile(profileData) {
// 乐观更新
setUserProfile(profileData);
// 发送命令
api.sendCommand('update-profile', {
userId: currentUser.id,
profileData
}).catch(error => {
// 回滚UI更新
revertProfileUpdate();
});
}
- 事件订阅与实时更新:
javascript复制// 建立WebSocket连接订阅用户相关事件
const socket = new WebSocket('/api/events');
socket.onmessage = (event) => {
const data = JSON.parse(event.data);
switch(data.type) {
case 'OrderStatusUpdated':
updateOrderStatus(data.payload.orderId, data.payload.status);
break;
// 处理其他事件类型
}
};
