概念
先说下什么是死信。这里边包含几个概念:
-
死信交换器:Dead-Letter-Exchange,简称DLX。作用是干嘛的呢,当一条消息在普通队列中变为死信后,这条消息就能被重新发送到另一个交换器中,这个交换器就是死信交换器。
-
死信队列:和死信交换器绑定的队列就是死信队列。当死信被重新发送到DLX后,被路由到此队列。
-
死信:简而言之,可以概括为被丢弃的消息,就是死信。
消息变为死信的几种情况:
- 消息被拒绝(Basic.Reject/Basic.Nack),并设置requeue(重新入队)参数为false;
- 消息设置了TTL,并且过期;
- 队列达到最大长度
其实在定义期间,死信交换器和死信队列都是普通的交换器和队列,只是在某个普通队列声明时使用x-dead-letter-exchange参数将此死信交换器赋予了DLX的职责而已。
示意图
延迟队列
延迟队列存储的是延迟消息,比如某条消息不想被立即消费,而是等待到固定时间后才被消费者接收。此场景比较常见的便是订单下单30分钟后未支付,订单状态变更为已取消状态,如果使用定时任务定时扫描订单表,那么扫描间隔长,造成业务无法精准,扫描间隔短,会对数据库造成压力。此时便可使用延时队列。
rabbitmq本身不具有延迟队列的功能,但是通过DLX和TTL的设置,可以模拟出延迟队列的功能。
实例
下面使用活动到期开始改变状态的业务来演示怎样使用延时队列
交换器、队列以及路由key的定义:
public class MQKeyStatic {
/**
* 营销模块死信交换器
*/
public static final String EXCHANGE_MARKETING_DLX = "exchange.marketing.dlx";
public static final String EXCHANGE_MARKETING_DELAY = "exchange.marketing.delay";
/**
* 营销模块延时队列-活动处理
*/
public static final String QUEUE_MARKETING_DELAY_ACTIVITY = "queue.good.delay.activity";
public static final String ROUTING_MARKETING_DELAY_ACTIVITY = "routing.good.delay.activity";
/**
* 营销模块死信队列-活动处理
*/
public static final String QUEUE_MARKETING_DLX_ACTIVITY = "queue.good.dlx.activity";
public static final String ROUTING_MARKETING_DLX_ACTIVITY = "routing.good.dlx.activity";
}
交换器,队列以及binding处理:
@Configuration
public class MQConfig {
@Bean
public MessageConverter messageConverter() {
Jackson2JsonMessageConverter jackson2JsonMessageConverter = new Jackson2JsonMessageConverter();
jackson2JsonMessageConverter.setCreateMessageIds(true);
return jackson2JsonMessageConverter;
}
/**
* @desc: 延迟队列处理死信交换器
* @return: org.springframework.amqp.core.TopicExchange
* @auther: Michael Wong
* @email: michael_wong@yunqihui.net
* @date: 2020/9/8 18:52
* @update:
*/
@Bean
public TopicExchange dlxExchange() {
return new TopicExchange(MQKeyStatic.EXCHANGE_MARKETING_DLX,true,false,null);
}
/**
* @desc: 延迟业务处理交换器
* @return: org.springframework.amqp.core.TopicExchange
* @auther: Michael Wong
* @email: michael_wong@yunqihui.net
* @date: 2020/9/8 18:52
* @update:
*/
@Bean
public TopicExchange delayExchange() {
return new TopicExchange(MQKeyStatic.EXCHANGE_MARKETING_DELAY,true,false,null);
}
/**
* @desc: 活动处理队列
* @return: org.springframework.amqp.core.Queue
* @auther: Michael Wong
* @email: michael_wong@yunqihui.net
* @date: 2020/9/5 14:46
* @update:
*/
@Bean
public Queue activityDlxQueue() {
return new Queue(MQKeyStatic.QUEUE_MARKETING_DLX_ACTIVITY, true, false, false, null);
}
@Bean
public Binding dlxExchangeBindingActivityDlx() {
return BindingBuilder.bind(activityDlxQueue()).to(dlxExchange()).with(MQKeyStatic.ROUTING_MARKETING_DLX_ACTIVITY);
}
@Bean
public Queue activityDelayQueue() {
// 注意要在延时队列上设置ttl和dlx属性
Map<String, Object> args = new HashMap<>(2);
args.put("x-dead-letter-exchange", MQKeyStatic.EXCHANGE_MARKETING_DLX);
args.put("x-dead-letter-routing-key", MQKeyStatic.ROUTING_MARKETING_DLX_ACTIVITY);
return new Queue(MQKeyStatic.QUEUE_MARKETING_DELAY_ACTIVITY, true, false, false, args);
}
@Bean
public Binding delayExchangeBindingActivityDelay() {
return BindingBuilder.bind(activityDelayQueue()).to(delayExchange()).with(MQKeyStatic.ROUTING_MARKETING_DELAY_ACTIVITY);
}
}
监听队列:
@Component
public class MQListener {
@Autowired
private RedisTemplate redisTemplate;
/**
* @desc: 活动商品上下架处理队列监听
* @param message
* @param channel
* @return: void
* @auther: Michael Wong
* @email: michael_wong@yunqihui.net
* @date: 2020/9/9 15:59
* @update:
*/
@RabbitListener(queuesToDeclare =
@Queue(name = MQKeyStatic.QUEUE_MARKETING_DLX_ACTIVITY, durable = "true", exclusive = "false", autoDelete = "false"))
public void activityListener(Message message, Channel channel) throws IOException {
if (idempotentMessage(message, channel)) {
return;
}
JSONObject messageJson = JSONObject.parseObject(new String(message.getBody()));
Integer activityId = messageJson.getInteger("activityId");
String activityType = messageJson.getString("activityType");
try {
// 此处使用策略模式
String executorName = activityType + "Service";
IActivityStrategy strategy = (IActivityStrategy) SpringContextHolder.getBean(executorName);
strategy.start(activityId);
ackMessage(message,channel);
} catch (Exception e) {
channel.basicReject(message.getMessageProperties().getDeliveryTag(),true);
}
}
/**
* @param message
* @param channel
* @desc: 确认消息公共方法
* @return: void
* @auther: Michael Wong
* @email: michael_wong@yunqihui.net
* @date: 2020/9/5 16:51
* @update:
*/
private void ackMessage(Message message, Channel channel) throws IOException {
try {
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
String rediskey = RedisKeyStatic.IDEMPOTENT_MESSAGE + message.getMessageProperties().getMessageId();
stringRedisTemplate.opsForValue().set(rediskey, "1", 3, TimeUnit.HOURS);
} catch (Exception e) {
log.error("接收消息失败,重新放回队列,message:{}", message);
// 解决方案,剔除此消息,然后记录到db中去补偿
channel.basicReject(message.getMessageProperties().getDeliveryTag(), true);
}
}
/**
* @param message
* @param channel
* @desc: 幂等消息
* @return: boolean
* @auther: Michael Wong
* @email: michael_wong@yunqihui.net
* @date: 2020/9/7 10:43
* @update:
*/
private boolean idempotentMessage(Message message, Channel channel) throws IOException {
String messageId = message.getMessageProperties().getMessageId();
String rediskey = RedisKeyStatic.IDEMPOTENT_MESSAGE + messageId;
long deliveryTag = message.getMessageProperties().getDeliveryTag();
if (stringRedisTemplate.hasKey(rediskey)) {
channel.basicReject(deliveryTag, false);
return true;
}
return false;
}
}