可靠性投递
消息丢失
如图所示,4个地方可能会出现消息丢失的情况
1、生产者发消息到broker,需要消费者应答
服务端天威延迟或者队列满了会倒是消息发送失败
RabbitMq提供了两种服务端确认模式
1、事务模式
利用txCommit 提交消息
异常时txRollback回滚
缺点:阻塞式。,一条消息没有发送完毕,不能发送下一条消息,降低了性能
try {
channel.txSelect();
// 发送消息,发布了4条,但只确认了3条
// String exchange, String routingKey, BasicProperties props, byte[] body
channel.basicPublish("", QUEUE_NAME, null, (msg).getBytes());
channel.txCommit();
channel.basicPublish("", QUEUE_NAME, null, (msg).getBytes());
channel.txCommit();
channel.basicPublish("", QUEUE_NAME, null, (msg).getBytes());
channel.txCommit();
channel.basicPublish("", QUEUE_NAME, null, (msg).getBytes());
int i =1/0;
channel.txCommit();
channel.basicPublish("", QUEUE_NAME, null, (msg).getBytes());
channel.txCommit();
System.out.println("消息发送成功");
} catch (Exception e) {
channel.txRollback();
System.out.println("消息已经回滚");
}
在Springboot中的设置
RabbitTemplate.setChannelTransacted(true)
2、Confirm模式
本质上是mq发送了ack的应答,确认模式有三种
1、普通确认模式
// 开启发送方确认模式
channel.confirmSelect();
channel.basicPublish("", QUEUE_NAME, null, msg.getBytes());
// 普通Confirm,发送一条,确认一条
if (channel.waitForConfirms()) {
System.out.println("消息发送成功" );
}else{
System.out.println("消息发送失败");
}
2、批量确认模式
批量发送,有一条失败,全部失败
try {
channel.confirmSelect();
for (int i = 0; i < 5; i++) {
// 发送消息
// String exchange, String routingKey, BasicProperties props, byte[] body
channel.basicPublish("", QUEUE_NAME, null, (msg +"-"+ i).getBytes());
}
// 批量确认结果,ACK如果是Multiple=True,代表ACK里面的Delivery-Tag之前的消息都被确认了
// 比如5条消息可能只收到1个ACK,也可能收到2个(抓包才看得到)
// 直到所有信息都发布,只要有一个未被Broker确认就会IOException
channel.waitForConfirmsOrDie();
System.out.println("消息发送完毕,批量确认成功");
} catch (Exception e) {
// 发生异常,可能需要对所有消息进行重发
e.printStackTrace();
}
3、异步确认
已经确认的消息,会被剔除掉
// 用来维护未确认消息的deliveryTag
final SortedSet<Long> confirmSet = Collections.synchronizedSortedSet(new TreeSet<Long>());
// 这里不会打印所有响应的ACK;ACK可能有多个,有可能一次确认多条,也有可能一次确认一条
// 异步监听确认和未确认的消息
// 如果要重复运行,先停掉之前的生产者,清空队列
channel.addConfirmListener(new ConfirmListener() {
public void handleNack(long deliveryTag, boolean multiple) throws IOException {
System.out.println("Broker未确认消息,标识:" + deliveryTag);
if (multiple) {
// headSet表示后面参数之前的所有元素,全部删除
confirmSet.headSet(deliveryTag + 1L).clear();
} else {
confirmSet.remove(deliveryTag);
}
// 这里添加重发的方法
}
public void handleAck(long deliveryTag, boolean multiple) throws IOException {
// 如果true表示批量执行了deliveryTag这个值以前(小于deliveryTag的)的所有消息,如果为false的话表示单条确认
System.out.println(String.format("Broker已确认消息,标识:%d,多个消息:%b", deliveryTag, multiple));
if (multiple) {
// headSet表示后面参数之前的所有元素,全部删除
confirmSet.headSet(deliveryTag + 1L).clear();
} else {
// 只移除一个元素
confirmSet.remove(deliveryTag);
}
System.out.println("未确认的消息:"+confirmSet);
}
});
// 开启发送方确认模式
channel.confirmSelect();
for (int i = 0; i < 10; i++) {
long nextSeqNo = channel.getNextPublishSeqNo();
// 发送消息
// String exchange, String routingKey, BasicProperties props, byte[] body
channel.basicPublish("", QUEUE_NAME, null, (msg +"-"+ i).getBytes());
confirmSet.add(nextSeqNo);
}
System.out.println("所有消息:"+confirmSet);
2、交换机找不到队列
队列写错了,绑定键写错了等等
1、添加监听器 true+returnListner 回发监听器 ,找不到接受队列,就发回给生产者
channel.addReturnListener(new ReturnListener() {
public void handleReturn(int replyCode,
String replyText,
String exchange,
String routingKey,
AMQP.BasicProperties properties,
byte[] body)
throws IOException {
System.out.println("=========监听器收到了无法路由,被返回的消息============");
System.out.println("replyText:"+replyText);
System.out.println("exchange:"+exchange);
System.out.println("routingKey:"+routingKey);
System.out.println("message:"+new String(body));
}
});
AMQP.BasicProperties properties = new AMQP.BasicProperties.Builder().deliveryMode(2).
contentEncoding("UTF-8").build();
channel.exchangeDeclare("TEST_EXCHANGE","topic", false, false, false, null);
channel.basicPublish("TEST_EXCHANGE","qingshan2673",true, properties,"只为更好的你".getBytes());
2、备份交换机
找不到交换机时发送到备份交换机
AMQP.BasicProperties properties = new AMQP.BasicProperties.Builder().deliveryMode(2).
contentEncoding("UTF-8").build();
// 备份交换机
channel.exchangeDeclare("ALTERNATE_EXCHANGE","topic", false, false, false, null);
channel.queueDeclare("ALTERNATE_QUEUE", false, false, false, null);
channel.queueBind("ALTERNATE_QUEUE","ALTERNATE_EXCHANGE","#");
// 在声明交换机的时候指定备份交换机
Map<String,Object> arguments = new HashMap<String,Object>();
arguments.put("alternate-exchange","ALTERNATE_EXCHANGE");
channel.exchangeDeclare("TEST_EXCHANGE","topic", false, false, false, arguments);
// 发送到了默认的交换机上,由于没有任何队列使用这个关键字跟交换机绑定,所以会被退回
// 第三个参数是设置的mandatory,如果mandatory是false,消息也会被直接丢弃
channel.basicPublish("TEST_EXCHANGE","qingshan2673",true, properties,"只为更好的你".getBytes());
3、消息在队列里一直没消息,怎么保持不丢
声明交换机队列时持久化,durable这个参数 ,设置为true
消息持久化
AMQP.BasicProperties properties = new AMQP.BasicProperties.Builder().deliveryMode(2).
contentEncoding("UTF-8").build();
deliveryMode(2) 表示消息持久化
4、消费者发送ACK
消费者:自动发送ack
channel.basicConsume(QUEUE_NAME, true, consumer);
这里表示接收到消息就返回true,并不会等到消息中的业务处理完才返回ack
如果需要再业务结束后返回,可以在业务结束后手动调用basicAck方法
业务在全局配置里设置
spring.rabbitmq.listener.simple.acknowledge-mode=auto
manual是手动ack
none 是自动ack
auto 不发生异常才会返回ack
mq拒绝策略
basicReject方法里的requeue=true 表示把消息丢回队列
basicNack 批量拒绝
if (msg.contains("拒收")){
// 拒绝消息
// requeue:是否重新入队列,true:是;false:直接丢弃,相当于告诉队列可以直接删除掉
// TODO 如果只有这一个消费者,requeue 为true 的时候会造成消息重复消费
channel.basicReject(envelope.getDeliveryTag(), true);
} else if (msg.contains("异常")){
// 批量拒绝
// requeue:是否重新入队列
// TODO 如果只有这一个消费者,requeue 为true 的时候会造成消息重复消费
channel.basicNack(envelope.getDeliveryTag(), true, false);
} else {
// 手工应答
// 如果不应答,队列中的消息会一直存在,重新连接的时候会重复消费
channel.basicAck(envelope.getDeliveryTag(), true);
}
生产者如何知道消息被消费了
1、消费者受到消息,处理完毕,调用发送者API;太low
2、发送一条响应消息给发送者
补偿机制
如果一致没有收到消费者回发的消息,可以重发消息,间隔时间发送
消息幂等
可以以messageId主键判断是否重复消费
消息顺序性
一个队列只有一个消费者时是顺序消费
集群高可用
搭建
略
实践经验总结
资源管理
交换机、队列一般是消费者创建
配置文件与命名规范
虚拟机:XXX_Vhost
交换机:XXX_EXCHANGE
队列:QUEUE
信息落库+定时任务重新发送
生产环境运维监控
zabbix+grafana 监控
主要关注:磁盘、内存、连接数
日志追踪
Firehose