一、事件溯源概述
事件溯源(Event Sourcing)是一种独特的架构模式:
核心思想:
- 用事件序列代替当前状态存储
- 任意时刻的状态都可以通过重放事件得到
- 完整的审计日志天然存在
与CRUD的对比:
| 操作 | CRUD模式 | 事件溯源模式 |
|---|---|---|
| 创建 | INSERT | 存储创建事件 |
| 更新 | UPDATE | 存储变更事件 |
| 删除 | DELETE | 存储删除事件 |
| 读取 | SELECT当前状态 | 重放事件计算状态 |
二、事件溯源核心概念
1. 事件本质
┌─────────────────────────────────────────────────────────────┐
│ 事件 │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Event │ │
│ │ - eventId: 唯一标识 │ │
│ │ - aggregateId: 聚合根ID │ │
│ │ - eventType: 事件类型 │ │
│ │ - eventData: 事件数据 │ │
│ │ - timestamp: 发生时间 │ │
│ │ - metadata: 元数据 │ │
│ └─────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
2. 聚合根与事件
java
// 事件定义
public class OrderCreatedEvent extends DomainEvent {
private final OrderId orderId;
private final CustomerId customerId;
private final List<OrderItemData> items;
public OrderCreatedEvent(OrderId orderId, CustomerId customerId,
List<OrderItemData> items) {
this.orderId = orderId;
this.customerId = customerId;
this.items = items;
}
// Getter...
}
public class OrderItemAddedEvent extends DomainEvent {
private final OrderId orderId;
private final ProductId productId;
private final int quantity;
public OrderItemAddedEvent(OrderId orderId, ProductId productId, int quantity) {
this.orderId = orderId;
this.productId = productId;
this.quantity = quantity;
}
}
public class OrderSubmittedEvent extends DomainEvent {
private final OrderId orderId;
public OrderSubmittedEvent(OrderId orderId) {
this.orderId = orderId;
}
}
3. 事件溯源聚合根
java
// 事件溯源聚合根
public class OrderAggregate {
private OrderId id;
private CustomerId customerId;
private OrderStatus status;
private List<OrderItem> items;
private Money totalAmount;
private int version;
// 事件溯源聚合根的构造方法
public OrderAggregate(List<DomainEvent> events) {
if (events == null || events.isEmpty()) {
throw new IllegalArgumentException("事件列表不能为空");
}
// 从事件重放构建状态
for (DomainEvent event : events) {
apply(event);
}
}
// 静态工厂方法创建新聚合
public static OrderAggregate create(OrderId id, CustomerId customerId) {
OrderAggregate aggregate = new OrderAggregate();
aggregate.id = id;
aggregate.customerId = customerId;
aggregate.status = OrderStatus.DRAFT;
aggregate.items = new ArrayList<>();
aggregate.totalAmount = Money.ZERO;
// 发布创建事件
aggregate.registerEvent(new OrderCreatedEvent(id, customerId));
return aggregate;
}
// 应用事件(状态变更)
public void apply(DomainEvent event) {
if (event instanceof OrderCreatedEvent) {
applyCreated((OrderCreatedEvent) event);
} else if (event instanceof OrderItemAddedEvent) {
applyItemAdded((OrderItemAddedEvent) event);
} else if (event instanceof OrderSubmittedEvent) {
applySubmitted((OrderSubmittedEvent) event);
}
}
private void applyCreated(OrderCreatedEvent event) {
this.id = event.getOrderId();
this.customerId = event.getCustomerId();
this.status = OrderStatus.DRAFT;
this.items = new ArrayList<>();
this.totalAmount = Money.ZERO;
}
private void applyItemAdded(OrderItemAddedEvent event) {
this.items.add(OrderItem.of(event.getProductId(), event.getQuantity()));
recalculateTotal();
}
private void applySubmitted(OrderSubmittedEvent event) {
this.status = OrderStatus.SUBMITTED;
}
// 命令方法
public void addItem(Product product, int quantity) {
if (status != OrderStatus.DRAFT) {
throw new BusinessException("只有草稿订单可以添加商品");
}
registerEvent(new OrderItemAddedEvent(id, product.getId(), quantity));
}
public void submit() {
if (items.isEmpty()) {
throw new BusinessException("订单不能为空");
}
registerEvent(new OrderSubmittedEvent(id));
}
// 事件注册
private List<DomainEvent> uncommittedEvents = new ArrayList<>();
protected void registerEvent(DomainEvent event) {
uncommittedEvents.add(event);
apply(event);
}
public List<DomainEvent> pullUncommittedEvents() {
List<DomainEvent> events = new ArrayList<>(uncommittedEvents);
uncommittedEvents.clear();
return events;
}
}
三、事件存储
1. 事件存储接口
java
// 事件存储接口
public interface EventStore {
// 保存事件
void append(String aggregateId, DomainEvent event);
// 批量保存事件
void append(String aggregateId, List<DomainEvent> events);
// 获取聚合的所有事件
List<DomainEvent> getEvents(String aggregateId);
// 获取聚合从某个版本之后的事件
List<DomainEvent> getEvents(String aggregateId, int fromVersion);
// 获取所有事件(用于投影重建)
Stream<DomainEvent> getAllEvents();
}
// 聚合快照接口
public interface SnapshotStore {
void save(String aggregateId, int version, Object state);
Optional<Object> getSnapshot(String aggregateId, int version);
}
2. JPA事件存储实现
sql
-- 事件表
CREATE TABLE domain_events (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
aggregate_id VARCHAR(64) NOT NULL,
aggregate_type VARCHAR(100) NOT NULL,
event_type VARCHAR(100) NOT NULL,
event_data JSON NOT NULL,
version INT NOT NULL,
timestamp TIMESTAMP NOT NULL,
metadata JSON,
INDEX idx_aggregate_id (aggregate_id),
INDEX idx_aggregate_version (aggregate_id, version),
INDEX idx_event_type (event_type),
INDEX idx_timestamp (timestamp)
);
java
// JPA事件存储实现
@Repository
public class JpaEventStore implements EventStore {
@Autowired
private DomainEventRepository eventRepository;
@Autowired
private ObjectMapper objectMapper;
@Transactional
@Override
public void append(String aggregateId, DomainEvent event) {
DomainEventEntity entity = new DomainEventEntity();
entity.setAggregateId(aggregateId);
entity.setAggregateType(event.getClass().getDeclaringClass().getSimpleName());
entity.setEventType(event.getClass().getSimpleName());
entity.setEventData(toJson(event));
entity.setVersion(event.getVersion());
entity.setTimestamp(LocalDateTime.now());
entity.setMetadata(toJson(event.getMetadata()));
eventRepository.save(entity);
}
@Override
public List<DomainEvent> getEvents(String aggregateId) {
List<DomainEventEntity> entities =
eventRepository.findByAggregateIdOrderByVersionAsc(aggregateId);
return entities.stream()
.map(this::toEvent)
.collect(Collectors.toList());
}
@Override
public List<DomainEvent> getEvents(String aggregateId, int fromVersion) {
List<DomainEventEntity> entities =
eventRepository.findByAggregateIdAndVersionGreaterThanOrderByVersionAsc(
aggregateId, fromVersion);
return entities.stream()
.map(this::toEvent)
.collect(Collectors.toList());
}
private String toJson(Object obj) {
try {
return objectMapper.writeValueAsString(obj);
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
}
private DomainEvent toEvent(DomainEventEntity entity) {
try {
Class<?> eventClass = Class.forName(
"com.example.domain.event." + entity.getEventType());
return (DomainEvent) objectMapper.readValue(
entity.getEventData(), eventClass);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
3. MongoDB事件存储
java
// MongoDB事件存储
@Component
public class MongoEventStore implements EventStore {
@Autowired
private MongoTemplate mongoTemplate;
@Override
public void append(String aggregateId, DomainEvent event) {
Document doc = new Document()
.append("aggregateId", aggregateId)
.append("eventType", event.getClass().getSimpleName())
.append("eventData", event)
.append("version", event.getVersion())
.append("timestamp", LocalDateTime.now());
mongoTemplate.insert(doc, "domain_events");
}
@Override
public List<DomainEvent> getEvents(String aggregateId) {
Query query = Query.query(
Criteria.where("aggregateId").is(aggregateId)
).sort(ascending("version"));
List<Document> docs = mongoTemplate.find(query, Document.class, "domain_events");
return docs.stream()
.map(this::documentToEvent)
.collect(Collectors.toList());
}
}
四、投影(Read Model)
1. 投影处理器
java
// 投影处理器
@Component
public class OrderProjectionHandler {
@Autowired
private JdbcTemplate jdbcTemplate;
// 处理订单创建
@EventHandler
public void handle(OrderCreatedEvent event) {
String sql = """
INSERT INTO orders (id, customer_id, status, total_amount, created_at)
VALUES (?, ?, ?, ?, ?)
""";
jdbcTemplate.update(sql,
event.getOrderId().getValue(),
event.getCustomerId().getValue(),
OrderStatus.DRAFT.name(),
0,
LocalDateTime.now()
);
}
// 处理订单项添加
@EventHandler
public void handle(OrderItemAddedEvent event) {
// 更新订单金额
String updateSql = """
UPDATE orders SET total_amount = total_amount + ?
WHERE id = ?
""";
jdbcTemplate.update(updateSql,
calculateItemAmount(event),
event.getOrderId().getValue());
// 插入订单项
String itemSql = """
INSERT INTO order_items (id, order_id, product_id, quantity, price)
VALUES (?, ?, ?, ?, ?)
""";
jdbcTemplate.update(itemSql,
UUID.randomUUID().toString(),
event.getOrderId().getValue(),
event.getProductId().getValue(),
event.getQuantity(),
getProductPrice(event.getProductId())
);
}
// 处理订单提交
@EventHandler
public void handle(OrderSubmittedEvent event) {
String sql = "UPDATE orders SET status = ? WHERE id = ?";
jdbcTemplate.update(sql,
OrderStatus.SUBMITTED.name(),
event.getOrderId().getValue());
}
}
2. 投影重建
java
// 投影重建服务
@Service
public class ProjectionRebuilder {
@Autowired
private EventStore eventStore;
@Autowired
private OrderProjectionHandler projectionHandler;
// 重建单个聚合的投影
public void rebuildOrderProjection(String orderId) {
List<DomainEvent> events = eventStore.getEvents(orderId);
// 清除现有投影
clearOrderProjection(orderId);
// 重新应用所有事件
for (DomainEvent event : events) {
dispatchToHandler(event);
}
}
// 重建所有投影
public void rebuildAllProjections() {
// 清除所有投影数据
clearAllProjections();
// 获取所有事件并重放
eventStore.getAllEvents().forEach(this::dispatchToHandler);
}
// 按时间范围重建
public void rebuildProjections(LocalDateTime from, LocalDateTime to) {
List<DomainEvent> events = eventStore.getEvents(from, to);
for (DomainEvent event : events) {
dispatchToHandler(event);
}
}
private void dispatchToHandler(DomainEvent event) {
if (event instanceof OrderCreatedEvent) {
projectionHandler.handle((OrderCreatedEvent) event);
} else if (event instanceof OrderItemAddedEvent) {
projectionHandler.handle((OrderItemAddedEvent) event);
} else if (event instanceof OrderSubmittedEvent) {
projectionHandler.handle((OrderSubmittedEvent) event);
}
}
}
五、快照机制
1. 快照实现
java
// 快照聚合根
public class SnapshotableAggregate {
private Object state;
private int snapshotVersion;
private List<DomainEvent> eventsSinceSnapshot;
protected void applySnapshot(Object state, int version) {
this.state = state;
this.snapshotVersion = version;
this.eventsSinceSnapshot = new ArrayList<>();
}
public Object getSnapshot() {
return state;
}
public int getSnapshotVersion() {
return snapshotVersion;
}
protected void registerEvent(DomainEvent event) {
eventsSinceSnapshot.add(event);
apply(event);
// 每10个事件保存一次快照
if (eventsSinceSnapshot.size() >= 10) {
saveSnapshot();
}
}
private void saveSnapshot() {
snapshotStore.save(getAggregateId(), snapshotVersion, getSnapshot());
eventsSinceSnapshot.clear();
}
}
// 快照仓储
@Component
public class SnapshotRepository {
@Autowired
private JdbcTemplate jdbcTemplate;
public void save(String aggregateId, int version, Object state) {
String sql = """
INSERT INTO snapshots (aggregate_id, version, state, created_at)
VALUES (?, ?, ?, ?)
ON DUPLICATE KEY UPDATE state = VALUES(state), created_at = VALUES(created_at)
""";
jdbcTemplate.update(sql, aggregateId, version, toJson(state), LocalDateTime.now());
}
public Optional<Object> findLatest(String aggregateId) {
String sql = """
SELECT state FROM snapshots
WHERE aggregate_id = ?
ORDER BY version DESC LIMIT 1
""";
List<Object> results = jdbcTemplate.query(sql,
(rs, rowNum) -> fromJson(rs.getString("state")),
aggregateId);
return results.isEmpty() ? Optional.empty() : Optional.of(results.get(0));
}
}
2. 聚合重建优化
java
// 使用快照重建聚合
@Service
public class AggregateRepository {
@Autowired
private EventStore eventStore;
@Autowired
private SnapshotStore snapshotStore;
public <T> T findById(String aggregateId, Class<T> aggregateClass) {
// 1. 尝试获取快照
Optional<Object> snapshotOpt = snapshotStore.findLatest(aggregateId);
List<DomainEvent> events;
int fromVersion = 0;
if (snapshotOpt.isPresent()) {
// 2. 有快照,从快照版本之后开始重放
Object snapshot = snapshotOpt.get();
SnapshotableAggregate aggregate = (SnapshotableAggregate) snapshot;
fromVersion = aggregate.getSnapshotVersion();
events = eventStore.getEvents(aggregateId, fromVersion + 1);
} else {
// 3. 无快照,从头重放
events = eventStore.getEvents(aggregateId);
}
// 4. 创建聚合并重放事件
Constructor<T> constructor = aggregateClass.getDeclaredConstructor(List.class);
return constructor.newInstance(events);
}
}
六、事件处理模式
1. 幂等处理
java
// 幂等事件处理器
@Component
public class IdempotentEventHandler {
@Autowired
private JdbcTemplate jdbcTemplate;
@Transactional
public void handleOrderCreated(OrderCreatedEvent event) {
// 检查是否已处理
String checkSql = "SELECT COUNT(*) FROM processed_events WHERE event_id = ?";
Integer count = jdbcTemplate.queryForObject(checkSql, Integer.class,
event.getEventId());
if (count != null && count > 0) {
return; // 已处理,跳过
}
// 处理事件
processOrderCreated(event);
// 记录已处理
String insertSql = "INSERT INTO processed_events (event_id, processed_at) VALUES (?, ?)";
jdbcTemplate.update(insertSql, event.getEventId(), LocalDateTime.now());
}
}
2. 顺序处理保证
java
// 确保事件按顺序处理
@Component
public class OrderedEventProcessor {
private final ConcurrentHashMap<String, AtomicInteger> processedVersions =
new ConcurrentHashMap<>();
public void process(DomainEvent event) {
String aggregateId = event.getAggregateId();
AtomicInteger expectedVersion = processedVersions.computeIfAbsent(
aggregateId, k -> new AtomicInteger(0));
synchronized (expectedVersion) {
int currentVersion = expectedVersion.get();
if (event.getVersion() != currentVersion + 1) {
// 版本不连续,等待或放入重试队列
throw new EventOutOfOrderException(event);
}
// 处理事件
doProcess(event);
// 更新版本
expectedVersion.incrementAndGet();
}
}
}
3. 并行处理
java
// 分区并行处理
@Component
public class PartitionedEventProcessor {
@KafkaListener(topics = "domain-events", concurrency = "3")
public void process(ConsumerRecord<String, DomainEvent> record) {
// 按aggregateId分区,保证同一聚合的事件顺序处理
DomainEvent event = record.value();
processEvent(event);
}
}
// 配置Kafka分区
@Configuration
public class KafkaConfig {
@Bean
public NewTopic domainEventsTopic() {
return TopicBuilder.name("domain-events")
.partitions(6)
.replicas(1)
.config("min.insync.replicas", "1")
.build();
}
}
七、事件溯源最佳实践
1. 事件设计原则
1. 事件是不可变的
- 不要修改已发生的事件
- 用补偿事件纠正错误
2. 事件要有意义
- 描述业务发生了什么
- 避免技术性事件
3. 事件数据要完整
- 包含重建状态所需的全部数据
- 避免引用外部数据
4. 事件要版本化
- 考虑事件格式变化
- 提供事件升级机制
2. 事件命名
java
// 好的命名
OrderCreatedEvent // 订单已创建 ✓
ItemAddedToOrderEvent // 商品已添加到订单 ✓
PaymentReceivedEvent // 收到付款 ✓
// 不好的命名
OrderSaveEvent // 订单保存 ✗
OrderUpdateEvent1 // 订单更新事件1 ✗
Event123 // 事件123 ✗
3. 适用场景
| 适合 | 不适合 |
|---|---|
| 需要完整审计日志 | 简单CRUD应用 |
| 业务规则复杂 | 数据量非常大 |
| 需要时间旅行查询 | 对延迟敏感 |
| 需要事件驱动架构 | 团队不熟悉CQRS/ES |
八、与其他模式结合
1. CQRS + Event Sourcing
命令端:
┌─────────────────────────────────────┐
│ 聚合根(存储事件) │
│ - 接收命令 │
│ - 发布事件 │
│ - 事件存储 │
└─────────────────────────────────────┘
│
▼
┌─────────────────────────────────────┐
│ 事件存储 │
│ - 所有事件 │
│ - 可重放 │
└─────────────────────────────────────┘
│
▼
查询端:
┌─────────────────────────────────────┐
│ 投影处理器 │
│ - 订阅事件 │
│ - 更新查询模型 │
└─────────────────────────────────────┘
2. Saga + Event Sourcing
java
// 事件溯源Saga
public class OrderPaymentSaga {
@StartSaga
@SagaEventHandler(associationProperty = "orderId")
public void handle(OrderCreatedEvent event) {
// 启动支付流程
commandGateway.send(new InitiatePaymentCommand(event.getOrderId(), event.getAmount()));
}
@SagaEventHandler(associationProperty = "orderId")
public void handle(PaymentSucceededEvent event) {
// 继续下一步
commandGateway.send(new ShipOrderCommand(event.getOrderId()));
}
@SagaEventHandler(associationProperty = "orderId")
public void handle(PaymentFailedEvent event) {
// 补偿
commandGateway.send(new CancelOrderCommand(event.getOrderId()));
}
}
九、总结
事件溯源是一种强大的架构模式:
- 事件存储:完整的业务历史
- 状态重建:重放事件得到任意状态
- 天然审计:所有变更都有记录
- 时间旅行:支持历史状态查询
最佳实践:
- 设计好事件结构
- 实现幂等处理
- 合理使用快照
- 做好投影重建机制
个人观点,仅供参考