死信キュー(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);
}
}
}