この記事は、最初のインターネット技術の生体内マイクロチャネル公共番号に登場し
たリンク:https://mp.weixin.qq.com/s/Z3uJhxJGDif3qN5OlE_woA
著者:wenbo張
[Road of Domain Driven Design Practice]過去の一連のすばらしい記事:
「Domain Driven Design(DDD)Practice Road(1)」では、主に戦略レベルでのDDD原則について説明します。
これは、「ドメイン主導の設計手法のロード」シリーズの2番目の記事で、イベントを使用してソフトウェアコアの複雑さを分離する方法を分析します。CQRSがDDDプロジェクトで広く使用されている理由と、CQRSフレームワークを現場で実装する方法について説明します。もちろん、失敗の教訓にも注意を払い、長所と短所を踏まえて適切な対応を選択する必要があります。
1.はじめに:ロジスティクスの詳細から始める
誰もがロジスティクストラッキングに精通しており、何がいつ何が起こったかを詳細に記録し、データは重要なドキュメントとして不変です。価値の背後にはいくつかの側面があることを理解しています。ビジネスパーティは各サブプロセスを制御して、それが現在どこにあるかを知ることができます。一方、トレースバックする必要がある場合、履歴プロセス全体は、各ステップを記録することによってのみ再生できます。
前回の記事で、「ソフトウェアプロジェクトも人間の社会的生産関係のカテゴリーですが、私たちが作成した労働の成果は目に見えません」と提案しました。したがって、ロジスティクス追跡のアイデアを使用して、ソフトウェアプロジェクトを開発し、複雑なプロセスを個々のステップ、サブプロセス、および状態に分解できます。これは、イベント分割と一致しています。これは、イベント駆動型の典型的なケースです。
2.ドメインイベント
ドメインイベント(ドメインイベント)は、ドメイン駆動設計(DDD)の概念であり、モデリングしているドメインで発生したことをキャプチャするために使用されます。
ドメインイベント自体もユニバーサル言語(ユビキタス言語)の一部であり、ドメインの専門家を含むすべてのプロジェクトメンバーのコミュニケーションの言語になります。
たとえば、前述の越境ロジスティクスの例では、商品が保税倉庫に到着した後、スタッフに仕分けと外注を割り当てる必要があり、「商品が保税倉庫に到着した」とはエリアイベントです。
まず、ビジネスロジックの観点から、このイベントはプロセス全体の成功または失敗に関連しています。同時に、後続のサブプロセスをトリガーします。また、ビジネス側にとって、このイベントは画期的なマイルストーンでもあります。つまり、商品自体が配送されます。自分の手で。
したがって、一般的に言えば、ドメインイベントには次の特性があります。より高いビジネス価値。完全なビジネスクローズドループを形成するのに役立ち、さらなるビジネスオペレーションにつながります。ここで、ドメインイベントには明確な境界があることも強調する必要があります。
例:レストランのチェックアウトシステムをモデル化している場合、顧客が到着したときにすぐに相手にお金を要求することができず、「顧客が注文した」ため、「顧客が到着した」ことは心配の対象ではありません。 「チェックアウトシステムに役立つイベントです。
1.モデリング分野のイベント
フィールドでイベントをモデル化する場合、境界のコンテキストで一般的な言語に従ってイベントと属性に名前を付ける必要があります。イベントがアグリゲートのコマンド操作によって生成される場合、通常、操作メソッドの名前に従ってドメインイベントに名前を付けます。
上記の例「保税倉庫に商品が到着しました」の場合、対応するドメインイベントを公開します
GoodsArrivedBondedWarehouseEvent(もちろん、明確な境界のコンテキストで集計の名前を削除し、それを直接命名規則であるArrivedBondedWarehouseEventとしてモデル化することもできます)。
イベントの名前は、集約でコマンドメソッドを実行した後に何が起こったかを示します。つまり、保留中のアイテムと不確定状態は、ドメインイベントと見なすことができません。
効果的な方法は、操作前および発生した状態変化を含む、現在のビジネスの状態フロー図を描画することです。ここでの式は、変更された状態であるため、削除やキャンセルなどの過去形を表す必要はありません。削除またはキャンセルされました。
次に、ノードのイベントをモデル化します。次の図は、ファイルクラウドストレージビジネスを示しています。事前アップロード、アップロード完了の確認、および削除のために、「過去形」のイベントであるPreUploadedEvent、ConfirmUploadedEvent、およびRemovedEventをモデル化します。
2.ドメインイベントコードの解釈
package domain.event;
import java.util.Date;
import java.util.UUID;
/**
* @Description:
* @Author: zhangwenbo
* @Since: 2019/3/6
*/
public class DomainEvent {
/**
* 领域事件还包含了唯一ID,
* 但是该ID并不是实体(Entity)层面的ID概念,
* 而是主要用于事件追溯和日志。
* 如果是数据库存储,该字段通常为唯一索引。
*/
private final String id;
/**
* 创建时间用于追溯,另一方面不管使用了
* 哪种事件存储都有可能遇到事件延迟,
* 我们通过创建时间能够确保其发生顺序。
*/
private final Date occurredOn;
public DomainEvent() {
this.id = String.valueOf(UUID.randomUUID());
this.occurredOn = new Date();
}
}
ドメインイベントを作成する場合、2つの注意点があります。
-
ドメインイベント自体は不変である必要があります(不変)。
- ドメインイベントには、イベントに関連するコンテキストデータが含まれている必要がありますが、集計ルート全体の状態データは含まれていません。たとえば、注文を作成するとき、注文の基本情報を運ぶことができ、ユーザーが注文受信アドレスイベントAddressUpdatedEventを更新するには、注文、ユーザー、および新しい住所のみを含める必要があります。
public class AddressUpdatedEvent extends DomainEvent {
//通过userId+orderId来校验订单的合法性;
private String userId;
private String orderId;
//新的地址
private Address address;
//略去具体业务逻辑
}
3.ドメインイベントの保存
イベントの不変性と追跡可能性は、永続化する必要があるという原則を決定します。いくつかの一般的な解決策を見てみましょう。
3.1別個のEventStore
一部のビジネスシナリオでは、Mysql、Redis、Mongo、またはファイルストレージでさえある別個のイベントストレージセンターが作成されます。ここでは、Mysqlを例にとります。Business_codeとevent_codeは、さまざまなビジネスのさまざまなイベントを区別するために使用されます。具体的な命名規則は、実際のニーズに基づくことができます。
ここでは、データソースがビジネスデータソースと一致しないシナリオに注意を払う必要があります。ビジネスデータが更新されたときに、イベントが正確に記録されるようにする必要があります。実際には、分散トランザクションの使用を避けるか、そのクロスデータベースシナリオを避けてください。それ以外の場合は、あなたはどのように補償するかを考えなければなりません。ユーザーが配信アドレスを更新したことは避けなければなりませんが、AddressUpdatedEventは保存に失敗しました。
一般的な原則は、分散トランザクションの場合はノーと言います。いずれにせよ、問題よりも方法の方が多いと思います。実際には、常に解決策を考えることができます。違いは、解決策が簡潔でデカップリングであるかどうかです。
# 考虑是否需要分表,事件存储建议逻辑简单
CREATE TABLE `event_store` (
`event_id` int(11) NOT NULL auto increment,
`event_code` varchar(32) NOT NULL,
`event_name` varchar(64) NOT NULL,
`event_body` varchar(4096) NOT NULL,
`occurred_on` datetime NOT NULL,
`business_code` varchar(128) NOT NULL,
UNIQUE KEY (`event id`)
) ENGINE=InnoDB COMMENT '事件存储表';
3.2ビジネスデータと一緒に保存する
分散アーキテクチャでは、各モジュールは比較的小さく、正確に「自律」です。現在のビジネスデータの量が少ない場合は、イベントをビジネスデータと共に保存し、関連する識別子を使用して、それが実際のビジネスデータであるかイベントレコードであるかを区別できます。または、現在のビジネスデータベースでビジネス独自のイベントストレージを確立しますが、イベントストレージを検討してください。規模は必然的に実際のビジネスデータよりも大きくなります。テーブルを分割する必要があるかどうかを検討してください。
このスキームの利点:データの自律性、分散トランザクションの回避、追加のイベントストレージセンターは不要です。もちろん、その欠点は再利用できないことです。
4.ドメインイベントを解放する方法
4.1ドメインイベントはドメイン集約によって送信されます
/*
* 一个关于比赛的充血模型例子
* 贫血模型会构造一个MatchService,我们这里通过模型来触发相应的事件
* 本例中略去了具体的业务细节
*/
public class Match {
public void start() {
//构造Event....
MatchEvent matchStartedEvent = new MatchStartedEvent();
//略去具体业务逻辑
DefaultDomainEventBus.publish(matchStartedEvent);
}
public void finish() {
//构造Event....
MatchEvent matchFinishedEvent = new MatchFinishedEvent();
//略去具体业务逻辑
DefaultDomainEventBus.publish(matchFinishedEvent);
}
//略去Match对象基本属性
}
4.2イベントバスVSメッセージミドルウェア
マイクロサービス内のドメインイベントは、イベントバスを通じて、またはアプリケーションサービスを使用して、異なる集約間のビジネスコラボレーションを実現できます。つまり、ドメインイベントがマイクロサービスで発生すると、イベントの統合のほとんどが同じスレッドで発生するため、メッセージミドルウェアを導入する必要がありません。ただし、イベントが複数の集約データを同時に更新する場合、DDDの原則「1つのトランザクションは1つの集約ルートのみを更新する」により、非同期メソッドを通じてマイクロサービスの異なる集約ルートに異なるトランザクションを採用するメッセージミドルウェアの導入を検討できます。
三、佐賀分散トランザクション
1、Saga概要
Sagaモードを使用してデータの一貫性を維持する方法を見てみましょう。
Sagaは、マイクロサービスアーキテクチャでデータの一貫性を維持するためのメカニズムであり、分散トランザクションによって引き起こされる問題を回避できます。
Sagaは、更新が必要な複数のサービスの1つを表します。つまり、Sagaは一連のローカルトランザクションで構成されます。各ローカルトランザクションは、サービスのプライベートデータベースの更新を担当しますが、これらの操作は、私たちが使い慣れているACIDトランザクションフレームワークと関数ライブラリに依存しています。
モード:佐賀
非同期メッセージを使用して一連のローカルトランザクションを調整することにより、複数のサービス間のデータの一貫性を維持します。
佐賀はTCCのTryオペレーションよりも1ステップ少ないため、最終的なトランザクションが成功したか失敗したかに関係なく、TCCはトランザクションの参加者と2回対話する必要があります。Sagaは、トランザクションが成功したときにトランザクション参加者と一度だけ対話する必要があります。トランザクションが失敗した場合、追加の補償ロールバックが必要です。
-
各佐賀は一連のサブトランザクションTiで構成されています。
- 各Tiには対応する補正アクションCiがあり、これはTiによる結果をキャンセルするために使用されます。
ご覧のとおり、TCCと比較して、佐賀には「予約済み」のアクションがなく、そのTiはライブラリに直接送信されます。
Sagaには2つの実行順序があります。
-
成功:T1、T2、T3、...、Tn;
- 失敗:T1、T2、...、Tj、Cj、...、C2、C1、其中0 <j <n;
佐賀県の廃止は非常に重要であることがわかりますが、佐賀県を利用する難しさは、ロールバック戦略の設計方法にあると言えます。
2.佐賀県の実施
上記の例を通して、私たちは佐賀に予備的な感覚を持っています、それではそれを深く達成する方法を議論しましょう。システムコマンドを使用して佐賀を開始する場合、調整ロジックは、ローカルトランザクションを実行する最初の佐賀参加者を選択して通知する必要があります。トランザクションが完了すると、佐賀は選択を調整し、次の佐賀参加者を呼び出します。
このプロセスは、佐賀がすべてのステップを完了するまで続きます。ローカルトランザクションが失敗した場合、佐賀県は逆の順序で補償トランザクションを実行する必要があります。以下のさまざまなメソッドを使用して、佐賀の調整ロジックを構築できます。
2.1コラボレーティブ(振付)
佐賀県の意思決定と実行のシーケンスロジックは、佐賀県の各参加者に分散され、イベントを交換することでコミュニケーションを図ります。
(「マイクロサービスアーキテクチャのデザインパターン」の関連する章から引用)
-
OrderサービスはOrderを作成し、OrderCreatedイベントを発行します。
-
コンシューマーサービスはOrderCreatedイベントを消費し、コンシューマーが注文できるかどうかを確認して、ConsumerVerifiedイベントを発行します。
-
KitchenサービスはOrderCreatedイベントを使用し、注文を確認し、CREATE_PENDING状態でトラブルチケットを作成して、TicketCreatedイベントを発行します。
-
アカウンティングサービスはOrderCreatedイベントを消費し、PENDING状態のクレジットカード承認を作成します。
-
アカウンティングサービスは、TicketCreatedイベントとConsumerVerifiedイベントを消費し、消費者のクレジットカードに請求し、クレジットカード承認失敗イベントを発行します。
-
キッチンサービスは、クレジットカード認証失敗イベントを使用して、トラブルチケットのステータスをREJECTEDに変更します。
- サービス消費クレジットカード認証失敗イベントを注文し、注文ステータスを拒否に変更します。
2.2オーケストレーション
佐賀の決定および実行シーケンスロジックを佐賀オーケストレータークラスに集中させます。佐賀オーケストラは、各佐賀参加者に命令メッセージを送信し、それらの参加者サービスに特定の操作(ローカルトランザクション)を完了するように指示します。状態マシンと同様に、参加者サービスは操作を完了すると、次の処理を決定するためにオーケストレーターに状態コマンドを送信します。
(「マイクロサービスアーキテクチャのデザインパターン」の関連する章から引用)
実行プロセスを分析しましょう
-
Order Serviceは、最初にOrderおよびOrder Creation Controllerを作成します。その後のパスの流れは次のとおりです。
-
佐賀オーケストレータは、コンシューマサービスにVerify Consumerコマンドを送信します。
-
Consumer Serviceは、Consumer Verifiedメッセージに応答します。
-
佐賀オーケストレーターがキッチンサービスにチケット作成コマンドを送信します。
-
キッチンサービスは、チケット作成メッセージに応答しました。
-
佐賀コーディネーターが承認カードメッセージをアカウンティングサービスに送信します。
-
アカウンティングサービス部門は、カード認証メッセージで応答します。
-
佐賀オーケストレーターはキッチンサービスにチケットの承認コマンドを送信します。
- 佐賀オーケストレーターは注文承認コマンドを注文サービスに送信します。
2.3報酬戦略
前の説明で、佐賀の最も重要なことは例外の処理方法であると述べましたが、ステートマシンは多くの異常な状態も定義します。上記6が失敗する場合は、AuthorizeCardFailureをトリガーします。この時点で注文が終了し、以前に送信されたトランザクションがロールバックされます。検証トランザクションと補償を必要とするトランザクションを区別する必要があります。
佐賀県は3つの異なるタイプの取引で構成されています:補償可能な取引(ロールバックできるため、補償取引があります)、重要な取引(これは、佐賀の成功または失敗の重要なポイントであり、4つの口座の源泉徴収など)と再現性トランザクション。ロールバックして完了を保証する必要はありません(6更新ステータスなど)。
Create Order Sagaでは、createOrder()およびcreateTicket()ステップは補正可能なトランザクションであり、更新をキャンセルする補正トランザクションがあります。
verifyConsumerDetails()トランザクションは読み取り専用であるため、補正トランザクションは必要ありません。authorizeCreditCard()トランザクションは、このSagaの主要なトランザクションです。消費者のクレジットカードが認証されることができれば、この佐賀は完成することが保証されています。ステップapproveTicket()およびapproveRestaurantOrder()は、重要なトランザクションの後で繰り返し可能なトランザクションです。
各ステップを慎重に解体し、その補償戦略を評価することが特に重要であることがわかるように、トランザクションのタイプごとに、対策では異なる役割を果たします。
4. CQRS
イベントの概念については前に説明し、佐賀が複雑なトランザクションをどのように解決したかを分析したので、CDDがDDDで広く採用されている理由を見てみましょう。個別の読み取りと書き込みの特性に加えて、コマンドロジックを実践するためのイベント駆動型の方法を使用して、ビジネスの複雑さを効果的に軽減できます。
イベントをモデル化する方法、複雑なトランザクションを回避する方法、メッセージミドルウェアを使用するタイミング、イベントバスを使用するタイミングを理解すると、CQRSである理由とそれを正しく適用する方法を理解できます。
(写真はネットワークからのものです)
以下は私たちのプロジェクトの設計です。ここに読み取り/書き込みサービスが表示されるのは、呼び出しをカプセル化するためです。サービスは、集計に基づいて内部的にイベントを送信します。実際のプロジェクトでは、多くの人が初めてXXXモデルではなくXXXServiceを要求することがわかったので、DDDが十分に普及していないプロジェクトでは、このセンタリング戦略を採用することをお勧めします。これも私たちのデカップリングと一致しています相手は私の抽象化能力に依存していますが、私がDDDに基づいているのか、従来のプロセスコードに基づいているのかは関係なく、透過的です。
まず、イベントとプロセッサーのタイミング関係を見てみましょう。
ここではまだファイルクラウドストレージサービスを例にしていますが、一部のプロセッサのコアコードを次に示します。コメント行は、コードの機能、使用法、および拡張の解釈です。注意深く読んでください。
package domain;
import domain.event.DomainEvent;
import domain.handler.event.DomainEventHandler;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* @Description: 事件注册逻辑
* @Author: zhangwenbo
* @Since: 2019/3/6
*/
public class DomainRegistry {
private Map<String, List<DomainEventHandler>> handlerMap =
new HashMap<String, List<DomainEventHandler>>();
private static DomainRegistry instance;
private DomainRegistry() {
}
public static DomainRegistry getInstance() {
if (instance == null) {
instance = new DomainRegistry();
}
return instance;
}
public Map<String, List<DomainEventHandler>> getHandlerMap() {
return handlerMap;
}
public List<DomainEventHandler> find(String name) {
if (name == null) {
return null;
}
return handlerMap.get(name);
}
//事件注册与维护,register分多少个场景根据业务拆分,
//这里是业务流的核心。如果多个事件需要维护前后依赖关系,
//可以维护一个priority逻辑
public void register(Class<? extends DomainEvent> domainEvent,
DomainEventHandler handler) {
if (domainEvent == null) {
return;
}
if (handlerMap.get(domainEvent.getName()) == null) {
handlerMap.put(domainEvent.getName(), new ArrayList<DomainEventHandler>());
}
handlerMap.get(domainEvent.getName()).add(handler);
//按照优先级进行事件处理器排序
。。。
}
}
ファイルアップロード完了イベントの例。
package domain.handler.event;
import domain.DomainRegistry;
import domain.StateDispatcher;
import domain.entity.meta.MetaActionEnums;
import domain.event.DomainEvent;
import domain.event.MetaEvent;
import domain.repository.meta.MetaRepository;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import javax.annotation.Resource;
/**
* @Description:一个事件操作的处理器
* 我们混合使用了Saga的两种模式,外层事件交互;
* 对于单个复杂的事件内部采取状态流转实现。
* @Author: zhangwenbo
* @Since: 2019/3/6
*/
@Component
public class MetaConfirmUploadedHandler implements DomainEventHandler {
@Resource
private MetaRepository metaRepository;
public void handle(DomainEvent event) {
//1.我们在当前的上下文中定义个ThreadLocal变量
//用于存放事件影响的聚合根信息(线程共享)
//2.当然如果有需要额外的信息,可以基于event所
//携带的信息构造Specification从repository获取
// 代码示例
// metaRepository.queryBySpecification(SpecificationFactory.build(event));
DomainEvent domainEvent = metaRepository.load();
//此处是我们的逻辑
。。。。
//对于单个操作比较复杂的,可以使用状态流转进一步拆分
domainEvent.setStatus(nextState);
//在事件触发之后,仍需要一个状态跟踪器来解决大事务问题
//Saga编排式
StateDispatcher.dispatch();
}
@PostConstruct
public void autoRegister() {
//此处可以更加细分,注册在哪一类场景中,这也是事件驱动的强大、灵活之处。
//避免了if...else判断。我们可以有这样的意识,一旦你的逻辑里面充斥了大量
//switch、if的时候来看看自己注册的场景是否可以继续细分
DomainRegistry.getInstance().register(MetaEvent.class, this);
}
public String getAction() {
return MetaActionEnums.CONFIRM_UPLOADED.name();
}
//适用于前后依赖的事件,通过优先级指定执行顺序
public Integer getPriority() {
return PriorityEnums.FIRST.getValue();
}
}
イベントバスロジック
package domain;
import domain.event.DomainEvent;
import domain.handler.event.DomainEventHandler;
import java.util.List;
/**
* @Description:
* @Author: zhangwenbo
* @Since: 2019/3/6
*/
public class DefaultDomainEventBus {
public static void publish(DomainEvent event, String action,
EventCallback callback) {
List<DomainEventHandler> handlers = DomainRegistry.getInstance().
find(event.getClass().getName());
handlers.stream().forEach(handler -> {
if (action != null && action.equals(handler.getAction())) {
Exception e = null;
boolean result = true;
try {
handler.handle(event);
} catch (Exception ex) {
e = ex;
result = false;
//自定义异常处理
。。。
} finally {
//write into event store
saveEvent(event);
}
//根据实际业务处理回调场景,DefaultEventCallback可以返回
if (callback != null) {
callback.callback(event, action, result, e);
}
}
});
}
}
5.自律サービスとシステム
DDDは、境界のあるコンテキストの自律性を強調します。実際には、より細かい粒度から、オブジェクトには自律性の4つの特性、つまり最小の完全性、自己実現、安定した空間、および独立した進化が必要です。中でも自己実現が鍵であり、外部に依存しないため安定しており、安定して独立して進化することが可能です。このため、DDDでは六角形のアーキテクチャがより一般的です。
(写真はネットワークからのものです)
6.まとめ
この記事で説明するイベント、Saga、およびCQRSソリューションは個別に使用でき、メソッドの1つまたはパッケージ全体に適用できます。プロジェクト内のいくつかのアイデアがプロジェクトの特定の問題を解決する限り、プロジェクト内でCQRSのセット全体を実践する必要はありません。
多分あなたは今あなたのナイフを研ぎ、プロジェクトでこれらのスキルを練習する準備ができています。ただし、「すべてのコインには両面がある」ことを理解する必要があります。高拡張、デカップリング、およびレイアウトの容易さの利点だけでなく、それがもたらす問題を理解する必要があります。長所と短所の分析の後、それはそれを実現する方法を決定する正しい方法です。
-
このタイプのプログラミングモードには一定の学習曲線があります。
-
メッセージングに基づくアプリケーションの複雑さ。
-
イベントの進化に対処することは困難です。
-
データの削除には一定の困難があります。
- イベントリポジトリのクエリは非常に困難です。
ただし、適切なシナリオでは、六角形のアーキテクチャとDDDの戦術によりドメインモデリングプロセスが加速し、要件ではなく、厳密な一般的な言語の観点からドメインを説明する必要があることも認識しなければなりません。テクノロジーの実装よりもコアドメインに重点を置く方法であれば、ビジネス価値を高め、競争力を高めることができます。
添付:参照
- 書籍:「マイクロサービスアーキテクチャのデザインパターン」