為什么需要CQRS,它能解決什么問題?
為什么需要CQRS?
圖片
在領(lǐng)域驅(qū)動(dòng)設(shè)計(jì)(DDD)中,業(yè)務(wù)邏輯的基本處理流程通常如下:接口層接收業(yè)務(wù)請(qǐng)求,進(jìn)行參數(shù)校驗(yàn)后,調(diào)用應(yīng)用服務(wù)執(zhí)行業(yè)務(wù)編排。在應(yīng)用服務(wù)中,加載聚合根,接著由領(lǐng)域?qū)ο筇幚順I(yè)務(wù)邏輯,最后通過基礎(chǔ)設(shè)施層更新領(lǐng)域?qū)ο蟆?/p>
然而,在實(shí)際開發(fā)中,我們經(jīng)常遇到一些復(fù)雜的查詢需求,比如分頁查詢、非業(yè)務(wù)標(biāo)識(shí)符的條件查詢以及多表關(guān)聯(lián)查詢。這些需求往往涉及到多個(gè)聚合根,并且在查詢時(shí)不一定需要加載完整的聚合根。
例如,在之前的章節(jié)中,我通過擴(kuò)展倉儲(chǔ)接口來支持條件查詢,如訂單服務(wù)的倉儲(chǔ)接口定義:
public interface OrderRepository extends Repository<TradeOrder, OrderId> {
/**
* 根據(jù)訂單編號(hào)查詢訂單
* @param orderSn 訂單號(hào)
* @return 訂單聚合
*/
TradeOrder findOrderByTransaction(String customerId);
/**
* 修改訂單狀態(tài)
* @author jam
* @date 2023/12/19 9:07
* @param orderSn 訂單編號(hào)
* @param status 訂單狀態(tài)
*/
void changeStatus(String orderSn, OrderStatusEnum status);
/**
* 分頁查詢
* @param customerId 用戶ID
* @param pageRequest 分頁請(qǐng)求體
*/
PageResponse<TradeOrder> pageQuery(Long customerId, PageRequest pageRequest);
}
這個(gè)設(shè)計(jì)存在一定問題:倉儲(chǔ)接口的職責(zé)變得不再單一。根據(jù)DDD的設(shè)計(jì)理念,Repository主要負(fù)責(zé)維護(hù)聚合根的生命周期,然而在這里,它同時(shí)承擔(dān)了分頁查詢職能,這與其單一職責(zé)原則相悖。每當(dāng)我們需要新增查詢功能時(shí),都需要在領(lǐng)域?qū)拥膫}儲(chǔ)接口中增加新方法,導(dǎo)致接口變得越來越復(fù)雜。
為了保持倉儲(chǔ)接口的單一職責(zé),我們需要將查詢操作與聚合根的生命周期管理分離。CQRS(命令查詢職責(zé)分離) 就是為了解決這個(gè)問題。
查詢與聚合根的關(guān)系
聚合根代表了事務(wù)的一致性邊界,倉儲(chǔ)接口需要確保在加載時(shí)獲取聚合根的完整狀態(tài)以保證數(shù)據(jù)的準(zhǔn)確性。然而,許多查詢操作,如分頁查詢和條件查詢,往往只需要讀取聚合根的一部分?jǐn)?shù)據(jù),而不需要修改它的狀態(tài)。在這種情況下,加載整個(gè)聚合根的狀態(tài)不僅會(huì)導(dǎo)致不必要的性能開銷,還可能使查詢變得更復(fù)雜和低效。
因此,在只查詢而不修改的場(chǎng)景下,其實(shí)沒必要完整的加載聚合根。接下來,我們將引入CQRS來解決這個(gè)問題。
什么是CQRS?
CQRS(Command Query Responsibility Segregation,命令查詢職責(zé)分離)是一種架構(gòu)模式,它通過將修改操作(命令,Command)與查詢操作(查詢,Query)分開,使用不同的模型來分別處理這兩類操作,從而實(shí)現(xiàn)命令與查詢的分離。
在領(lǐng)域驅(qū)動(dòng)設(shè)計(jì)(DDD)中引入CQRS后,應(yīng)用層的職責(zé)被明確分為兩個(gè)部分:
- 命令應(yīng)用服務(wù)(Command Application Service):負(fù)責(zé)處理寫操作,如創(chuàng)建、更新和刪除。
- 查詢應(yīng)用服務(wù)(Query Application Service):負(fù)責(zé)處理讀操作,包括數(shù)據(jù)查詢和展示。
引入CQRS后,命令和查詢操作在應(yīng)用層使用不同的模型進(jìn)行處理:
- 在命令應(yīng)用服務(wù)中,依舊使用領(lǐng)域模型來執(zhí)行業(yè)務(wù)操作。通過倉儲(chǔ)(Repository)加載完整的聚合根,并由聚合根修改其內(nèi)部狀態(tài)來實(shí)現(xiàn)業(yè)務(wù)邏輯。
- 在查詢應(yīng)用服務(wù)中,使用專門的數(shù)據(jù)模型來處理查詢操作。這些數(shù)據(jù)模型直接從數(shù)據(jù)庫讀取數(shù)據(jù),并將結(jié)果展示給用戶。查詢操作不涉及領(lǐng)域邏輯,只關(guān)注高效的數(shù)據(jù)檢索和展示,可以直接使用基礎(chǔ)設(shè)施層的額數(shù)據(jù)模型和ORM接口來完成操作。
實(shí)際上,CQRS并非DDD獨(dú)有的概念,無論是否使用領(lǐng)域驅(qū)動(dòng)設(shè)計(jì),都會(huì)推薦采用CQRS架構(gòu)。具體而言,在三層架構(gòu)中,可以將Service層拆分為不同職責(zé)的模型;在DDD的四層架構(gòu)中,則將應(yīng)用服務(wù)(Application Service)拆分為命令服務(wù)和查詢服務(wù)。
CQRS的實(shí)現(xiàn)
CQRS的實(shí)現(xiàn)通常分為相同數(shù)據(jù)源模式和異構(gòu)數(shù)據(jù)源模式,兩者適用于不同的業(yè)務(wù)場(chǎng)景。
相同數(shù)據(jù)源的CQRS
在這種模式下,命令服務(wù)和查詢服務(wù)共用同一套數(shù)據(jù)源。命令操作通過領(lǐng)域模型完成,查詢操作則通過數(shù)據(jù)模型實(shí)現(xiàn)。由于數(shù)據(jù)源相同,CQRS的實(shí)現(xiàn)相對(duì)簡(jiǎn)單,且能夠滿足大部分業(yè)務(wù)場(chǎng)景需求。
如下圖所示:
圖片
異構(gòu)數(shù)據(jù)源的CQRS
雖然相同數(shù)據(jù)源模式可以滿足大多數(shù)業(yè)務(wù)需求,但在某些場(chǎng)景下,為了優(yōu)化性能、解決特定問題,可能會(huì)引入其他數(shù)據(jù)存儲(chǔ)中間件,將業(yè)務(wù)數(shù)據(jù)的副本存儲(chǔ)在新的數(shù)據(jù)源中,從而形成異構(gòu)數(shù)據(jù)源。這時(shí),命令操作和查詢操作將分別由不同的數(shù)據(jù)源承接。
示例:
以訂單查詢?yōu)槔?,為了提高查詢性能,訂單領(lǐng)域在創(chuàng)建訂單后,可以通過 binlog 將 MySQL 數(shù)據(jù)同步到 Elasticsearch,查詢操作則直接從 Elasticsearch 獲取數(shù)據(jù)。這就是典型的異構(gòu)數(shù)據(jù)源 CQRS 模式。
圖片
注意:異構(gòu)數(shù)據(jù)源不一定是兩種不同的數(shù)據(jù)中間件。例如,即使兩個(gè)數(shù)據(jù)源都是 MySQL,只要表結(jié)構(gòu)不同,也可以被視為異構(gòu)數(shù)據(jù)源。
部署方式
在實(shí)際應(yīng)用中,CQRS 架構(gòu)可以根據(jù)項(xiàng)目需求靈活部署:
- 同一應(yīng)用內(nèi)實(shí)現(xiàn):命令服務(wù)和查詢服務(wù)共存于同一個(gè)應(yīng)用中,適用于簡(jiǎn)單場(chǎng)景。
- 物理隔離部署:將命令服務(wù)和查詢服務(wù)拆分為不同的應(yīng)用,獨(dú)立部署,適用于高并發(fā)、大規(guī)模業(yè)務(wù)場(chǎng)景。
在Dailymart改造CQRS模式
以訂單模塊為例,看看如何實(shí)踐CQRS模式,以下為實(shí)踐步驟:
1、拆分應(yīng)用服務(wù)
將原應(yīng)用服務(wù)接口OrderService拆成兩個(gè)服務(wù),分別是OrderCommandService 和 OrderQueryService,將分頁接口定義遷移到OrderQueryService中,OrderCommandService 中只包含聚合的加載和更新操作。
public interface OrderQueryService {
/**
* 分頁查詢
* @author jam
* @date 2024/12/17 14:56
*/
PageResponse<OrderRespDTO> findListByUserId(OrderPageQueryDTO pageRequest);
}
public interface OrderCommandService {
/**
* 創(chuàng)建訂單
* @param orderCreateRequest 創(chuàng)建訂單參數(shù)
*/
String createOrder(OrderCreateRequest orderCreateRequest);
/**
* 訂單發(fā)貨
*/
String ship(String orderSn);
/**
* 加載訂單詳情
*/
OrderRespDTO getOrderBySn(String orderSn);
}
2、實(shí)現(xiàn)查詢服務(wù): 使用 MyBatis-Plus 進(jìn)行分頁查詢并轉(zhuǎn)換 DO 到 DTO
在查詢服務(wù)中,我們直接使用 MyBatis-Plus 提供的 selectPage 方法進(jìn)行分頁查詢,并通過 OrikaUtils.convertList 方法將數(shù)據(jù)庫對(duì)象DO轉(zhuǎn)換為DTO。
@Service
@Slf4j
public class OrderQueryServiceImpl implements OrderQueryService {
@Resource
private OrderMapper orderMapper;
@Override
public PageResponse<OrderRespDTO> findListByUserId(OrderPageQueryDTO pageRequest) {
Page<OrderDO> page = new Page<>(pageRequest.getCurrent(), pageRequest.getSize());
LambdaQueryWrapper<OrderDO> queryWrapper = Wrappers.lambdaQuery(OrderDO.class).eq(OrderDO::getCustomerId, pageRequest.getCustomerId());
Page<OrderDO> selectedPage = orderMapper.selectPage(page, queryWrapper);
List<OrderDO> records = selectedPage.getRecords();
Map<String,String> refMap = new HashMap<>(1);
//map key 放置 源屬性,value 放置 目標(biāo)屬性
refMap.put("orderId","id");
//Do -> Dto
List<OrderRespDTO> pageList = OrikaUtils.convertList(records, OrderRespDTO.class,refMap);
return new PageResponse<>(pageRequest.getCurrent(), pageRequest.getSize(), selectedPage.getTotal(), pageList);
}
}
3、刪除倉儲(chǔ)接口中關(guān)于分頁查詢的接口
Repository的職責(zé)應(yīng)集中在持久化和聚合的加載上。分頁查詢不應(yīng)包含在倉儲(chǔ)接口中。通過移除分頁查詢方法來簡(jiǎn)化倉儲(chǔ)接口的設(shè)計(jì),使其專注于聚合根的生命周期管理。
public interface OrderRepository extends Repository<TradeOrder, OrderId> {
/**
* 根據(jù)訂單編號(hào)查詢訂單
* @param orderSn 訂單號(hào)
* @return 訂單聚合
*/
TradeOrder load(String orderSn);
}
4、修改訂單接口層的調(diào)用方式,分別使用不同的應(yīng)用服務(wù)完成業(yè)務(wù)操作。
@RestController
@RequiredArgsConstructor(onConstructor = @__(@Autowired))
@Tag(name = "OrderController", description = "C端訂單模塊")
public class OrderController {
private final OrderCommandService orderCommandService;
private final OrderQueryService orderQueryService;
...
}
小結(jié)
本文詳細(xì)介紹了CQRS模式的基本概念及其實(shí)現(xiàn)方式,重點(diǎn)分析了在DailyMart項(xiàng)目中如何通過實(shí)踐CQRS架構(gòu)對(duì)訂單模塊進(jìn)行改造。希望通過本文的講解,能夠幫助你更好地理解CQRS模式的應(yīng)用場(chǎng)景、優(yōu)勢(shì)及實(shí)施細(xì)節(jié),提升系統(tǒng)架構(gòu)的可維護(hù)性和擴(kuò)展性。