Event Sourcing 是什么(不是“只是日志”)
在 event sourcing 里,事件才是真相。你不保存对象的“最终状态”,而是保存它发生过的每一件事。然后通过回放事件重建状态。是的,像回放。也不,这不是“只是日志”:日志讲故事,事件具有事实效力。
简单例子:不保存 balance = 120,而是记录 MoneyDeposited(200) 和 MoneyWithdrawn(80)。余额是结果,不是凭空冒出来的数字。
为什么要折腾?
因为未来的你会感谢你:
- 需要审计谁在什么时候做了什么
- 需要调试,通过回放现实来还原问题
- 业务变化后要重建模型
- 需要投影(read model),而不想把数据库折腾崩
一句话:现在付一点复杂度,之后少一点灾后复盘。
核心组件(无烟版)
- 事件:过去发生的不可变事实。
OrderPlaced,不是 PlaceOrder。
- 流:一个聚合的事件序列。
- 聚合:应用事件来重建状态的领域对象。
- 投影:派生视图(SQL/Elastic/缓存等)用于快速读取。
什么时候用(什么时候别用)
适合用:
不适合用:
- CRUD 很基础
- 不需要审计
- 团队还在摸索仓储模式
Symfony 入门小示例
保持简单:一个 Cart 聚合、事件、一个内存 event store。没有完整 CQRS,没有 Kafka,没有魔法。就核心。
1) 定义事件
1<?php
2
3namespace App\Cart\Domain\Event;
4
5final class ItemAdded
6{
7 public function __construct(
8 public readonly string $cartId,
9 public readonly string $sku,
10 public readonly int $quantity,
11 ) {
12 }
13}
2) 回放事件的聚合
1<?php
2
3namespace App\Cart\Domain;
4
5use App\Cart\Domain\Event\ItemAdded;
6
7final class Cart
8{
9 private array $items = [];
10 private array $recordedEvents = [];
11
12 public static function reconstitute(array $events): self
13 {
14 $cart = new self();
15 foreach ($events as $event) {
16 $cart->apply($event);
17 }
18 return $cart;
19 }
20
21 public function addItem(string $cartId, string $sku, int $quantity): void
22 {
23 $event = new ItemAdded($cartId, $sku, $quantity);
24 $this->record($event);
25 }
26
27 public function recordedEvents(): array
28 {
29 return $this->recordedEvents;
30 }
31
32 private function record(object $event): void
33 {
34 $this->apply($event);
35 $this->recordedEvents[] = $event;
36 }
37
38 private function apply(object $event): void
39 {
40 if ($event instanceof ItemAdded) {
41 $this->items[$event->sku] = ($this->items[$event->sku] ?? 0) + $event->quantity;
42 }
43 }
44}
3) 最小内存事件存储
1<?php
2
3namespace App\Cart\Infrastructure;
4
5final class InMemoryEventStore
6{
7 private array $streams = [];
8
9 public function append(string $streamId, array $events): void
10 {
11 $this->streams[$streamId] ??= [];
12 $this->streams[$streamId] = array_merge($this->streams[$streamId], $events);
13 }
14
15 public function load(string $streamId): array
16 {
17 return $this->streams[$streamId] ?? [];
18 }
19}
4) 在 Symfony 服务中的使用
1<?php
2
3namespace App\Cart\Application;
4
5use App\Cart\Domain\Cart;
6use App\Cart\Infrastructure\InMemoryEventStore;
7
8final class AddItemToCart
9{
10 public function __construct(private InMemoryEventStore $eventStore)
11 {
12 }
13
14 public function __invoke(string $cartId, string $sku, int $quantity): void
15 {
16 $events = $this->eventStore->load($cartId);
17 $cart = Cart::reconstitute($events);
18
19 $cart->addItem($cartId, $sku, $quantity);
20
21 $this->eventStore->append($cartId, $cart->recordedEvents());
22 }
23}
就这样。没有 ORM,没有缓存,也没有游行。只有核心想法:事件是真相,状态是计算出来的视图。想玩更高级的,可以再接投影和 Symfony Messenger 的监听器。
要点(给人解释用)
- Event sourcing 保存的是历史,不是只有结果。
- 通过回放事件重建状态。
- 适合审计和不断变化的领域,普通 CRUD 没必要。
- Symfony 不会替你完成,但它允许你结构化地做。
如果你觉得沉重,那很正常:你用短期舒适换长期清晰。现在付钱,或者未来的你付。自己选。