为什么CQRS和事件溯源不是“过度设计”,而是复杂系统的救星
\n\n您是否曾遇到过这样的情况:用Rust编写的系统在处理读取请求时表现完美,但在写入时却“力不从心”?或者反过来——为了保存完整的历史变更记录而牺牲了读取性能?听起来很熟悉?那么欢迎来到CQRS和事件溯源的世界。
\n\n许多开发者认为这些模式是“重型武器”,仅适用于C#或Java的企业级项目。但随着Rust在高负载系统中的流行(从游戏服务器到金融平台),CQRS和事件溯源不仅变得有用——它们变得必不可少。Rust凭借其零成本抽象和严格的类型系统,是实现这些模式的理想语言。
\n\n在本文中,我们不仅会讨论理论。我们将编写可在Rust中运行的工作代码,您可以将其适配到自己的项目中。您将了解如何分离命令和查询的职责,如何存储事件而非当前状态,以及这一切如何成倍地加速您的系统。
\n\n什么是CQRS和事件溯源?为Rust开发者用通俗语言解释
\n\n想象一下您管理一个银行账户。在传统的CRUD模型中,您有一个“账户”表,其中存储当前余额。如果需要查看历史记录,您会再创建一个“交易”表。但如果您不仅需要知道余额,还需要理解为什么它发生了变化?或者需要恢复任意日期的状态?
\n\nCQRS:分而治之
\n\nCQRS(命令查询职责分离)是一种将操作分为两组的原理:
\n- \n
- 命令(Commands)——改变状态。它们不返回数据,只返回确认或错误。 \n
- 查询(Queries)——只读取数据。它们不改变状态。 \n
在Rust中,这种分离看起来很自然:命令和查询的类型是不同的结构体,它们的处理程序是不同的函数或模块。这不仅仅是“良好实践”,更是一种避免在读取时意外修改的方法。
\n\n// 命令——改变状态\npub struct CreateOrder {\n pub user_id: Uuid,\n pub items: Vec<OrderItem>,\n}\n\n// 查询——只读取\npub struct GetOrderHistory {\n pub user_id: Uuid,\n pub limit: usize,\n}\n\n事件溯源:存储历史,而非快照
\n\n事件溯源是一种方法,我们存储的不是对象的当前状态,而是导致该状态的一系列事件。想知道2023年1月1日的账户余额?只需从开始“重放”所有事件直到该日期。
\n\n优势显而易见:
\n- \n
- 审计——每次变更都被记录。 \n
- 时间切片——可以恢复任何状态。 \n
- 对错误的鲁棒性——如果代码中有错误,您不会丢失数据,只是错误地“重放”了它们。 \n
实践示例:基于Rust的订单管理系统
\n\n让我们为在线商店构建一个小型系统。它将处理命令(创建订单、添加商品)和查询(获取购物车、订单历史)。所有逻辑都基于事件构建。
\n\n步骤1:定义事件和命令
\n\n首先要做的是设计领域。在Rust中,我们使用枚举(enum)表示事件,使用严格的结构体表示命令。
use serde::{Deserialize, Serialize};\nuse uuid::Uuid;\n\n// 事件——不可变的事实\n#[derive(Debug, Clone, Serialize, Deserialize)]\npub enum OrderEvent {\n Created { order_id: Uuid, user_id: Uuid, timestamp: i64 },\n ItemAdded { order_id: Uuid, item: Str