はじめに: モジュール間の通信
モジュール間の通信は、モジュール式モノリスの心臓部です。とは異なり、 通信がネットワーク (HTTP、gRPC、メッセージ ブローカー) 経由でのみ行われるマイクロサービス、 モジュラーモノリスでは、以下から選択できるという利点があります。 同期通信 仕掛品 e イベントベースの非同期通信、それぞれ カップリング、レイテンシー、デバッグ可能性に関する特定のトレードオフ。
この記事では、次の 3 つの主要な通信パターンを分析します。 直接通話 メソッド、インプロセス イベント バス、および非同期メッセージング。それらを実装する方法を見ていきます。 スプリングブーツ そして、コンテキストに基づいてどちらかをいつ選択するか。
この記事で学べること
- 同期通信: インターフェースを介したメソッド呼び出し、利点と制限
- インプロセス イベント バス: 同じ JVM 内の pub-sub
- 非同期メッセージング: 高度なデカップリングのための RabbitMQ と Kafka
- メディエーター パターン: モジュール間の依存関係を分離する
- 完全な CQRS: コマンドとクエリの分離
- 一貫性: 強い一貫性と結果的な一貫性
- 障害管理: 再試行、デッドレターキュー、サーキットブレーカー
- 非同期ストリームでのデバッグと可観測性
パターン1:同期通信(直接メソッド呼び出し)
最も単純なパターン: モジュールがパブリック インターフェイス メソッドを直接呼び出します。 別のモジュール。電話がかかります 仕掛品、ネットワークのオーバーヘッドなしで、 標準の Java メソッド呼び出しと同じ遅延 (ナノ秒)。
利点
- シンプルさ: 追加のインフラストラクチャやメッセージ ブローカーは不要
- 強い一貫性: 呼び出しは同じトランザクションの一部です
- 直接デバッグ: フルスタックトレース、ブレークポイント、ステップスルー
- タイプセーフティ: コンパイラはモジュール間の契約をチェックします
短所
- 時間的結合: 着信者が応答するまで発信者はブロックされます。
- 連鎖的な障害: 呼び出されたモジュールが失敗すると、呼び出し側も失敗します
- 直接の依存関係: 呼び出し側モジュールは、呼び出された側のインターフェイスを知っています。
// Comunicazione sincrona: Order chiama Catalog via interfaccia
@Service
class OrderServiceImpl implements OrderModuleApi {
// Dipendenza esplicita verso l'API del modulo Catalog
private final CatalogModuleApi catalogModule;
private final UserModuleApi userModule;
@Override
@Transactional
public OrderDto createOrder(CreateOrderCommand cmd) {
// Chiamata sincrona 1: verifica utente
UserDto user = userModule.findById(cmd.userId())
.orElseThrow(() -> new UserNotFoundException(cmd.userId()));
// Chiamata sincrona 2: verifica prodotti
List<ProductDto> products = cmd.productIds().stream()
.map(id -> catalogModule.findById(id)
.orElseThrow(() -> new ProductNotFoundException(id)))
.toList();
// Logica locale del modulo Order
Order order = Order.create(user.id(), products);
orderRepository.save(order);
return order.toDto();
// Tutto avviene nella stessa transazione ACID
}
}
パターン 2: イベント バス処理中
L'インプロセスイベントバス パターンを実装します パブリッシュ-サブスクライブ JVM 自体内で。 1 つのモジュールがイベントを発行し、0 個以上のモジュールがそのイベントをサブスクライブします 彼らはその出来事に反応します。プロデューサーはコンシューマーを知りません。切り離しは完了しています。
Spring Framework は、ネイティブのインプロセス イベント バスを提供します。
ApplicationEventPublisher と注釈 @EventListener
e @TransactionalEventListener.
利点
- デカップリング: 生産者は消費者のことを知りません
- 拡張性: プロデューサを変更せずに新しいコンシューマを追加できます
- インフラストラクチャがない: 外部ブローカーなしで JVM 自体内で動作します
短所
- より複雑なデバッグ: フローは直線的ではないため、イベントを追跡する必要があります
- 執行順序: リスナーの順序は保証されません
- イベントの損失: アプリケーションがクラッシュすると、メモリ内のイベントが失われます
// Event Bus in-process con Spring Events
// 1. Definizione dell'evento (nel package api del modulo)
public record OrderCreatedEvent(
UUID orderId,
UUID userId,
Money total,
List<UUID> productIds,
Instant timestamp
) {}
// 2. Pubblicazione dell'evento (nel modulo Order)
@Service
class OrderServiceImpl {
private final ApplicationEventPublisher eventPublisher;
@Transactional
public OrderDto createOrder(CreateOrderCommand cmd) {
Order order = Order.create(cmd);
orderRepository.save(order);
// Pubblica evento DOPO il commit della transazione
eventPublisher.publishEvent(new OrderCreatedEvent(
order.getId(),
order.getUserId(),
order.getTotal(),
order.getProductIds(),
Instant.now()
));
return order.toDto();
}
}
// 3. Consumatori in moduli diversi
// Modulo Payment: processa il pagamento
@Service
class PaymentEventHandler {
@TransactionalEventListener(phase = AFTER_COMMIT)
void handleOrderCreated(OrderCreatedEvent event) {
paymentService.initiatePayment(
event.userId(), event.total()
);
}
}
// Modulo Inventory: riserva lo stock
@Service
class InventoryEventHandler {
@TransactionalEventListener(phase = AFTER_COMMIT)
void handleOrderCreated(OrderCreatedEvent event) {
for (UUID productId : event.productIds()) {
inventoryService.reserveStock(productId);
}
}
}
// Modulo Notification: invia conferma
@Service
class NotificationEventHandler {
@TransactionalEventListener(phase = AFTER_COMMIT)
void handleOrderCreated(OrderCreatedEvent event) {
notificationService.sendOrderConfirmation(
event.userId(), event.orderId()
);
}
}
TransactionalEventListener と EventListener
@EventListener リスナーを実行します その間 の取引
メーカー。リスナーが失敗した場合、元のトランザクションはロールバックされます。
@TransactionalEventListener(phase = AFTER_COMMIT) リスナーを実行します
dopo トランザクションのコミット、イベントが確実に処理されること
元のトランザクションが成功した場合のみ。
パターン 3: メディエーター パターン
Il メディエーターパターン コミュニケーションを調整する仲介者を紹介します モジュール間。ターゲット モジュール インターフェイスを直接挿入する代わりに、モジュール コマンドまたはクエリをメディエーターに送信し、メディエーターはそれらを適切なハンドラーにルーティングします。
// Mediator Pattern: disaccoppia completamente i moduli
// Interfaccia del Mediator
public interface Mediator {
<R> R send(Command<R> command);
<R> R query(Query<R> query);
}
// Comando: CreateOrder restituisce OrderDto
public record CreateOrderCommand(
UUID userId, List<UUID> productIds
) implements Command<OrderDto> {}
// Handler nel modulo Order
@Component
class CreateOrderHandler implements CommandHandler<CreateOrderCommand, OrderDto> {
@Override
public OrderDto handle(CreateOrderCommand cmd) {
Order order = Order.create(cmd.userId(), cmd.productIds());
orderRepository.save(order);
return order.toDto();
}
}
// Implementazione del Mediator
@Component
class MediatorImpl implements Mediator {
private final Map<Class<?>, CommandHandler<?, ?>> handlers;
@SuppressWarnings("unchecked")
public <R> R send(Command<R> command) {
CommandHandler<Command<R>, R> handler =
(CommandHandler<Command<R>, R>) handlers.get(command.getClass());
if (handler == null) {
throw new NoHandlerException(command.getClass());
}
return handler.handle(command);
}
}
// Utilizzo: il chiamante non conosce il modulo target
@RestController
class OrderController {
private final Mediator mediator;
@PostMapping("/orders")
OrderDto createOrder(@RequestBody CreateOrderRequest request) {
return mediator.send(new CreateOrderCommand(
request.userId(), request.productIds()
));
}
}
パターン 4: RabbitMQ を使用した非同期メッセージング
必要なシナリオの場合 高度なデカップリング、失敗に対する回復力 e 再試行の可能性、外部メッセージ ブローカーなど ラビットMQ 保証を提供します インプロセス イベント バスに加えて、メッセージ永続性、デッドレター キュー、 自動再試行と複数のインスタンスへの分散。
// Messaging asincrono con RabbitMQ e Spring AMQP
// Configurazione
@Configuration
class RabbitConfig {
@Bean
public TopicExchange orderExchange() {
return new TopicExchange("order.events");
}
@Bean
public Queue paymentQueue() {
return QueueBuilder.durable("payment.order-created")
.withArgument("x-dead-letter-exchange", "order.events.dlx")
.build();
}
@Bean
public Binding paymentBinding() {
return BindingBuilder
.bind(paymentQueue())
.to(orderExchange())
.with("order.created");
}
}
// Produttore nel modulo Order
@Service
class OrderEventPublisher {
private final RabbitTemplate rabbitTemplate;
public void publishOrderCreated(OrderCreatedEvent event) {
rabbitTemplate.convertAndSend(
"order.events", // exchange
"order.created", // routing key
event // payload
);
}
}
// Consumatore nel modulo Payment
@Component
class PaymentOrderListener {
@RabbitListener(queues = "payment.order-created")
public void handleOrderCreated(OrderCreatedEvent event) {
try {
paymentService.processPayment(
event.userId(), event.total()
);
} catch (Exception e) {
// Il messaggio viene reinviato alla DLQ dopo 3 retry
throw new AmqpRejectAndDontRequeueException(e);
}
}
}
CQRS: コマンドとクエリの分離
パターン CQRS (コマンドクエリ責任分離) 完全に分離する クエリ パス (読み取り) からのコマンド パス (書き込み)。コマンドが通過する モジュール API を使用して状態を変更します。クエリは最適化された非正規化投影にアクセスします 読書用に。
// CQRS: Command side (scrittura)
// Passa attraverso le API dei moduli con validazione
@Service
class OrderCommandService {
@Transactional
public UUID createOrder(CreateOrderCommand cmd) {
// Validazione, logica di dominio, persistenza
Order order = Order.create(cmd);
orderRepo.save(order);
events.publish(new OrderCreatedEvent(order));
return order.getId();
}
}
// CQRS: Query side (lettura)
// Accede a viste denormalizzate, nessuna logica di dominio
@Service
class OrderQueryService {
private final OrderReadModelRepository readModelRepo;
// Query veloce: la vista contiene già tutti i dati necessari
public OrderDetailsView getOrderDetails(UUID orderId) {
return readModelRepo.findById(orderId)
.orElseThrow(() -> new OrderNotFoundException(orderId));
}
// Query di lista con filtri
public Page<OrderSummaryView> searchOrders(
OrderSearchCriteria criteria, Pageable pageable) {
return readModelRepo.search(criteria, pageable);
}
}
// Vista denormalizzata: contiene dati da più moduli
@Entity
public class OrderDetailsView {
private UUID orderId;
private String customerName; // dal modulo User
private String customerEmail; // dal modulo User
private List<OrderItemView> items;
private String productNames; // dal modulo Catalog
private BigDecimal total;
private String paymentMethod; // dal modulo Payment
private String paymentStatus; // dal modulo Payment
}
破産管理
非同期フローでは、失敗は避けられません。それらを管理する主なパターンは次のとおりです。
指数バックオフによる再試行
ハンドラーが失敗した場合は、間隔を 1 秒、2 秒、4 秒、8 秒と増やして再試行します。数字の後に 最大試行回数に応じて、メッセージは次の場所に移動されます。 デッドレターキュー (DLQ) 手動分析用。
デッドレターキュー
すべての再試行後に処理できなかったメッセージはキューに移動されます 献身的。オペレーターまたは自動プロセスは、失敗したメッセージを分析して再試行できます。
サーキットブレーカー
ターゲットモジュールが繰り返し故障すると、サーキットブレーカーが 通話を中断する 一定期間、すでに問題が発生しているモジュールの過負荷を回避します。タイムアウト後、 サーキットブレーカーは徐々に再試行してください。
デバッグと可観測性
非同期ストリームは、同期呼び出しよりもデバッグが困難です。ここにいます 基本的な実践:
- 相関ID: 各フローには、すべてのイベントと呼び出しを通じて実行される一意の ID があります。
- 構造化されたロギング: 相関ID、ソースモジュール、ターゲットモジュールを含む構造化ログ
- イベントストア: 追跡および再生のために公開されたすべてのイベントを保存します
- 健康診断: すべてのモジュールとブローカーが動作していることを確認します。
次の記事
次回の記事では、次のトピックについて取り上げます。 導入とスケーリング モジュラーの モノリス: Blue-Green デプロイメント戦略、段階的リリースの機能フラグ、自動スケーリング インテリジェントなメトリクスベースで、いつモジュールをマイクロサービスとして抽出するか 分離されたスケーラビリティ。







