1.保证消息不丢失
1.开启生产者确认机制,确保生产者的消息能到达队列
2.开启持久化功能(交换机持久化、队列持久化、消息持久化),确保信息未消费前在队列中不会丢失
3.开启消费者确认机制为auto,由spring确认消息处理成功后完成ack
4.开启消费者失败重试机制,多次失败后将消息投递到异常交换机,交由人工处理
开启生产者确认机制
yml
spring:
rabbitmq:
publisher-confirm-type: correlated # 默认为none禁用生产者确认机制
publisher-returns: true # 消息回退
@Slf4j
@Configuration
public class RabbitMQConfig implements ApplicationContextAware {
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
// 获取RabbitTemplate
RabbitTemplate rabbitTemplate = applicationContext.getBean(RabbitTemplate.class);
rabbitTemplate.setConfirmCallback((correlationData, b, s)->{
String id = correlationData != null ? correlationData.getId() : "";
if (b){
log.info("接收到ID为:{}的消息",id);
}else {
log.info("未接受到ID为:{}的消息,发生原因:{}",id,s);
}
});
// 设置ReturnCallback
rabbitTemplate.setReturnCallback((message, replyCode, replyText, exchange, routingKey) -> {
// 投递失败,记录日志
log.info("消息发送失败,应答码{},原因{},交换机{},路由键{},消息{}",
replyCode, replyText, exchange, routingKey, message.toString());
// 如果有业务需要,可以重发消息
});
}
}
@Component
@Slf4j
public class MyCallBack implements RabbitTemplate.ConfirmCallback,RabbitTemplate.ReturnCallback{
@Autowired
private RabbitTemplate rabbitTemplate;
@PostConstruct
public void init(){
rabbitTemplate.setConfirmCallback(this);
rabbitTemplate.setReturnCallback(this);
}
@Override
public void confirm(CorrelationData correlationData, boolean b, String s) {
String id = correlationData != null ? correlationData.getId() : "";
if (b){
log.info("接收到ID为:{}的消息",id);
}else {
log.info("未接受到ID为:{}的消息,发生原因:{}",id,s);
//填写具体的业务代码比如插入到数据库
}
}
@Override
public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
// 投递失败,记录日志
log.info("消息发送失败,应答码{},原因{},交换机{},路由键{},消息{}",
replyCode, replyText, exchange, routingKey, message.toString());
// 如果有业务需要,可以重发消息
}
}
两种方法都可以
开启消费重试
yml
spring:
rabbitmq:
listener:
simple:
# acknowledge-mode: auto # 可以省略
# 重试机制
retry:
enabled: true #是否开启消费者重试
max-attempts: 3 #最大重试次数(包括第一次也算一次)
initial-interval: 5000ms #重试间隔时间(单位毫秒)
@Bean
public TopicExchange errorExchange(){
return new TopicExchange(MqConstans.ERROR_EXCHANGE,true,false);
}
@Bean
public Queue errorQueue(){
return new Queue(MqConstans.ERROR_QUEUE,true);
}
@Bean
public Binding errorQueueBindingErrorExchange(){
return BindingBuilder.bind(errorQueue()).to(errorExchange()).with(MqConstans.ERROR_ROUTING_KEY);
}
@Bean
public MessageRecoverer messageRecoverer(){
return new RepublishMessageRecoverer(rabbitTemplate, MqConstans.ERROR_EXCHANGE, MqConstans.ERROR_ROUTING_KEY);
}
05-29 21:42:00:814 INFO 5244 --- [io-8099-exec-10] c.c.hotel.controller.SendMsgController : 当前时间Mon May 29 21:42:00 CST 2023,发送一条错误信息给队列:消息1
05-29 21:42:00:816 INFO 5244 --- [io-8099-exec-10] c.c.hotel.controller.SendMsgController : 发送消息内容为:消息1
05-29 21:42:00:825 INFO 5244 --- [ntContainer#2-1] com.cxf.hotel.listen.confirmListen : 接受到队列confirm.queue消息:消息1
05-29 21:42:00:828 INFO 5244 --- [nectionFactory5] com.cxf.hotel.config.RabbitMQConfig : 接收到ID为:1的消息
05-29 21:42:03:212 INFO 5244 --- [nio-8099-exec-2] c.c.hotel.controller.SendMsgController : 当前时间Mon May 29 21:42:03 CST 2023,发送一条错误信息给队列:消息2
05-29 21:42:03:213 INFO 5244 --- [nio-8099-exec-2] c.c.hotel.controller.SendMsgController : 发送消息内容为:消息2
05-29 21:42:03:265 INFO 5244 --- [nectionFactory5] com.cxf.hotel.config.RabbitMQConfig : 接收到ID为:1的消息
05-29 21:42:06:849 INFO 5244 --- [ntContainer#2-1] com.cxf.hotel.listen.confirmListen : 接受到队列confirm.queue消息:消息1
05-29 21:42:17:868 INFO 5244 --- [ntContainer#2-1] com.cxf.hotel.listen.confirmListen : 接受到队列confirm.queue消息:消息1
05-29 21:42:18:881 WARN 5244 --- [ntContainer#2-1] o.s.a.r.retry.RepublishMessageRecoverer : Republishing failed message to exchange 'error.exchange' with routing key error.routing-key
05-29 21:42:18:881 INFO 5244 --- [ntContainer#2-1] com.cxf.hotel.listen.confirmListen : 接受到队列confirm.queue消息:消息2
05-29 21:42:18:883 INFO 5244 --- [nectionFactory5] com.cxf.hotel.config.RabbitMQConfig : 接收到ID为:的消息
05-29 21:42:24:897 INFO 5244 --- [ntContainer#2-1] com.cxf.hotel.listen.confirmListen : 接受到队列confirm.queue消息:消息2
05-29 21:42:35:918 INFO 5244 --- [ntContainer#2-1] com.cxf.hotel.listen.confirmListen : 接受到队列confirm.queue消息:消息2
05-29 21:42:36:931 WARN 5244 --- [ntContainer#2-1] o.s.a.r.retry.RepublishMessageRecoverer : Republishing failed message to exchange 'error.exchange' with routing key error.routing-key
05-29 21:42:36:933 INFO 5244 --- [nectionFactory5] com.cxf.hotel.config.RabbitMQConfig : 接收到ID为:的消息
从结果可以看出设置重试次数3此时一共消费了三次(包含第一次),并且会阻塞MQ不会继续处理其他消息会等待该消息重试成功或重试次数达到上限后再消费其他消息,注意开启消息自动重试时不能try catch处理,如果使用try catch需在catch中将异常抛出,否则认为该消息执行成功不会进行重试,springboot中只会对有异常的进行nack其他的都为ack
05-29 21:54:22:882 INFO 5296 --- [nio-8099-exec-1] c.c.hotel.controller.SendMsgController : 当前时间Mon May 29 21:54:22 CST 2023,发送一条错误信息给队列:哈哈哈
05-29 21:54:22:898 INFO 5296 --- [nio-8099-exec-1] c.c.hotel.controller.SendMsgController : 发送消息内容为:哈哈哈
05-29 21:54:22:920 INFO 5296 --- [nectionFactory1] com.cxf.hotel.config.RabbitMQConfig : 接收到ID为:1的消息
05-29 21:54:22:929 INFO 5296 --- [ntContainer#2-1] com.cxf.hotel.listen.confirmListen : 接受到队列confirm.queue消息:哈哈哈
05-29 21:54:22:929 INFO 5296 --- [ntContainer#2-1] com.cxf.hotel.listen.confirmListen : java.lang.NullPointerException
05-29 21:55:11:491 INFO 5296 --- [nio-8099-exec-2] c.c.hotel.controller.SendMsgController : 当前时间Mon May 29 21:55:11 CST 2023,发送一条错误信息给队列:哈哈哈
05-29 21:55:11:493 INFO 5296 --- [nio-8099-exec-2] c.c.hotel.controller.SendMsgController : 发送消息内容为:哈哈哈
05-29 21:55:11:504 INFO 5296 --- [ntContainer#2-1] com.cxf.hotel.listen.confirmListen : 接受到队列confirm.queue消息:哈哈哈
05-29 21:55:11:504 INFO 5296 --- [ntContainer#2-1] com.cxf.hotel.listen.confirmListen : java.lang.NullPointerException
05-29 21:55:11:525 INFO 5296 --- [nectionFactory1] com.cxf.hotel.config.RabbitMQConfig : 接收到ID为:1的消息
2.重复消费
1.每条消息设置一个唯一的标识id
2.幂等方案
在消费者拿到消息时使用分布式锁通过一个自定义标识符加上消息设置的唯一表示id给该消息上锁,当有其他消息尝试获取锁时若此时有其他线程正在处理该消息则直接返回不再处理该消息
@RabbitListener(queues = RabbitMQConfig.CONFIRM_QUEUE_NAME)
public void receiveConfirmMessage(Message message, Channel channel) throws IOException {
String messageId = message.getMessageProperties().getMessageId();
log.info("message:{},messgeId:{}",message,messageId);
RLock lock = redissonClient.getLock("confirm_mq:" + messageId);
if (lock.tryLock()){
try {
String msg = new String(message.getBody());
log.info("接收到消息:{},开始处理消息",msg);
//开始处理消息
}finally {
lock.lock();
}
}else {
channel.basicReject(message.getMessageProperties().getDeliveryTag(), false);
}
}
使用数据库也可以将字段设为不可重复字段插入时如果重复了会报错
3.延迟队列
延迟队列=死信交换机+TTL(生存时间)
1.死信交换机
当一个队列中的消息满足下列情况之一,可以成为死信
1.)消费者使用basic.reject或basic.nack声明消费失败,并且消息的requeue参数设置为false
2.)消息是一个过期消息,超时无人消费
3.)要投递的队列消息堆积满了,最早的消息可能成为死信
如果该队列配置了dead-letter-exchange属性,指定了一个交换机,那么队列中的死信就会投递到这个交换机中,而这个交换机成为死信交换机
延迟队列插件实现延迟队列DelayExchange
声明一个交换机,添加delayed属性为true
发送消息时,添加x-delay头,值为超时时间
实现死信交换机
public class MqConstans {
//普通交换机名称
public static final String MY_EXCHANGE = "MY";
//死信交换机名称
public static final String MY_DEAD_LETTER_EXCHANGE = "MY_DEAD_LETTER";
//普通队列名称
public static final String QUEUE_MY = "Q_MY";
//死信队列名称
public static final String DEAD_LETTER_QUEUE_MY = "DEAD_LETTER_Q_MY";
//普通RoutingKey名称
public static final String MY_ROUTING_KEY = "ROUTING_MY";
//死信RoutingKey名称
public static final String DEAD_LETTER_ROUTING_KEY = "DEAD_LETTER_ROUTING_MY";
}
@Configuration
public class TTLQueueConig {
//声明普通交换机
@Bean
public TopicExchange myExchange(){
return new TopicExchange(MqConstans.MY_EXCHANGE,true,false);
}
//声明死信交换机
@Bean
public TopicExchange myDeadLetterExchange(){
return new TopicExchange(MqConstans.MY_DEAD_LETTER_EXCHANGE,true,false);
}
//声明普通队列
@Bean
public Queue myQueue(){
return QueueBuilder
.durable(MqConstans.QUEUE_MY)
.deadLetterExchange(MqConstans.MY_DEAD_LETTER_EXCHANGE) //设置死信交换机
.deadLetterRoutingKey(MqConstans.DEAD_LETTER_ROUTING_KEY) //设置死信RoutingKey
.build();
}
//声明死信队列
@Bean
public Queue myDeadLetterQueue(){
return QueueBuilder
.durable(MqConstans.DEAD_LETTER_QUEUE_MY)
.build();
}
//绑定普通队列
@Bean
public Binding myQueueBindingMyExchange(){
return BindingBuilder.bind(myQueue()).to(myExchange()).with(MqConstans.MY_ROUTING_KEY);
}
//绑定死信队列
@Bean
public Binding myDeadLetterQueueBindingMyDeadLetterExchange(){
return BindingBuilder.bind(myDeadLetterQueue()).to(myDeadLetterExchange()).with(MqConstans.DEAD_LETTER_ROUTING_KEY);
}
}
@GetMapping("/message/{message}/{ttlTime}")
public void senMessage(@PathVariable String message,@PathVariable String ttlTime){
log.info("当前时间{},发送一条时长{}毫秒TTL信息给死信队列:{}",new Date().toString(),ttlTime,message);
// MessageProperties messageProperties = new MessageProperties();
// messageProperties.setExpiration(ttlTime);
// Message build = MessageBuilder
// .withBody(message.getBytes())
// .andProperties(messageProperties)
// .build();
// rabbitTemplate.convertAndSend(MqConstans.MY_EXCHANGE,MqConstans.MY_ROUTING_KEY,build);
rabbitTemplate.convertAndSend(MqConstans.MY_EXCHANGE,MqConstans.MY_ROUTING_KEY,"消息来自ttl:"+message, msg->{
msg.getMessageProperties().setExpiration(ttlTime);
return msg;
});
}
@Component
@Slf4j
public class DeleteLetterListen {
//接收消息
@RabbitListener(queues = MqConstans.DEAD_LETTER_QUEUE_MY)
public void receiveD(Message message, Channel channel) throws Exception{
String msg = new String(message.getBody());
log.info("当前时间:{},接受到死信队列消息:{}",new Date().toString(),msg);
}
}
05-27 17:59:04:139 INFO 12152 --- [nio-8099-exec-4] c.c.hotel.controller.SendMsgController : 当前时间Sat May 27 17:59:04 CST 2023,发送一条时长20000毫秒TTL信息给死信队列:哈哈哈1
05-27 17:59:09:791 INFO 12152 --- [nio-8099-exec-5] c.c.hotel.controller.SendMsgController : 当前时间Sat May 27 17:59:09 CST 2023,发送一条时长2000毫秒TTL信息给死信队列:哈哈哈2
05-27 17:59:24:242 INFO 12152 --- [ntContainer#1-1] com.cxf.hotel.listen.DeleteLetterListen : 当前时间:Sat May 27 17:59:24 CST 2023,接受到死信队列消息:消息来自ttl:哈哈哈1
05-27 17:59:24:243 INFO 12152 --- [ntContainer#1-1] com.cxf.hotel.listen.DeleteLetterListen : 当前时间:Sat May 27 17:59:24 CST 2023,接受到死信队列消息:消息来自ttl:哈哈哈2
由于队列的特性先进先出如果第一个请求延迟20S第二个请求延迟2秒,经过2秒后并不会返回第二个请求的结果会等待20秒后先返回第一个请求的结果再返回第二个请求的结果要想解决这个问题需要使用RabbitMQ的rabbitmq_delayed_message_exchange插件
public class MqConstans {
public static final String DELAYED_EXCHANGE="delayed.exchange";
public static final String DELAYED_QUEUE="delayed.queue";
public static final String DELAYED_ROUTING_KEY="delayed.routingkey";
}
@Configuration
public class DelayedQueueConfig {
//声明交换机
@Bean
public CustomExchange delayedExchange(){
Map<String,Object> arguments = new HashMap<>();
arguments.put("x-delayed-type","direct");
return new CustomExchange(MqConstans.DELAYED_EXCHANGE,"x-delayed-message",true,false,arguments);
}
//声明队列
@Bean
public Queue delayedQueue(){
return new Queue(MqConstans.DELAYED_QUEUE);
}
//绑定队列
@Bean
public Binding delayedQueueBindingDelayedExchange(){
return BindingBuilder.bind(delayedQueue()).to(delayedExchange()).with(MqConstans.DELAYED_ROUTING_KEY).noargs();
}
}
@GetMapping("/sendDelayedMessage/{message}/{delayTime}")
public void sendDelayedMessage(@PathVariable String message,@PathVariable Integer delayTime){
log.info("当前时间{},发送一条时长{}毫秒TTL信息给延迟队列delayed.queue:{}",
new Date().toString(),delayTime,message);
rabbitTemplate.convertAndSend(MqConstans.DELAYED_EXCHANGE,MqConstans.DELAYED_ROUTING_KEY,"消息来自ttl:"+message, msg->{
msg.getMessageProperties().setDelay(delayTime);
return msg;
});
}
@Component
@Slf4j
public class DelayedListen {
@RabbitListener(queues = MqConstans.DELAYED_QUEUE)
public void receiveDelayedMessage(Message message){
String msg = new String(message.getBody());
log.info("当前时间:{},收到延迟队列的信息:{}",new Date().toString(),msg);
}
}
05-27 17:55:39:748 INFO 12152 --- [nio-8099-exec-1] c.c.hotel.controller.SendMsgController : 当前时间Sat May 27 17:55:39 CST 2023,发送一条时长20000毫秒TTL信息给延迟队列delayed.queue:哈哈哈1
05-27 17:55:43:220 INFO 12152 --- [nio-8099-exec-2] c.c.hotel.controller.SendMsgController : 当前时间Sat May 27 17:55:43 CST 2023,发送一条时长2000毫秒TTL信息给延迟队列delayed.queue:哈哈哈2
05-27 17:55:45:234 INFO 12152 --- [ntContainer#0-1] com.cxf.hotel.listen.DelayedListen : 当前时间:Sat May 27 17:55:45 CST 2023,收到延迟队列的信息:消息来自ttl:哈哈哈2
05-27 17:55:59:760 INFO 12152 --- [ntContainer#0-1] com.cxf.hotel.listen.DelayedListen : 当前时间:Sat May 27 17:55:59 CST 2023,收到延迟队列的信息:消息来自ttl:哈哈哈1
4.延迟堆积
1.增加消费者,提高消费速度
2.在消费者内开启线程池加快消息处理速度
3.扩大队列容积,提高堆积上限,采用惰性队列
3.1在声明队列的时候可以设置属性x-queue-mode为lazy,即为惰性队列
3.2基于磁盘存储,消息上线高
3.3性能比较稳定,但基于磁盘存储,受限于磁盘IO,失效性降低