SpringBoot + 事件溯源 + CQRS:高一致性与高性能读写分离架构

一、你的系统是不是也遇到了这些问题?

公司的订单系统越来越慢了,用户查询订单要等好几秒,而且经常出现数据不一致的问题。更糟糕的是,每次优化查询性能,都会影响订单创建的性能;优化订单创建,查询又变慢了。简直是个死循环。

这样的场景,作为后端开发的你,是不是也遇到过?

二、传统架构的困境

在传统的CRUD架构中,我们通常是这样设计的:

  • 一个数据库表存储订单的所有信息
  • 写操作(创建、更新、删除)直接操作数据库
  • 读操作(查询)也从同一个数据库读取

这种架构在业务简单、数据量不大的情况下工作得很好。但随着业务的发展,问题就暴露出来了:

1. 读写冲突

写操作和读操作都在同一个数据库上,相互影响。写操作需要加锁,影响读性能;读操作占用连接,影响写性能。

2. 性能瓶颈

随着数据量的增长,查询越来越慢。为了优化查询,我们添加索引、做分库分表,但这些都增加了系统的复杂度。

3. 数据一致性

在分布式环境下,保证数据一致性变得更加困难。多个服务同时操作同一份数据,很容易出现数据不一致的情况。

4. 审计困难

我们只知道数据的当前状态,但不知道数据是如何变成这样的。如果出现问题,很难追溯原因。

三、事件溯源 + CQRS:架构设计的革命

为了解决这些问题,业界提出了两个重要的架构模式:

1. 事件溯源(Event Sourcing)

传统的CRUD架构存储的是数据的当前状态,而事件溯源存储的是导致状态变化的事件

打个比方:

  • 传统架构:就像拍照片,只能看到当前的样子
  • 事件溯源:就像录像带,可以回放整个过程

2. CQRS(Command Query Responsibility Segregation)

CQRS的核心思想是将**写操作(Command)读操作(Query)**分离:

  • 写操作:只负责接收命令,发布事件
  • 读操作:只负责查询数据,从读模型中读取

四、方案详解

1. 事件溯源的实现

(1)事件定义

首先,我们需要定义事件:

// 订单创建事件
public class OrderCreatedEvent {
    private String orderId;
    private String userId;
    private BigDecimal amount;
    private List<OrderItem> items;
    private LocalDateTime createTime;
}

// 订单支付事件
public class OrderPaidEvent {
    private String orderId;
    private String transactionId;
    private LocalDateTime payTime;
}

// 订单取消事件
public class OrderCancelledEvent {
    private String orderId;
    private String reason;
    private LocalDateTime cancelTime;
}

(2)事件存储

事件需要持久化存储,我们可以使用数据库来存储事件:

@Entity
@Table(name = "event_store")
public class EventStore {
    
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @Column(name = "aggregate_id", nullable = false)
    private String aggregateId; // 聚合根ID
    
    @Column(name = "event_type", nullable = false)
    private String eventType; // 事件类型
    
    @Column(name = "event_data", columnDefinition = "JSON", nullable = false)
    private String eventData; // 事件数据
    
    @Column(name = "version", nullable = false)
    private Long version; // 版本号
    
    @Column(name = "create_time", nullable = false)
    private LocalDateTime createTime;
}

(3)事件发布

当业务操作发生时,我们发布事件而不是直接修改数据:

@Service
public class OrderCommandService {
    
    @Autowired
    private EventStore eventStore;
    @Autowired
    private ApplicationEventPublisher eventPublisher;
    
    public void createOrder(CreateOrderCommand command) {
        // 1. 创建订单ID
        String orderId = UUID.randomUUID().toString();
        
        // 2. 创建订单创建事件
        OrderCreatedEvent event = new OrderCreatedEvent();
        event.setOrderId(orderId);
        event.setUserId(command.getUserId());
        event.setAmount(command.getAmount());
        event.setItems(command.getItems());
        event.setCreateTime(LocalDateTime.now());
        
        // 3. 存储事件
        eventStore.saveEvent(orderId, event);
        
        // 4. 发布事件
        eventPublisher.publishEvent(event);
    }
}

2. CQRS的实现

(1)命令处理器

命令处理器负责处理写操作:

@Component
public class OrderCommandHandler {
    
    @Autowired
    private EventStore eventStore;
    @Autowired
    private ApplicationEventPublisher eventPublisher;
    
    @Autowired
    private OrderQueryService queryService;
    
    public void handle(CreateOrderCommand command) {
        // 验证订单
        validateOrder(command);
        
        // 创建订单创建事件
        OrderCreatedEvent event = new OrderCreatedEvent();
        event.setOrderId(command.getOrderId());
        event.setUserId(command.getUserId());
        event.setAmount(command.getAmount());
        event.setItems(command.getItems());
        event.setCreateTime(LocalDateTime.now());
        
        // 存储事件
        eventStore.saveEvent(command.getOrderId(), event);
        
        // 发布事件
        eventPublisher.publishEvent(event);
    }
    
    public void handle(PayOrderCommand command) {
        // 验证订单状态
        Order order = queryService.getOrder(command.getOrderId());
        if (order.getStatus() != OrderStatus.PENDING) {
            throw new RuntimeException("订单状态不正确");
        }
        
        // 创建订单支付事件
        OrderPaidEvent event = new OrderPaidEvent();
        event.setOrderId(command.getOrderId());
        event.setTransactionId(command.getTransactionId());
        event.setPayTime(LocalDateTime.now());
        
        // 存储事件
        eventStore.saveEvent(command.getOrderId(), event);
        
        // 发布事件
        eventPublisher.publishEvent(event);
    }
}

(2)事件处理器

事件处理器监听事件,更新读模型:

@Component
@Slf4j
public class OrderEventHandler {
    
    @Autowired
    private OrderReadModelRepository readModelRepository;
    
    @EventListener
    public void handle(OrderCreatedEvent event) {
        log.info("处理订单创建事件:{}", event.getOrderId());
        
        // 创建读模型
        OrderReadModel readModel = new OrderReadModel();
        readModel.setOrderId(event.getOrderId());
        readModel.setUserId(event.getUserId());
        readModel.setAmount(event.getAmount());
        readModel.setStatus(OrderStatus.PENDING);
        readModel.setItems(event.getItems());
        readModel.setCreateTime(event.getCreateTime());
        
        // 保存读模型
        readModelRepository.save(readModel);
    }
    
    @EventListener
    public void handle(OrderPaidEvent event) {
        log.info("处理订单支付事件:{}", event.getOrderId());
        
        // 更新读模型
        OrderReadModel readModel = readModelRepository.findByOrderId(event.getOrderId());
        if (readModel != null) {
            readModel.setStatus(OrderStatus.PAID);
            readModel.setPayTime(event.getPayTime());
            readModelRepository.save(readModel);
        }
    }
    
    @EventListener
    public void handle(OrderCancelledEvent event) {
        log.info("处理订单取消事件:{}", event.getOrderId());
        
        // 更新读模型
        OrderReadModel readModel = readModelRepository.findByOrderId(event.getOrderId());
        if (readModel != null) {
            readModel.setStatus(OrderStatus.CANCELLED);
            readModel.setCancelReason(event.getReason());
            readModel.setCancelTime(event.getCancelTime());
            readModelRepository.save(readModel);
        }
    }
}

(3)查询服务

查询服务只负责读操作:

@Service
public class OrderQueryService {
    
    @Autowired
    private OrderReadModelRepository readModelRepository;
    
    public Order getOrder(String orderId) {
        OrderReadModel readModel = readModelRepository.findByOrderId(orderId);
        if (readModel == null) {
            throw new RuntimeException("订单不存在");
        }
        return convertToOrder(readModel);
    }
    
    public List<Order> getUserOrders(String userId) {
        List<OrderReadModel> readModels = readModelRepository.findByUserId(userId);
        return readModels.stream()
            .map(this::convertToOrder)
            .collect(Collectors.toList());
    }
    
    private Order convertToOrder(OrderReadModel readModel) {
        Order order = new Order();
        order.setOrderId(readModel.getOrderId());
        order.setUserId(readModel.getUserId());
        order.setAmount(readModel.getAmount());
        order.setStatus(readModel.getStatus());
        order.setItems(readModel.getItems());
        order.setCreateTime(readModel.getCreateTime());
        return order;
    }
}

3. 状态重建

事件溯源的一个强大特性是可以从事件流中重建状态:

@Service
public class OrderStateRebuilder {
    
    @Autowired
    private EventStore eventStore;
    
    public Order rebuildOrder(String orderId) {
        // 1. 获取订单的所有事件
        List<Event> events = eventStore.getEvents(orderId);
        
        // 2. 按顺序重放事件
        Order order = new Order();
        for (Event event : events) {
            applyEvent(order, event);
        }
        
        return order;
    }
    
    private void applyEvent(Order order, Event event) {
        if (event instanceof OrderCreatedEvent) {
            OrderCreatedEvent e = (OrderCreatedEvent) event;
            order.setOrderId(e.getOrderId());
            order.setUserId(e.getUserId());
            order.setAmount(e.getAmount());
            order.setStatus(OrderStatus.PENDING);
        } else if (event instanceof OrderPaidEvent) {
            order.setStatus(OrderStatus.PAID);
        } else if (event instanceof OrderCancelledEvent) {
            order.setStatus(OrderStatus.CANCELLED);
        }
    }
}

五、方案优势

1. 高性能读写分离

  • 写操作:只负责存储和发布事件,性能很高
  • 读操作:从专门的读模型中读取,可以针对查询场景优化

2. 完整的审计日志

  • 所有的状态变化都以事件的形式记录下来
  • 可以随时回放历史,追溯问题原因

3. 高度可扩展

  • 读模型可以根据不同的查询场景进行优化
  • 可以轻松添加新的读模型,不影响写操作

4. 最终一致性

  • 通过事件机制保证最终一致性
  • 可以通过重放事件修复数据不一致问题

六、实战经验总结

  1. 事件设计要合理:事件应该是不可变的,包含足够的信息
  2. 版本控制很重要:事件结构可能会变化,需要做好版本控制
  3. 事件存储要高效:事件存储的性能直接影响整个系统的性能
  4. 读模型要及时更新:可以通过消息队列保证事件的可靠传递
  5. 监控要到位:对事件的发布、处理、消费等环节进行监控

七、完整架构图

┌─────────────┐          ┌─────────────┐          ┌─────────────┐
│             │          │             │          │             │
│  客户端请求   │─────────>│  命令处理器   │─────────>│  事件存储    │
│             │          │             │          │             │
└─────────────┘          └─────────────┘          └─────────────┘
       │                          │                      │
       │                          │                      │
       │                          ▼                      │
       │                  ┌─────────────┐               │
       │                  │             │               │
       │                  │  事件发布器   │               │
       │                  │             │               │
       │                  └─────────────┘               │
       │                          │                      │
       │                          ▼                      │
       │                  ┌─────────────┐               │
       │                  │             │               │
       │                  │  事件处理器   │               │
       │                  │             │               │
       │                  └─────────────┘               │
       │                          │                      │
       │                          ▼                      │
       │                  ┌─────────────┐               │
       │                  │             │               │
       └──────────────────│  读模型      │               │
                          │             │               │
                          └─────────────┘               │
                                │                      │
                                ▼                      │
                          ┌─────────────┐               │
                          │             │               │
                          │  查询服务    │               │
                          │             │               │
                          └─────────────┘               │
                                │                      │
                                ▼                      │
                          ┌─────────────┐               │
                          │             │               │
                          │  客户端响应   │               │
                          │             │               │
                          └─────────────┘               │
                                │                      │
                                ▼                      │
                          ┌─────────────┐               │
                          │             │               │
                          │  状态重建    │◄──────────────┘
                          │             │
                          └─────────────┘

八、适用场景

事件溯源 + CQRS 并不是银弹,它适用于以下场景:

1. 高并发读写分离

  • 读操作远多于写操作
  • 需要对读性能进行优化
  • 写操作和读操作的性能要求不同

2. 审计要求高

  • 需要完整的操作日志
  • 需要追溯历史状态
  • 需要支持数据回滚

3. 业务逻辑复杂

  • 业务规则复杂,需要精确控制
  • 需要支持复杂的业务流程
  • 需要支持业务规则的动态调整

4. 分布式系统

  • 需要保证最终一致性
  • 需要支持服务解耦
  • 需要支持服务自治

九、写在最后

事件溯源 + CQRS 是一种强大的架构模式,它从根本上改变了我们设计和实现系统的方式。

通过事件溯源,我们可以完整地记录业务的发生过程;通过CQRS,我们可以实现高性能的读写分离。这两者结合,为我们提供了一种全新的思路来构建高一致性与高性能的系统。

当然,这种架构模式也有它的复杂度和学习成本。但在合适的场景下,它带来的收益是巨大的。

希望这篇文章能给你带来一些启发,帮助你在实际项目中更好地应用事件溯源和CQRS。

如果你在事件溯源和CQRS的实践中有其他经验或困惑,欢迎在评论区留言交流!


服务端技术精选,专注分享后端开发实战经验,让技术落地更简单。

如果你觉得这篇文章有用,欢迎点赞、在看、分享三连!


标题:SpringBoot + 事件溯源 + CQRS:高一致性与高性能读写分离架构
作者:jiangyi
地址:http://www.jiangyi.space/articles/2026/02/10/1770536593883.html

    评论
    0 评论
avatar

取消