【架构实战】Event Sourcing事件溯源详解

一、事件溯源概述

事件溯源(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()));
    }
}

九、总结

事件溯源是一种强大的架构模式:

  • 事件存储:完整的业务历史
  • 状态重建:重放事件得到任意状态
  • 天然审计:所有变更都有记录
  • 时间旅行:支持历史状态查询

最佳实践:

  1. 设计好事件结构
  2. 实现幂等处理
  3. 合理使用快照
  4. 做好投影重建机制

个人观点,仅供参考

相关推荐
ting94520001 小时前
Kimi-VL-A3B-Thinking 技术全解
人工智能·架构
qq_411262422 小时前
四博 AI 智能音箱 4G S3架构方案
人工智能·架构·智能音箱
大龄码农-涵哥2 小时前
Spring Cloud微服务架构详解:从服务注册到配置中心,阿里面试核心知识点
spring cloud·微服务·架构
roman_日积跬步-终至千里2 小时前
【案例题-知识点】架构风格与架构模式(2):高频架构模式与选型
架构
企业架构师老王2 小时前
药品生产环节:用实在Agent自动生成批记录与打印领料单的合规设计与架构落地
大数据·人工智能·ai·架构
小柯博客2 小时前
STM32MP2 RIF资源隔离框架详解:从架构到实践
网络·stm32·单片机·嵌入式硬件·架构·嵌入式·yocto
ai产品老杨2 小时前
告别重复造轮子:深度解析支持源码交付的 AI 视频平台架构,实现 X86/ARM 与 GPU/NPU 异构算力融合
人工智能·架构·音视频
LSL666_2 小时前
微服务架构——有关概念
微服务·云原生·架构
小江的记录本2 小时前
【微服务与云原生架构】Serverless架构、FaaS/BaaS、核心原理、优缺点
java·后端·微服务·云原生·架构·系统架构·serverless