封面

基于Event Sourcing和CQRS的微服务架构设计与实战

业务场景描述

在电商系统中,订单的高并发写入与复杂的状态流转(下单、支付、发货、退货等)给传统的CRUD模型带来了挑战:

  • 数据一致性难保证:跨服务事务处理复杂,分布式事务开销大。
  • 写放大问题:频繁更新导致热点写入及性能瓶颈。
  • 审计和追溯需求:需要完整的订单状态变更历史。

针对上述痛点,我们引入Event Sourcing(事件溯源)与CQRS(命令查询职责分离)来构建高可用、可追溯、易扩展的订单微服务。

技术选型过程

  1. Event Sourcing:将状态变化记录为不可变事件,完整保留历史。优点是天然可审计、可回溯,但事件存储和重播需要额外设计。
  2. CQRS:将写模型(Command)与读模型(Query)分离,写入事件后异步同步或投影至专门的查询存储,提高读写性能。缺点是最终一致性带来的复杂性。
  3. 消息中间件:选择Kafka作为事件总线,提供高吞吐与持久保证。
  4. 存储:事件存储使用关系型数据库(PostgreSQL + EventStore表),查询存储使用Elasticsearch,以满足复杂搜索与报表需求。

综合考虑,系统采用:Spring Boot + Spring Cloud 构建微服务;Event Sourcing + CQRS;Kafka 事件总线;PostgreSQL 事件表;Elasticsearch 查询库。

实现方案详解

项目结构(简化)

order-service/
├── cmd-api/           // Command 侧 REST 接口
├── cmd-impl/          // Command 处理、Event Sourcing 模块
├── query-service/     // Query 侧服务(Spring Data + ES)
├── common/            // 共享模型和工具包
└── config/            // 配置中心、Spring Cloud Config

1. 事件定义

// OrderCreatedEvent.java
public class OrderCreatedEvent {private String orderId;private BigDecimal amount;private LocalDateTime createdTime;// getter/setter
}// OrderStatusChangedEvent.java
public class OrderStatusChangedEvent {private String orderId;private String fromStatus;private String toStatus;private LocalDateTime occurredTime;// getter/setter
}

2. 聚合与Command处理

@Service
public class OrderAggregate {@Aggregateprivate String orderId;private String status;@CommandHandlerpublic OrderAggregate(CreateOrderCommand cmd) {// 校验if (cmd.getAmount().compareTo(BigDecimal.ZERO) <= 0) {throw new IllegalArgumentException("订单金额必须大于0");}// 发布事件apply(new OrderCreatedEvent(cmd.getOrderId(), cmd.getAmount(), LocalDateTime.now()));}@CommandHandlerpublic void handle(ChangeOrderStatusCommand cmd) {apply(new OrderStatusChangedEvent(orderId, this.status, cmd.getNewStatus(), LocalDateTime.now()));}@EventSourcingHandlerpublic void on(OrderCreatedEvent evt) {this.orderId = evt.getOrderId();this.status = "CREATED";}@EventSourcingHandlerpublic void on(OrderStatusChangedEvent evt) {this.status = evt.getToStatus();}
}

3. Kafka配置(application.yml)

spring:kafka:bootstrap-servers: ${KAFKA_SERVERS}producer:key-serializer: org.apache.kafka.common.serialization.StringSerializervalue-serializer: org.springframework.kafka.support.serializer.JsonSerializerconsumer:key-deserializer: org.apache.kafka.common.serialization.StringDeserializervalue-deserializer: org.springframework.kafka.support.serializer.JsonDeserializerproperties:spring.json.trusted.packages: "*"

4. 读模型投影

@Service
public class OrderProjection {@EventListenerpublic void handle(OrderCreatedEvent evt) {OrderIndex idx = new OrderIndex(evt.getOrderId(), evt.getAmount(), evt.getCreatedTime(), "CREATED");orderIndexRepository.save(idx);}@EventListenerpublic void handle(OrderStatusChangedEvent evt) {OrderIndex idx = orderIndexRepository.findById(evt.getOrderId()).orElseThrow();idx.setStatus(evt.getToStatus());orderIndexRepository.save(idx);}
}

Elasticsearch实体:

@Document(indexName = "order_index")
public class OrderIndex {@Id private String orderId;private BigDecimal amount;private LocalDateTime createdTime;private String status;// constructor/getter/setter
}

5. API示例

// 创建订单
@PostMapping("/orders")
public ResponseEntity<String> create(@RequestBody CreateOrderDTO dto) {commandGateway.send(new CreateOrderCommand(dto.getOrderId(), dto.getAmount()));return ResponseEntity.accepted().body("创建成功");
}// 查询订单
@GetMapping("/orders/{id}")
public Mono<OrderIndex> get(@PathVariable String id) {return orderIndexRepository.findById(id);
}

踩过的坑与解决方案

  1. 事件顺序乱序:Kafka多分区导致同一订单事件投递顺序不一致。解决:指定订单ID为分区键,保证同一Key事件有序。
  2. 投影脏读:事件尚未投影完成前查询不到数据。解决:业务可加重试机制或在响应中返回Location,让客户端轮询获取。
  3. 事件库膨胀:历史事件表过大影响查询。解决:定期归档老事件或冷表分区清理策略。
  4. 聚合重放性能:启动时重放全量事件过慢。解决:采用快照(Snapshot)机制定期保留最新状态,以快照为起点加载。

总结与最佳实践

  • Event Sourcing+CQRS模式适用于高并发、复杂状态流转、强审计需求场景。
  • 读写分离提升性能,但带来最终一致性,需要在业务层做好补偿。
  • 采用分区键、快照、归档等手段优化性能与存储。
  • 强烈建议构建完善的监控和可视化工具,如使用Prometheus监控事件延迟、投影时长。

通过本实战示例,您可以快速上手Event Sourcing和CQRS在微服务中的落地,并在生产环境中规避常见坑,实现高可用、高性能的系统架构设计!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/diannao/92064.shtml
繁体地址,请注明出处:http://hk.pswp.cn/diannao/92064.shtml
英文地址,请注明出处:http://en.pswp.cn/diannao/92064.shtml

如若内容造成侵权/违法违规/事实不符,请联系英文站点网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

初级安全课第二次作业

&#xff08;一&#xff09;xss-labs 1~8关 1、前期准备 &#xff08;1&#xff09;打开小皮面板&#xff0c;并启动Apache和MySQL&#xff08;2&#xff09;将 xss-labs放到 phpstudy_pro 的 WWW 目录下&#xff08;3&#xff09;访问连接&#xff1a;http://localhost/xss-la…

从零搭建智能搜索代理:LangGraph + 实时搜索 + PDF导出完整项目实战

传统的AI聊天系统往往局限于预训练数据的知识范围&#xff0c;无法获取实时信息。本文将详细阐述如何构建一个基于LangGraph的智能代理系统&#xff0c;该系统能够智能判断何时需要进行网络搜索、有效维护对话上下文&#xff0c;并具备将对话内容导出为PDF文档的功能。 本系统…

C语言分支和循环语句——猜数字游戏

分支语句的语法形式1. if(表达式)语句;2. if(表达式)语句1;else语句2;3. Switch(表达式){ case 1: break;case 2: break;case 3: break; default: break; }循环语句的语法形式1. while(表达式)语句 ;2. for&#xff08;表达…

Python设计模式深度解析:原型模式(Prototype Pattern)完全指南

Python设计模式深度解析&#xff1a;原型模式&#xff08;Prototype Pattern&#xff09;完全指南前言什么是原型模式&#xff1f;模式的核心组成实际案例&#xff1a;游泳比赛管理系统游泳者数据结构原型模式的实现深拷贝 vs 浅拷贝&#xff1a;核心概念解析浅拷贝&#xff08…

SAP-ABAP:SAP万能长度计算:DYNAMIC_OUTPUT_LENGTH 深度解析

&#x1f4cf; SAP ABAP 万能长度计算&#xff1a;DYNAMIC_OUTPUT_LENGTH 深度解析核心作用&#xff1a;智能计算数据对象在列表/ALV中的实际显示宽度 | 关键优势&#xff1a;多字节字符处理 | 格式感知 | 动态适配&#x1f50d; 一、核心功能与技术特性 &#x1f4ca; 数据类型…

20250720-2-Kubernetes 调度-资源限制对Pod调度的影响(1)_笔记

一、创建一个Pod的工作流程&#xfeff;1. k8s架构解析&#xfeff;组件交互模式: Kubernetes采用list-watch机制的控制器架构&#xff0c;实现组件间交互的解耦。各组件通过监控自己负责的资源&#xff0c;当资源发生变化时由kube-apiserver通知相关组件。类比说明: 类似小卖铺…

mobaxteam x11传输界面避坑

mobaxteam x11传输界面避坑 文章目录mobaxteam x11传输界面避坑1 windows系统必须下载xing2 配置1 windows系统必须下载xing 因为windows系统本身没有x服务。 2 配置 如图

flink sql如何对hive string类型的时间戳进行排序

在 Flink SQL 中对 Hive 表的 STRING 类型时间戳进行排序&#xff0c;需要先将字符串转换为时间类型&#xff0c;再基于时间类型排序。以下是具体方法和示例&#xff1a; 一、核心解决方案 1. 字符串转 TIMESTAMP 后排序 若 Hive 中的时间戳格式为 yyyy-MM-dd HH:mm:ss&#xf…

Linux:线程控制

线程概念线程&#xff08;Thread&#xff09;是进程&#xff08;Process&#xff09; 中的一个执行单元&#xff0c;是操作系统能够进行运算调度的最小单位。线程也被称为“轻量级进程”&#xff08;Lightweight Process, LWP&#xff09;。一个进程可以包含多个线程&#xff0…

React 学习(4)

核心API———createRoot、render方法1.createRoot 方法是创建react的根容器&#xff0c;就是react元素的插入位置&#xff0c;插入的dom会被转化成react元素&#xff0c;根容器内的内容都会被react管理&#xff0c;原有dom都会被删除。react17 根容器创建、渲染方式&#xff0…

ASP .NET Core 8集成Swagger全攻略

Swagger (现在称为 OpenAPI) 是一个用于描述 RESTful API 的规范&#xff0c;ASP.NET Core 内置支持通过 Swashbuckle 库生成 Swagger 文档。以下是在 ASP.NET Core 8 中实现 Swagger 的完整步骤。1、添加Swagger NuGet 包dotnet add package Swashbuckle.AspNetCore2、添加Swa…

【iOS】源码阅读(六)——方法交换

文章目录方法交换什么是Method-Swizzling方法交换核心API**1. 获取方法对象****2. 添加/替换方法实现****3. 交换方法实现****4. 获取方法信息****5. 修改方法实现****使用示例&#xff1a;完整的 Method-Swizzling 流程****注意事项**使用方法交换注意事项线程安全方法交换的影…

mysql运维问题解决:MySQL主从延迟(锁阻塞与读写分离)

小亦平台会持续给大家科普一些运维过程中常见的问题解决案例&#xff0c;运维朋友们可以在常见问题及解决方案专栏查看更多案例 问题概述 告警事件&#xff1a; 2023-07-28 03:31:39.571 首次触发主从延迟告警&#xff08;延迟1515秒&#xff09;2023-07-28 07:41:37 告警解除…

SSH 密钥

什么是 SSH 密钥 SSH 密钥就像是你电脑的“身份证”和“钥匙”&#xff0c; 用来安全登录另一台电脑&#xff08;服务器&#xff09;&#xff0c;而不需要每次输入密码。SSH 密钥是一种安全登录远程服务器的方式&#xff0c;由一对加密的“钥匙”组成&#xff1a;一个公钥 一个…

st-Gcn训练跳绳识别模型一:数据标注工具和标注流程

目录 工具展示和使用说明 工具标注后文件展示说明 json转换成单个npy文件 数据获取补充 工具展示和使用说明 文件名labelV.py集于PySide6实现&#xff1a; 通过选择视频来选择你要标注的视频&#xff0c;然后选择保存路径&#xff1a; 然后视频两个类别。当你看见视频中的人…

springboot跨域问题 和 401

springboot跨域问题 和 401 1.跨域import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.web.servlet.FilterRegistrationBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotatio…

构建直播平台大体的流程

✅ 直播流程完整链路&#xff08;基于 SRS OBS 前后端&#xff09;&#x1f9cd;‍♂️ 用户操作流程&#xff1a;✅ 用户登录系统&#xff08;前端&#xff09;系统中校验用户身份&#xff08;JWT 等&#xff09;后端可能校验权限&#xff0c;比如“是否有开播资格”✅ 用户…

KOSMOS-2: 将多模态大型语言模型与世界对接

温馨提示&#xff1a; 本篇文章已同步至"AI专题精讲" KOSMOS-2: 将多模态大型语言模型与世界对接 摘要 我们介绍了 KOSMOS-2&#xff0c;一种多模态大型语言模型&#xff08;MLLM&#xff09;&#xff0c;赋予了模型感知物体描述&#xff08;例如&#xff0c;边界框…

协作机器人操作与编程-PE系统示教编程和脚本讲解(直播回放)

协作机器人操作与编程-PE系统示教编程和脚本讲解本次讲解主要围绕协作机器人PE系统的操作与编程展开&#xff0c;内容涵盖软件安装、虚拟机配置、手动操作、在线编程、变量设置、网络通信及标定方法等方面。以下是主要内容要点提炼&#xff1a; 软件安装与虚拟机配置 需从官网下…

【前后端】Node.js 模块大全

用到的全部总结在这里&#xff0c;不定期更新 链接 node一本通 包括&#xff1a; express path fs/ process/ os/ http/ mysql/mongoose/ express-jwt/jsonwebtoken/ dotenv/ multer/ swagger/ cors/ nodemon (docker篇有)常用模块 内置 fs 文件系统操作&#xff08;读写、重命…