传统 CRUD 的瓶颈
当你构建一个标准的 Laravel 应用时,你通常会遵循 CRUD(创建、读取、更新、删除)范式。你有一个 User 模型和一个 UserController。这个控制器既处理写操作(创建用户),也处理读操作(获取用户列表)。在底层,读写操作都与同一个 MySQL 或 PostgreSQL 数据库表进行交互。
对于大多数应用来说,这样做完全没有问题。但在高流量的企业系统中,工作负载很少是对称的。在报表仪表盘或电商平台中,你的应用可能每执行 1 次“写”查询,就要执行 1000 次“读”查询。如果你对读写操作都使用完全相同的数据库和相同的 Eloquent 模型,那么繁重的写操作(会锁定行并要求事务完整性)就会开始阻塞飞快的读操作,导致应用性能骤降。
在 Smart Tech Devs,当我们为需要大规模可扩展性的平台设计架构时,我们会实现 CQRS(命令查询职责分离)。CQRS 是一种架构模式,它将修改数据(命令)与读取数据(查询)严格分离。
CQRS 的理念
通过分离命令和查询,我们可以独立优化它们。
- 命令(写操作):负责处理复杂的业务验证、领域逻辑和事务完整性。它们不返回数据(仅返回成功确认)。
- 查询(读操作):完全绕过复杂的领域逻辑,只是尽可能快地获取数据,通常来自高度优化的反规范化读副本或 Redis 等缓存层。
第一阶段:构建命令栈
让我们设计一个订单处理系统。当用户下单时,这是一个涉及库存检查、支付处理和状态变更的复杂写操作。我们将整个流程封装在命令对象和命令处理器中。
namespace App\CQRS\Commands;
// 1. 命令:一个简单的数据传输对象(DTO)
class PlaceOrderCommand
{
public function __construct(
public readonly string $userId,
public readonly array $items,
public readonly string $paymentToken
) {}
}
接下来,我们构建处理器。该类执行业务逻辑。注意它不会返回创建的 Order 对象;它只是执行变更操作。
namespace App\CQRS\Handlers;
use App\CQRS\Commands\PlaceOrderCommand;
use App\Models\Order;
use Illuminate\Support\Facades\DB;
class PlaceOrderHandler
{
public function handle(PlaceOrderCommand $command): void
{
DB::transaction(function () use ($command) {
// 1. 验证库存(领域逻辑)
// 2. 通过 Stripe 处理支付
// 3. 持久化到“写”数据库
$order = Order::create([
'user_id' => $command->userId,
'status' => 'processing',
'total' => calculateTotal($command->items)
]);
// 4. 触发事件以便稍后同步读数据库
OrderPlaced::dispatch($order);
});
}
}
第二阶段:构建查询栈
现在,我们来看读取端。如果用户想要查看订单历史,我们不需要复杂的领域逻辑,甚至不需要 Eloquent 的对象水化开销。我们只需要原始速度。我们创建查询对象来定义请求,并创建查询处理器来获取数据。
namespace App\CQRS\Queries;
class GetUserOrdersQuery
{
public function __construct(public readonly string $userId) {}
}
namespace App\CQRS\Handlers;
use App\CQRS\Queries\GetUserOrdersQuery;
use Illuminate\Support\Facades\DB;
class GetUserOrdersHandler
{
public function handle(GetUserOrdersQuery $query): array
{
// 1. 为了获得最大读取速度,我们完全绕过 Eloquent。
// 2. 我们从优化的“读”数据库连接进行查询。
return DB::connection('mysql_read')
->table('user_order_views')
->where('user_id', $query->userId)
->orderBy('created_at', 'desc')
->get()
->toArray();
}
}
第三阶段:物理数据库分离
CQRS 的真正威力在于物理分离数据库。在 Laravel 中,你可以在 config/database.php 文件中配置主/从连接。你将数据写入主数据库,从从副本读取数据。
'mysql' => [
'read' => [
'host' => ['192.168.1.2', '192.168.1.3'], // 读副本
],
'write' => [
'host' => ['192.168.1.1'], // 主库
],
'driver' => 'mysql',
'database' => env('DB_DATABASE'),
'username' => env('DB_USERNAME'),
'password' => env('DB_PASSWORD'),
// ...
],
当订单被创建(命令)时,它会写入写数据库。我们之前派发的 OrderPlaced 事件会触发异步后台任务。该任务会更新读副本上高度反规范化的 user_order_views 表。这被称为 最终一致性。读数据库可能比写数据库慢几毫秒,但作为交换,我们可以在不锁定主表的情况下处理数万并发读请求。
工程回报
在 Laravel 中采用 CQRS 并不适用于简单的博客或内部工具;它会引入不可否认的架构复杂性。然而,对于处理高吞吐量报表、大量用户并发或复杂领域规则的企业系统而言,这将带来巨大改变。通过解耦读写操作,你可以不对称地扩展数据库基础设施——可以启动十台廉价的读副本服务器,同时仅维护一台强大的主写服务器。此外,你的代码库也会变得高度组织化;优化复杂报表查询的开发者永远不会意外破坏核心支付处理逻辑,因为这两个关注点在物理和逻辑上都是分离的。
0 Comments
Log in to join the conversation.No comments yet. Be the first to share your thoughts.