当常规CRUD不再够用:认识CQRS和事件溯源
\n想象一下,您正在构建一个票务预订系统。当只有十个用户时,一切都很简单。但当用户数量达到一千,甚至一百万时,问题就开始出现了:数据库被锁定,请求发生冲突,而且无法恢复变更历史。这种情况熟悉吗?
\n在Go开发的世界里,性能和并发是我们的核心,两种架构模式有助于解决这些问题:CQRS(命令查询职责分离)和事件溯源。今天,我们将通过代码、示例和实际使用场景来实践它们。
\n\n什么是CQRS?简单解释
\nCQRS是命令(写入)和查询(读取)之间职责的分离。在经典的CRUD中,一个数据模型同时负责创建和读取。在CQRS中,我们创建两个独立的模型:
\n- \n
- 命令模型 — 负责状态变更(创建、更新、删除) \n
- 查询模型 — 仅负责数据读取 \n
这使得每一方都可以独立优化:写入可以严格验证,而读取可以是非规范化和快速的。在Go中,由于goroutine和通道的存在,这一点尤其高效。
\n\nGo中CQRS的简单示例
\n让我们创建一个基本的订单管理系统。从命令开始:
\n\ntype CreateOrderCommand struct {\n OrderID string\n UserID string\n ProductID string\n Amount float64\n}\n\ntype CommandHandler struct {\n repo *OrderRepository\n}\n\nfunc (h *CommandHandler) HandleCreateOrder(ctx context.Context, cmd CreateOrderCommand) error {\n order := Order{\n ID: cmd.OrderID,\n UserID: cmd.UserID,\n ProductID: cmd.ProductID,\n Amount: cmd.Amount,\n Status: "pending",\n CreatedAt: time.Now(),\n }\n return h.repo.Save(ctx, order)\n}\n\n现在创建一个独立的读取模型:
\n\ntype OrderQuery struct {\n repo *ReadRepository\n}\n\ntype OrderView struct {\n OrderID string\n UserName string\n ProductName string\n Total float64\n Status string\n}\n\nfunc (q *OrderQuery) GetOrderSummary(ctx context.Context, orderID string) (*OrderView, error) {\n return q.repo.GetOrderView(ctx, orderID)\n}\n\n请注意:在OrderView中已经包含了非规范化的数据(用户名、产品名称),这些数据来自不同的来源。这使读取速度提高了数十倍。
事件溯源:记录事件而非状态
\n事件溯源改变了数据存储的方式:我们不存储对象的当前状态,而是存储导致该状态的一系列事件。这带来了超能力:完整的变更历史以及将状态“回滚”的能力。
\n\n在Go中实现事件溯源
\n从基本的事件类型开始:
\n\ntype Event struct {\n ID string\n AggregateID string\n Type string\n Data []byte\n Timestamp time.Time\n Version int\n}\n\ntype EventStore interface {\n Save(ctx context.Context, events []Event) error\n Load(ctx context.Context, aggregateID string) ([]Event, error)\n}\n\n现在实现聚合——一个从事件中恢复自身状态的对象:
\n\ntype OrderAggregate struct {\n ID string\n Status string\n Items []OrderItem\n Version int\n}\n\nfunc (a *OrderAggregate) Apply(event Event) {\n switch event.Type {\n case "OrderCreated":\n var data OrderCreatedData\n json.Unmarshal(event.Data, &data)\n a.ID = data.OrderID\n a.Status = "created"\n case "OrderConfirmed":\n a.Status = "confirmed"\n case "OrderShipped":\n a.Stat