图片

你是否也曾深陷在臃肿的领域模型(Domain Model)的泥潭,一个 User 或 Order 实体类,既要处理复杂的业务逻辑和数据校验,又要承载各种为前端展示而生的DTO转换,导致模型越来越胖,读写性能相互掣肘?是时候用CQRS(命令查询职责分离) 架构模式来解脱了!这是一种高级的架构模式,它将系统的数据更新操作(命令)和数据读取操作(查询)彻底分离,让它们可以使用各自最优的模型和技术栈。

在 Spring Boot 中,CQRS是构建高性能、高可扩展性复杂业务系统的终极武器。它能将你的系统清晰地划分为“指挥部”(处理命令)和“情报部”(响应查询),让两侧可以独立演进和优化。本文将探讨为什么“一个模型走天下”的传统CRUD会成为瓶颈,通过一个实际的电商商品服务示例来展示CQRS的强大威力,并一步步指导你如何在 Spring Boot 中实现它 —— 让我们今天就开始解锁更高级的系统架构之道吧!

什么是CQRS模式?🤔

CQRS(Command Query Responsibility Segregation)的核心思想是:将一个系统中改变状态的操作(命令)和读取状态的操作(查询)在模型层面进行分离。

这意味着,同一个业务概念(如“商品”)在系统内部会有两套完全不同的模型:

  • • 命令模型 (Write Model / Command Model): 用于处理所有的数据创建、更新和删除操作。这个模型通常是丰富的、包含复杂业务逻辑的领域模型(如JPA实体),并关注数据的一致性和验证。

  • • 查询模型 (Read Model / Query Model): 专用于数据读取和展示。这个模型通常是扁平化的、非规范化的“瘦”对象(如DTO),它被高度优化以满足前端页面的快速查询需求。

这两个模型之间通过某种机制(如事件、消息队列、数据库同步)进行数据同步。

这个模式的实现通常需要:

  • • 命令 (Command): 一个封装了修改系统状态意图的对象(如CreateProductCommand)。它不返回值。

  • • 查询 (Query): 一个封装了数据请求的对象(如GetProductByIdQuery)。它只返回数据,不修改任何状态。

  • • 命令处理器 (Command Handler): 接收并处理命令,与命令模型交互。

  • • 查询处理器 (Query Handler): 接收并处理查询,直接访问查询模型并返回数据。

为什么要在 Spring Boot 中使用CQRS模式?💡

CQRS能带来诸多架构上的好处:

  • • 性能与扩展性 (Performance & Scalability): 这是最核心的价值。你可以独立地对读、写两端进行优化和扩展。如果系统读多写少,你可以为查询端增加多个只读副本和缓存;如果写操作复杂,你可以为命令端配置更强大的服务器,二者互不影响。

  • • 优化的数据模型 (Optimized Data Models): 你可以为写入操作设计一个高度规范化的、保证数据一致性的模型;同时为读取操作设计一个或多个非规范化的、预先聚合好的“宽表”模型,免去复杂的关联查询。

  • • 简化的逻辑 (Simplified Logic): 命令端的代码只关心业务逻辑和状态变更,查询端的代码只关心如何最高效地拿数据。职责单一使得两边的代码都更容易理解和维护。

  • • 增强的安全性 (Enhanced Security): 查询模型和API天然就是只读的,没有任何方法可以修改数据,这从根本上杜绝了通过查询接口非法修改数据的风险。

  • • 技术栈灵活性 (Technology Flexibility): 你甚至可以为读写两端选择不同的数据库。例如,命令端使用关系型数据库(如MySQL)保证事务,查询端使用搜索引擎(如Elasticsearch)或文档数据库(如MongoDB)来提供高性能的复杂查询。

问题所在:不堪重负的CRUD模型

在传统的CRUD应用中,我们通常为“商品”定义一个Product实体类,它几乎无所不能:

@Entity
public class Product {@Id private Long id;@NotEmpty // 用于创建和更新时的校验private String name;@Positive // 校验private BigDecimal price;// 为业务逻辑而生,但在查询列表时通常不需要,可能导致N+1问题@ManyToOne(fetch = FetchType.LAZY) private Category category;@JsonIgnore // 为了在API中隐藏这个字段private String internalCode;// ... 大量getter/setter, 业务方法, toString...
}

这个Product实体既要负责写入时的验证和业务逻辑,又要负责读取时的JSON序列化。

❌ 模型臃肿: 一个类承担了过多的职责,变得难以理解和维护。
❌ 性能问题: 查询一个简单的列表可能也会触发懒加载,或者返回大量不必要的字段。更新时,一个简单的价格修改可能需要加载整个复杂的对象。
❌ 优化困难: 针对读和写的优化策略相互冲突,无法两全其美。

✅ CQRS模式来修复
CQRS将上述Product模型拆分为两个:

  1. 1. 命令模型: 一个完整的、包含校验和业务方法的Product实体,仅用于处理创建和更新命令。

  2. 2. 查询模型: 一个或多个简单的ProductDTO,仅包含页面展示所需的字段,仅用于处理查询。

一步步实现 Java 示例:银行账户操作

这是一个概念性的例子,展示了读写分离的思想。

第一步:定义命令、查询和模型

// 命令
class DepositMoneyCommand { double amount; /* ... */ }
// 查询
class GetAccountBalanceQuery { String accountId; /* ... */ }
// 写模型 (领域实体)
class BankAccount { private double balance; public void deposit(double amount) { this.balance += amount; } }
// 读模型 (DTO)
class AccountBalanceDTO { double balance; /* ... */ }

第二步:实现命令处理器和查询处理器

// 命令处理器 - 负责修改
class BankAccountCommandHandler {public void handle(DepositMoneyCommand command) {// 1. 加载写模型BankAccount account = repository.findById(command.getAccountId());// 2. 执行业务逻辑account.deposit(command.getAmount());// 3. 保存写模型repository.save(account);// 4. (可选) 发布事件,通知更新读模型}
}// 查询处理器 - 负责读取
class BankAccountQueryHandler {public AccountBalanceDTO handle(GetAccountBalanceQuery query) {// 直接从一个优化的读库(或视图)中查询,返回DTOreturn readDb.findBalance(query.getAccountId());}
}
Spring Boot 应用案例:CQRS化的商品服务

第一步:实现命令端 (Write Side)

// 命令对象
public record CreateProductCommand(String name, BigDecimal price) {}// JPA实体 (写模型)
@Entity public class Product { /* ... */ }// 命令处理器
@Service
public class ProductCommandHandler {private final ProductRepository productRepo;private final ApplicationEventPublisher eventPublisher;@Transactionalpublic void handle(CreateProductCommand command) {Product product = new Product(command.name(), command.price());productRepo.save(product);// 发布事件,用于更新读模型eventPublisher.publishEvent(new ProductCreatedEvent(this, product));}
}

第二步:实现查询端 (Read Side)

// DTO (读模型)
public record ProductDTO(Long id, String name) {}// 查询处理器
@Service
public class ProductQueryHandler {private final JdbcTemplate jdbcTemplate; // 使用JdbcTemplate直接查询,性能更高public List<ProductDTO> handleGetAllProducts() {return jdbcTemplate.query("SELECT id, name FROM product", (rs, rowNum) -> new ProductDTO(rs.getLong("id"), rs.getString("name")));}
}

第三步:在控制器中按职责分发

@RestController
@RequestMapping("/products")
public class ProductController {private final ProductCommandHandler commandHandler;private final ProductQueryHandler queryHandler;// 写操作 -> 调用命令处理器@PostMappingpublic void createProduct(@RequestBody CreateProductCommand command) {commandHandler.handle(command);}// 读操作 -> 调用查询处理器@GetMappingpublic List<ProductDTO> getAllProducts() {return queryHandler.handleGetAllProducts();}
}
CQRS 与事件溯源 (Event Sourcing)

这是一个天作之合,但两者并非绑定关系:

  • • CQRS: 是关于分离读写模型的架构模式。

  • • 事件溯源 (Event Sourcing): 是一种持久化技术。它不保存对象的最终状态,而是保存导致该状态的所有事件序列
    CQRS 的查询端(读模型)可以完美地通过监听事件溯源产生的事件流,来构建和维护自己所需的、高度优化的数据视图。

✅ 何时使用CQRS模式
  • • 当你的应用读写负载差异巨大,需要独立扩展时(例如,内容平台读多写少)。

  • • 当读操作和写操作的业务模型差异巨大时。

  • • 在需要极高性能和低延迟的查询场景下。

  • • 在一个高度协作的领域,多个用户同时操作可能导致数据冲突时。

  • • 当你计划使用事件溯源时。

🚫 何时不宜使用CQRS模式
  • • 对于简单的CRUD应用: CQRS会引入不必要的复杂性,是典型的高射炮打蚊子。

  • • 当业务领域很简单,读写模型几乎没有差异时。

  • • 当团队对更高级的架构模式不熟悉时,可能会增加维护成本。

🏁 总结

CQRS 不是一个具体的“设计模式”,而是一种更宏观的“架构模式”。它通过将应用的读写职责进行彻底分离,为解决复杂业务场景下的性能、扩展性和可维护性问题提供了一把锋利的“手术刀”。

在现代化的 Spring Boot 开发中,借助 Spring Data、内置事件机制和强大的依赖注入,我们拥有了实现CQRS所需的所有工具。有意识地运用CQRS思想来设计你的复杂服务,将帮助你:

  • • 构建出真正高性能、高可用的系统

  • • 让读写两端的模型和代码都更加纯粹

  • • 从容应对未来的业务增长和技术演进

理解CQRS的本质,并审慎地在正确的场景下应用它,是每一位从普通开发者迈向资深架构师的必经之路。

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

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

相关文章

UE5 Rotate 3 Axis In One Material

首先没有用旋转矩阵&#xff0c;我用过旋转矩阵&#xff0c;传进去的角度旋转的角度和欧拉角传进去角度旋转出来的不一样&#xff0c;就没有用最后用的RotateAboutAxis&#xff0c;这个玩意儿研究老半天&#xff0c;只能转一个轴&#xff0c;角度和欧拉角的一样的最后研究出Rot…

计算机网络实验——访问H3C网络设备

一、实验目的1. 熟悉H3C路由器的开机界面&#xff1b;2. 通过Console端口实现对上电的H3C路由器的第一次本地访问&#xff1b;3. 掌握H3C设备命名等几个常用指令&#xff1b;4. 掌握如何将H3C设备配置为Telnet服务器&#xff1b;5. 掌握如何将H3C设备配置为Telnet客户端并实现访…

【C语言】学习过程教训与经验杂谈:思想准备、知识回顾(四)

&#x1f525;个人主页&#xff1a;艾莉丝努力练剑 ❄专栏传送门&#xff1a;《C语言》、《数据结构与算法》、C语言刷题12天IO强训、LeetCode代码强化刷题 &#x1f349;学习方向&#xff1a;C/C方向 ⭐️人生格言&#xff1a;为天地立心&#xff0c;为生民立命&#xff0c;为…

Vim 指令

Vim 是一款功能强大但学习曲线陡峭的文本编辑器&#xff0c;核心在于其模式化操作。掌握常用指令能极大提升效率。以下是指令分类整理&#xff1a;一、核心模式切换 (必须掌握&#xff01;)i&#xff1a;在光标前进入 插入模式 (Insert Mode)a&#xff1a;在光标后进入 插入模式…

vue2中使用xgplayer播放流视频

1、官网 2、安装后无法播放时&#xff0c;经测试&#xff0c;需要降低版本 "xgplayer-hls": "2.2.2","xgplayer": "2.31.6"改为以上版本可以正常播放 3、完整使用 &#xff08;1&#xff09;引入 import xgplayer import hlsjsPlayer…

Jmeter进阶篇(35)完美解决Jmeter转换HTML报告报错“Begin size 0 is not equal to fixed size 5”

今天博主在使用Jmeter运行完压测,使用生成的csv文件,运行以下命令: C:\apache-jmeter-5.2.1\bin>jmeter -g C:\res.csv -o C:\report生成HTML报告时,发现报错“Begin size 0 is not equal to fixed size 5”。 问题原因 原因是我:本地用的是JDK17,但Jmeter5.2.1仅支…

linux中tcpdump抓包中有组播数据,应用程序收不到数据问题

问题描述服务器运行正常&#xff0c;维保需要&#xff0c;重启服务器后应用程序无法收到组播的媒体数据。百思不得其解。原因分析最终的定位原因是 linux系统的自我保护机制导致的。rp_filter&#xff08;反向路径过滤&#xff09;是Linux内核的一个安全特性&#xff0c;用于防…

人工智能-基础篇-29-什么是低代码平台?

低代码平台&#xff08;Low-Code Development Platform, LCDP&#xff09;是一种通过可视化界面和少量代码&#xff08;或无需代码&#xff09;快速构建应用程序的开发工具。它的核心目标是通过简化开发流程&#xff0c;降低技术门槛&#xff0c;使企业能够更高效地响应业务需求…

PyTorch随机擦除:提升模型抗遮挡能力

PyTorch中内置的随机擦除&#xff08;Random Erasing&#xff09;数据增强通过torchvision.transforms.RandomErasing实现&#xff0c;以下是原理和用法的详细说明&#xff1a;核心原理正则化作用&#xff1a; 随机擦除在训练图像上随机遮盖一个矩形区域&#xff0c;模拟遮挡场…

微信小程序交互精髓:点击操作与状态管理实战

目录 一、点击事件绑定&#xff1a;bindtap 与 catchtap 的正确使用 基础语法对比 事件对象详解 二、点击切换选中状态&#xff1a;数据驱动视图的实现 1. 单元素状态切换 2. 多元素单选状态 3. 多元素多选状态 三、样式动态切换&#xff1a;数据绑定与 CSS 的完美结合 …

Language Models are Few-Shot Learners: 开箱即用的GPT-3(二)

接上一篇 Approach 前面的摘要和Introduction做了一些概要性的介绍,论文在第二章,也就是approach中,介绍了模型的设计,zero,one,few-shot的设计等等。 这一章一开头就说,GPT-3的结构和GPT-2的结构一样,只是在相应的把模型尺寸,数据规模,训练时间等增加了。Our bas…

【养老机器人】核心技术

1. 毫米波雷达如何检测心跳和呼吸&#xff1f;毫米波雷达&#xff08;通常工作在60GHz或77GHz频段&#xff09;可以探测到人体胸腔的微米级位移&#xff0c;而心跳和呼吸会引起胸腔的周期性运动&#xff1a;呼吸&#xff1a;幅度较大&#xff08;约5-10毫米&#xff09;&#x…

二 Javascript 入门

我们 从已经知道了 Javascript的历史以及什么是Javascript&#xff0c;那实际编写的时候在哪里编写&#xff1f; script 标签 HTML 为我们提供了无数的标签来做无数的事情。例如&#xff0c; 用于为段落添加边距&#xff0c; 用于使文本加粗&#xff0c; 用于在网页上嵌入音…

《信息技术服务监理 第5部分:软件工程监理规范》(GB/T 19668.5-2018)标准解读

《信息技术服务监理 第 5 部分&#xff1a;软件工程监理规范》&#xff08;GB/T 19668.5-2018&#xff09;是规范软件工程监理服务的国家标准&#xff0c;旨在为软件工程监理的规划设计、招标、设计、实施、验收等阶段及相关支持过程提供明确的监理要求、服务内容和实施要点。 …

RedisJSON 路径语法深度解析与实战

一、两种路径语法概览语法类型触发标志简介JSONPath以 $ 开头全功能路径&#xff0c;支持递归 (..)、通配符 (*)、切片 ([start:end:step])、过滤 (?())、脚本表达式等Legacy以 . 或键名开头早期版本&#xff08;v1&#xff09;遗留语法&#xff0c;只支持简单的点式和中括号&…

从Rust模块化探索到DLB 2.0实践|得物技术

一、前言在云原生架构高速迭代的背景下&#xff0c;基础设施的性能瓶颈与安全隐患成为技术演进的关键挑战。本文系统记录了团队基于Rust语言改造Nginx组件的完整技术路径&#xff1a;从接触Cloudflare的quiche库&#xff0c;引发对Rust安全特性的探索&#xff0c;到通过FFI实现…

【 MySQL】一点点相关的记录

打开 MySQL Workbench 并连接到你的数据库在 MySQL Connections 下&#xff0c;选择连接的数据库实例&#xff08; Local instance MySQL80&#xff09;登录时输入 用户名 和 密码。 root&#xff0c;密码是在 MySQL 安装时设置的密码创建新数据库登录后&#xff0c;在 MySQL W…

旅游企业如何通过数字化转型实现高效运营

在旅游行业竞争日益激烈、游客需求日趋多样的当下&#xff0c;数字化管理成为旅游企业提升竞争力的关键协同办公系统以其丰富功能与灵活特性&#xff0c;为旅游行业带来全新的数字化变革&#xff0c;助力企业高效运营。优化行程规划与调度旅游行程的规划与调度繁杂且关键。协同…

大数据Spark(六十二):Spark基于Yarn提交任务流程

文章目录 Spark基于Yarn提交任务流程 一、Yarn-Client模式 1、提交命令 2、任务执行流程 二、Yarn-Cluster模式 1、提交命令 2、任务执行流程 Spark基于Yarn提交任务流程 在Yarn模式下&#xff0c;Spark的任务提交同样根据Driver程序运行的位置不同&#xff0c;分为cli…

Docker 高级管理-容器通信技术与数据持久化

(1)创建一个叫 my-net 的 bridge 类型的网络(2)查看都有哪些网络(3)运行一个容器井连接到新建的 my-net 网络(4)运行一个容器井加入到 my-net 网络2:Host 模式由于使用了 Host 模式&#xff0c;容器会直接使用宿主机的网络端口&#xff0c;因此可以直接在宿主机上通过 localhos…