一、思路
rabbitmq里面自带了retry的功能,但在手动确认模式里面就不太好用了。本篇文章的主要思路就是通过redis以消息的messageId为key,记录重试次数。
实现代码
首先创建队列和交换机
@Configuration
public class LogMqConfig {
/**
* 交换机
*/
@Bean
public DirectExchange logExchange() {
return new DirectExchange(MqConstants.LOG_LOGIN_EXCHANGE, true, false);
}
/**
* 队列
*/
@Bean
public Queue logQueue() {
Map<String, Object> map = new HashMap<>(4);
// 绑定该队列到私信交换机
map.put("x-dead-letter-exchange", MqConstants.DEAD_EXCHANGE);
map.put("x-dead-letter-routing-key", MqConstants.DEAD_ROUTING);
return new Queue(MqConstants.LOG_LOGIN_QUEUE, true, false, false, map);
}
/**
* 队列交换机绑定
*/
@Bean
public Binding logBinding(DirectExchange logExchange, Queue logQueue) {
return BindingBuilder.bind(logQueue).to(logExchange).with(MqConstants.LOG_LOGIN_ROUTING);
}
}
消息到达最大重试次数,可以让它进入死信队列,所这里创建死信队列
@Configuration
public class DeadMqConfig {
/**
* 死信交换机
*/
@Bean
DirectExchange deadExchange() {
return new DirectExchange(MqConstants.DEAD_EXCHANGE, true, false);
}
/**
* 死信队列
*/
@Bean
public Queue deadQueue() {
return new Queue(MqConstants.DEAD_QUEUE, true, false, false);
}
@Bean
Binding deadRouteBinding() {
return BindingBuilder.bind(deadQueue()).to(deadExchange()).with(MqConstants.DEAD_ROUTING);
}
}
发送消息类
@Component
@RequiredArgsConstructor
public class MessageSender {
private final RabbitTemplate rabbitTemplate;
public void sendMsg(String exchange, String routingKey, Object object) {
String msg = JSONUtil.toJsonStr(object);
MessageProperties messageProperties = new MessageProperties();
messageProperties.setMessageId(IdUtil.fastSimpleUUID());
messageProperties.setContentType("text/plain");
messageProperties.setContentEncoding("utf-8");
Message message = new Message(msg.getBytes(Charset.defaultCharset()), messageProperties);
rabbitTemplate.convertAndSend(exchange, routingKey, message);
}
}
消息的消费者
@Slf4j
@Component
@RequiredArgsConstructor
public class LogListener {
private final RedisCache redisCache;
private static final int RETRY_TIME = 3;
@RabbitListener(queues = MqConstants.LOG_LOGIN_QUEUE)
public void logProcess(Message message, Channel channel) {
ActionEnum action = ActionEnum.ACCEPT;
LogMessage logMessage = new LogMessage();
String messageId = "";
long deliveryTag = 0L;
try {
String json = new String(message.getBody());
logMessage = JSONUtil.toBean(json, LogMessage.class);
deliveryTag = message.getMessageProperties().getDeliveryTag();
messageId = message.getMessageProperties().getMessageId();
log.info("收到消息,路由:【{}】,消息体:【{}】", MqConstants.LOG_LOGIN_ROUTING, logMessage);
int i = 1/0;
} catch (Exception e) {
action = ActionEnum.RETRY;
} finally {
boolean requeue = false;
try {
if (action == ActionEnum.ACCEPT) {
channel.basicAck(deliveryTag, false);
log.error("消息消费成功------message:【{}】",logMessage);
} else if (action == ActionEnum.RETRY) {
String key = RedisKeys.RABBITMQ_RETRY_KEY + messageId;
Object obj = redisCache.get(key);
if (obj == null) {
log.error("消息消费失败,进入第【1次】重试-------messageId:【{}】",messageId);
// 第一次重试
redisCache.set(key, 1, 60 *2);
requeue = true;
} else {
Integer retryTime = Convert.toInt(obj);
if (retryTime < RETRY_TIME) {
log.error("消息消费失败,进入第【{}次】重试-------messageId:【{}】",retryTime + 1, messageId);
requeue = true;
redisCache.increment(key);
} else {
redisCache.delete(key);
log.info("重试已到最高次数,进入死信队列!");
}
}
// requeue为true就重新进入队列
channel.basicNack(deliveryTag, false, requeue);
}
} catch (IOException ex) {
ex.printStackTrace();
}
}
}
}
消息生产者测试
@RestController
@RequestMapping("/test")
@RequiredArgsConstructor
@Slf4j
public class TestController {
private final MessageSender messageSender;
@GetMapping("/sendMsg")
public Result<LogMessage> testMq() {
LogMessage logMessage = new LogMessage();
logMessage.setCode("test");
logMessage.setName("张三");
messageSender.sendMsg(MqConstants.LOG_LOGIN_EXCHANGE, MqConstants.LOG_LOGIN_ROUTING, logMessage);
log.info("MQ消息发送,交换机:【{}】, 路由:【{}】, 消息体:【{}】", MqConstants.LOG_LOGIN_EXCHANGE, MqConstants.LOG_LOGIN_ROUTING, logMessage);
return Result.success(logMessage);
}
}
测试结果
消息消费成功时:
消息消费失败时: