RabbitMQにおける遅延メッセージ処理の実装:DLXと死信キュー活用法

死信キュー(Dead Letter Queue, DLQ)は、RabbitMQで正常に処理されなかったメッセージを一時的に格納するための標準的な機構です。本質的には、通常のキューおよびエクスチェンジと同じ構成要素であり、特別な型や拡張機能を持つわけではありません。その動作は、Dead-Letter-Exchange(DLX)という属性付きの交換所によって制御されます。

1. 死信メッセージの発生条件

以下のいずれかの状況が発生した場合、メッセージは対応するDLXへルーティングされ、そこから指定された死信キューに送信されます:

  • basic.reject または basic.nack で拒否され、かつ requeue = false が設定された場合
  • キュー内で設定されたTTL(Time-To-Live)が経過したメッセージ
  • キューの最大長(x-max-length)を超えて追加された新規メッセージ

2. 主なユースケース

DLXを活用することで、明示的なタイマー機構なしに非同期の遅延処理を実現できます。代表的な例として:

  • 決済未完了の注文の自動キャンセル(例:30分経過後)
  • 有効期限付きクーポンやギフトの無効化
  • 購入後の自動受取確認(例:7日経過後)

3. アーキテクチャ設計の要点

実装では、2つのキューと1つのトピック型エクスチェンジを組み合わせます:

  • 一時キュー(delayed-queue):消費者がバインドされず、TTLとDLX関連パラメータを設定。メッセージはここで「待機」
  • 死信キュー(dl-queue):DLXからメッセージを受け取り、実際に処理を行うコンシューマーが監視
  • 共通エクスチェンジ:両キューをルーティングキーで分岐させ、メッセージフローを統一管理

4. Spring AMQPによる実装例

@Configuration
public class RabbitMQDelayConfig {

    private static final String ORDER_EXCHANGE = "order.topic.exchange";
    private static final String DELAYED_QUEUE = "order.delay.queue";
    private static final String DEAD_LETTER_QUEUE = "order.dl.queue";
    private static final String DL_ROUTING_KEY = "order.cancel";
    private static final String MAIN_ROUTING_KEY = "order.create";

    @Bean
    public TopicExchange orderTopicExchange() {
        return new TopicExchange(ORDER_EXCHANGE);
    }

    @Bean
    public Queue delayedQueue() {
        Map<String, Object> config = new HashMap<>();
        config.put("x-dead-letter-exchange", ORDER_EXCHANGE);
        config.put("x-dead-letter-routing-key", DL_ROUTING_KEY);
        config.put("x-message-ttl", 30_000); // 30秒
        return QueueBuilder.durable(DELAYED_QUEUE)
                .withArguments(config)
                .build();
    }

    @Bean
    public Queue deadLetterQueue() {
        return QueueBuilder.durable(DEAD_LETTER_QUEUE).build();
    }

    @Bean
    public Binding bindingToDelayedQueue() {
        return BindingBuilder.bind(delayedQueue())
                .to(orderTopicExchange())
                .with(MAIN_ROUTING_KEY);
    }

    @Bean
    public Binding bindingToDeadLetterQueue() {
        return BindingBuilder.bind(deadLetterQueue())
                .to(orderTopicExchange())
                .with(DL_ROUTING_KEY);
    }
}

5. 死信キューのコンシューマー

@Component
public class OrderCancellationHandler {

    @RabbitListener(queues = DEAD_LETTER_QUEUE)
    public void handleExpiredOrder(Message message, Channel channel) throws IOException {
        String payload = new String(message.getBody(), StandardCharsets.UTF_8);
        long deliveryTag = message.getMessageProperties().getDeliveryTag();

        try {
            System.out.printf("[%s] 注文キャンセル処理開始: %s%n", 
                    LocalDateTime.now(), payload);
            // 実際のビジネスロジック(例:DB更新、通知送信)
            channel.basicAck(deliveryTag, false);
        } catch (Exception e) {
            channel.basicNack(deliveryTag, false, true);
        }
    }
}

タグ: RabbitMQ DLX dead-letter-queue spring-amqp Message-TTL

10月19日 07:13 投稿