讀取QPS 10萬,寫入QPS 1000,如何設計系統架構?

圖片

你是否也曾深陷在臃腫的領域模型(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…