上一篇文章介绍了 Springboot 如何集成 RabbitMQ,同时也形成了最基本的实现,不过这个实现好像有一点点问题,记得还在菊厂培训的时候,有一篇培训 PPT 讲过,极其健壮的服务需要达到一年下来,平均只有一到两个小时不能提供服务,反过来就是说,程序可能会在这一两个小时中宕机,宕机意味着数据丢失。就例如我们讲到的 RabbitMQ,假如我们在处理某一条数据的过程中宕机了,程序没有完成处理过程不会正常返回,同时 RabbitMQ 上这条消息因为已经发送给消费者,所以不会保留,那么这条未处理完的数据是不是就丢失了。RabbitMQ 给了我们一个保护机制,手动回复 ack,上一篇文章使用了默认的自动 ack 就无法避免这个问题。
注:后面代码都是对上一篇文章的程序小小改动后测试的
修改手动回复 ack 配置文件
增加开启手动回复的配置
修改的配置信息如下
#rabbitmq相关配置
spring.rabbitmq.host=127.0.0.1
spring.rabbitmq.port=5672
spring.rabbitmq.username=guest
spring.rabbitmq.password=guest
# 开启发送失败退回
spring.rabbitmq.publisher-returns=true
# 开启 ACK,必须开启,否则回调没有通道信息
spring.rabbitmq.listener.simple.acknowledge-mode=manual
spring.rabbitmq.listener.direct.acknowledge-mode=manual
也可以手动设置
生产者
生产者调整为以 Message 的形式发送送至队列
@Component
public class HoshMQSender implements RabbitTemplate.ConfirmCallback, RabbitTemplate.ReturnCallback{
private RabbitTemplate rabbitTemplate;
@Autowired
public HoshMQSender(RabbitTemplate rabbitTemplate) {
this.rabbitTemplate = rabbitTemplate;
this.rabbitTemplate.setConfirmCallback(this);
this.rabbitTemplate.setReturnCallback(this);
}
public void send(String str) {
sendMessageWithAck(str);
}
private void sendMessageWithAck(String str) {
// 消息内容
byte[] toSendBytes = str.getBytes();
MessageProperties messageProperties = new MessageProperties();
messageProperties.setMessageId(String.valueOf(System.currentTimeMillis()));
Message msg = new Message(toSendBytes, messageProperties);
CorrelationData correlationData = new CorrelationData();
correlationData.setId(UUID.randomUUID().toString());
rabbitTemplate.convertAndSend(Constant.HOSH_TOPIC_EXC, Constant.HOSH_TOPIC, msg, correlationData);
}
// 队列内容发送到 MQ 的确认
@Override
public void confirm(CorrelationData correlationData, boolean b, String s) {
// if (b) {
// System.out.println("recv HoshMQSender confirm id=" + correlationData.getId());
// } else {
// System.out.println("not recv " + s);
// }
}
/**
* exchange 到达 queue, 则 returnedMessage 不回调
* exchange 到达 queue 失败, 则 returnedMessage 回调
* 需要设置spring.rabbitmq.publisher-returns=true
* @param message
* @param replyCode
* @param replyText
* @param exchange
* @param routingKey
*/
@Override
public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
System.out.println("sender return success" + message.toString()
+"\n replyCode "+replyCode+"\n replyText "+replyText
+"\n exchange "+exchange + "\n routingKey "+routingKey);
}
}
消费者
为了验证手动 ack 的效果,消费者会有回复 ack 和不回复 ack 的对比
1、不回复 ack 的消费者代码
@Component
public class HoshMqReceiver2 {
@RabbitHandler
@RabbitListener(queues = Constant.HOSH_TOPIC)
public void onReceiver(String info, Channel channel, Message msg) {
// try {
// // 开启手动应答 ack 以后,只有当程序明确回复,数据已经被处理,
// // 对应数据才会被 RabbitMQ server 清除,否则保留在 RabbitMQ server 上
// channel.basicAck(msg.getMessageProperties().getDeliveryTag(), false);
// System.out.println("HoshMqReceiver msg " + JSON.toJSONString(msg));
System.out.println("HoshMqReceiver info " + info);
// } catch (IOException e) {
// e.printStackTrace();
// }
}
}
测试一下向队列发送消息
可以看到我们还是受到了消息
2、回复 ack 的消费者代码块
@Component
public class HoshMqReceiver2 {
@RabbitHandler
@RabbitListener(queues = Constant.HOSH_TOPIC)
public void onReceiver(String info, Channel channel, Message msg) {
try {
// 开启手动应答 ack 以后,只有当程序明确回复,数据已经被处理,
// 对应数据才会被 RabbitMQ server 清除,否则保留在 RabbitMQ server 上
channel.basicAck(msg.getMessageProperties().getDeliveryTag(), false);
System.out.println("HoshMqReceiver msg " + JSON.toJSONString(msg));
System.out.println("HoshMqReceiver info " + info);
} catch (IOException e) {
e.printStackTrace();
}
}
}
再看一下测试结果,这次,我们只是直接运行了程序,还并没有向队列发送消息
大家应该发现了,如果消费者没有为对应的消息回复 ack,那么这条消息依旧会保存在队列里,直到收到了消费者对该条消息的 ack 后,才会从队列移除