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

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

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

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

二、传统架构的困境

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

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

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

1. 读写冲突

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

2. 性能瓶颈

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

3. 数据一致性

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

4. 审计困难

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

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

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

1. 事件溯源(Event Sourcing)

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

打个比方:

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

2. CQRS(Command Query Responsibility Segregation)

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

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

四、方案详解

1. 事件溯源的实现

(1)事件定义

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

java 复制代码
// 订单创建事件
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)事件存储

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

java 复制代码
@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)事件发布

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

java 复制代码
@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)命令处理器

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

java 复制代码
@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)事件处理器

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

java 复制代码
@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)查询服务

查询服务只负责读操作:

java 复制代码
@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. 状态重建

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

java 复制代码
@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. 监控要到位:对事件的发布、处理、消费等环节进行监控

七、完整架构图

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

八、适用场景

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

1. 高并发读写分离

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

2. 审计要求高

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

3. 业务逻辑复杂

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

4. 分布式系统

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

九、写在最后

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

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

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

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

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


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

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

相关推荐
颜酱3 分钟前
# 02 | 搭骨架:用 LangGraph 编排 12 步工作流(思路)
前端·人工智能·后端
颜酱4 分钟前
02 | 搭骨架:用 LangGraph 编排 12 步工作流
前端·人工智能·后端
嘉伟桑1 小时前
Python调用电价API返回JSON示例:分时电价、现货电价和字段解析
后端
武子康1 小时前
Shippy:确定性工具、会话级 Sandbox 与 Live-Data Eval(4 类收敛 + 7 步实现方案 + 6 类评测指标)
前端·人工智能·后端
我叫黑大帅2 小时前
我为什么单一消费者的场景下,要用 Redis List 当消息队列?
redis·后端·面试
AskHarries2 小时前
文件上传系统
后端
止语Lab3 小时前
好的 DX 不等于少写代码——三种语言的摩擦力设计课
后端
吃饱了得干活3 小时前
别再手动解析 LLM 输出了!LangChain 四种结构化输出方案对比
后端·python·langchain
程序员天天困3 小时前
Arthas trace 命令怎么用?一行定位最慢那行代码
jvm·后端
Huiturn3 小时前
GPT 5.6 连续编码 10 小时,纯 Python 啃下 Word 二进制格式——doc2docx 实现拆解
后端