1、消息的可靠性投递方案
- 消息落库,对消息状态进行标识(存储到消息数据库上。对状态进行修改。适合并发量不高的情况下)
- 消息延迟投递,做二次确认,回调检查(推荐使用。可以减少多次DB的存储)
消费幂等性:
利用Redis的原子性去实现
confirm确认消息
生产者(指定消息投递模式,并设置消息应答监听)
public class ProductConfirem {
public static void main(String[] args) throws IOException, TimeoutException {
Connection connection = ConnectionUtils.getConnection();
Channel channel = connection.createChannel();
//指定我们的消息投递模式:消息确认模式********************
channel.confirmSelect();
String exchange="confirem_muke";
String rokey="confirem.save";
channel.basicPublish(exchange,rokey,null,"这是个消息".getBytes());
//添加一个消息确认监听********************
channel.addConfirmListener(new ConfirmListener() {
/**
*
* @param l 第一个参数为这个消息的唯一标签
* @param b 是否批量的
* @throws IOException
*/
@Override
public void handleAck(long l, boolean b) throws IOException {
System.out.println("应答成功!"+l+"--->"+b);
}
@Override
public void handleNack(long l, boolean b) throws IOException {
System.out.println("应答失败!");
}
});
}
}
消费者(只需要消费消息就可以)
public class ConsumerConfirem {
public static void main(String[] args) throws IOException, TimeoutException {
Connection connection = ConnectionUtils.getConnection();
Channel channel = connection.createChannel();
String queuename="confirm_queue";
String exchange="confirem_muke";
//声明队列,声明交换机,并将队列绑定到交换机上
channel.queueDeclare(queuename,true,false,false,null);
channel.exchangeDeclare(exchange,"topic",true);
channel.queueBind(queuename,exchange,"confirem.#");
DefaultConsumer consumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.out.println(new String(body,"UTF-8"));
}
};
channel.basicConsume(queuename,true,consumer);
}
}
Return返回消息机制(多适用于没有找到路由key或者exchange)
如果设置return返回消息机制,就必须把Mandatory设置成true,监听器就会接收到路由不可达的消息。然后进行处理。如果为false了。那么broker端会自动删除消息。
生产者
public class ProductSend {
/**
* 测试消息发送失败调用return机制
* 1、首先消费者声明交换机的时,定义rokingkey为send.#。生产者故意写成aaa.save.这样就绝对不会路由到这个交换机上
* 2、定义returnLisener
* 3、channel.basicPublish()中的第三个参数Mandatory,必须设置成true,才会在发送失败后,触发return机制
*/
public static void main(String[] args) throws IOException, TimeoutException {
Connection connection = ConnectionUtils.getConnection();
Channel channel = connection.createChannel();
String exchangeName="test_send_exchange";
String roketingkey="aaa.save";
//添加监听,只要发送的消息失败,就会调用这个方法。
//但是必须在channel.basicPublish()的第三个参数设置成true,才会起作用
channel.addReturnListener(new ReturnListener() {
@Override
public void handleReturn(int replyCode, String replyText, String exchange, String routingKey, AMQP.BasicProperties basicProperties, byte[] bytes) throws IOException {
System.out.println("--------------------------------------------");
System.out.println("replyCode---->"+replyCode);
System.out.println("replyText---->"+replyText);
System.out.println("exchange---->"+exchange);
System.out.println("routingKey---->"+routingKey);
System.out.println("basicProperties---->"+basicProperties);
System.out.println("bytes---->"+bytes);
}
});
//必须为true,才会在消息发送失败后,触发上面的return机制
boolean mandatory=true;
channel.basicPublish(exchangeName,roketingkey,mandatory,null,"这是测试send消息".getBytes());
}
}
消费者(消费者没有什么可改变的。按照以前的方式写)
public class ConsumerSend {
public static void main(String[] args) throws IOException, TimeoutException {
Connection connection = ConnectionUtils.getConnection();
Channel channel = connection.createChannel();
String exchangeName="test_send_exchange";
String queueName="test_send_queue";
channel.queueDeclare(queueName,true,false,false,null);
channel.exchangeDeclare(exchangeName,"topic");
channel.queueBind(queueName,exchangeName,"send.#");
DefaultConsumer consumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.out.println("--->"+new String(body,"utf-8"));
}
};
channel.basicConsume(queueName,true,consumer);
}
}
消费端限流
为了防止大量消息在同一时间传给消费端。造成消费端故障。
生产者
public class ProductXl {
private static final String EXCHANGE_NAME="muke_xl_exchange";
public static void main(String[] args) throws Exception {
Connection connection = ConnectionUtils.getConnection();
Channel channel = connection.createChannel();
String key="topic.send";
for (int i = 0; i < 5; i++) {
channel.basicPublish(EXCHANGE_NAME,key,null,"消费端限流得到消息".getBytes());
}
channel.close();
connection.close();
}
}
消费者(设置接收个数,接收消息后给生产者应答。改自动应答为手动应答)
public class ConsumerXl {
private static final String EXCHANGE_NAME="muke_xl_exchange";
private static final String QUEUE_NAME="muke_xl_queue";
public static void main(String[] args) throws Exception {
Connection connection = ConnectionUtils.getConnection();
Channel channel = connection.createChannel();
channel.exchangeDeclare(EXCHANGE_NAME,"topic",true,false,false,null);
channel.queueDeclare(QUEUE_NAME,false,false,false,null);
channel.queueBind(QUEUE_NAME,EXCHANGE_NAME,"topic.#");
/**
* 第一个参数:限制消息大小
* 第二个参数:每次接收消息个数
* 第三个参数:不应用到channel级别,应用到整个consumer级别
*/
channel.basicQos(0,1, false);
DefaultConsumer consumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.out.println("----->"+new String(body,"utf-8"));
/**
* 消费完消息后,给生产者回应ack
* envelope.getDeliveryTag()代表当前消息数
* false代表不批量签收
*/
channel.basicAck(envelope.getDeliveryTag(),false);
}
};
//必须改成false,变成手动应答
boolean autoack=false;
channel.basicConsume(QUEUE_NAME,autoack,consumer);
}
}
消息重回队列
生产者
public class ProductDl {
private static final String EXCHANGE_NAME="muke_dl_exchange";
public static void main(String[] args) throws Exception {
Connection connection = ConnectionUtils.getConnection();
Channel channel = connection.createChannel();
String key="topic.send";
for (int i = 0; i < 5; i++) {
Map<String,Object> header=new HashMap<>();
header.put("num",i);
String msg="消费失败重回队列---》"+i;
//设置消息的附加属性,给消息加上header,我们更容易在消费者端进行判断
AMQP.BasicProperties properties=new AMQP.BasicProperties().builder()
.contentEncoding("UTF-8")
.expiration("2000")
.headers(header)
.build();
channel.basicPublish(EXCHANGE_NAME,key,properties,msg.getBytes());
}
}
}
消费者(重回队列必须是用basicNack,并将第三个参数设置成true。让当前消息重回队列中)
public class ConsumerDl {
private static final String EXCHANGE_NAME="muke_dl_exchange";
private static final String QUEUE_NAME="muke_dl_queue";
public static void main(String[] args) throws IOException, TimeoutException {
Connection connection = ConnectionUtils.getConnection();
Channel channel = connection.createChannel();
channel.exchangeDeclare(EXCHANGE_NAME,"topic",true,false,false,null);
channel.queueDeclare(QUEUE_NAME,false,false,false,null);
channel.queueBind(QUEUE_NAME,EXCHANGE_NAME,"topic.#");
DefaultConsumer consumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.out.println("----->"+new String(body,"utf-8"));
if((Integer)properties.getHeaders().get("num")==1){
/**
* **********重回队列是调用basicNack************
* 第三个参数,代表是否重回队列.重回队列也是重回队列尾部,按序执行
*/
channel.basicNack(envelope.getDeliveryTag(),false,true);
}else {
channel.basicAck(envelope.getDeliveryTag(),false);
}
}
};
//必须改成手动应答
boolean autoack=false;
channel.basicConsume(QUEUE_NAME,autoack,consumer);
}
}
TTL消息队列
TTL代表time to live的缩写,也就是生存时间。消息没被消费,在指定时间过期后自动清除(可以对消息设置过期时间。也可以设置对于某个队列中,只要进行这个队列的消息,在某个时间还没被消费,就会被过期删除)
AMQP.BasicProperties properties=new AMQP.BasicProperties().builder()
.contentEncoding("UTF-8")
//expiration就是设置过期时间。这是对于消息设置过期时间的
.expiration("2000")
.headers(header)
.build();
死信队列(DLX)
解释:当消息在一个队列中由于某个原因没被消费,这个消息就成为死信。变成死信之后,它能被重新publish到另一个exchange,而这个Exchange就称为死信队列。
DLX其实就是一个Exchange,它只是可以在任何队列进行绑定。当这个队列有 死信的时候,MQ就会自动将这个消息发送给这个Exchange,进行被路由到另一个队列中去。
成为死信的几种情况:
- 消息被拒绝:设置basic.Nack,并且第三个参数中的requeue还设置成false,不能重回队列。这个消息就成为死信
- 消息TTL过期
- 队列达到最大长度
设置死信队列前提:
首先需要设置死信队列的exchange和queue,然后进行绑定:
Exchange:dlx.exchange
Queue:dlx.queue
RoutingKey:# //代表所有死信消息都会被路由到这个队列上来
然后我们进行正常声明交换机、队列、绑定。只不过我们需要在队列上加一个参数:
argument.put("x-dead-letter-exchange","死信的exchange")
代码实现:在测试的时候,先启动consumer,把该声明的队列声明出来。然后关闭consumer端。启动product端,看管控台变化。是否10秒过后,消息没被消费,转而进入死信队列中。
生产者
public class ProductDlx {
public static void main(String[] args) throws IOException, TimeoutException {
Connection connection = ConnectionUtil.getConnection();
Channel channel = connection.createChannel();
String exchangeName="test_dxl_exchange";
String routingkey="dxl.save";
String msg="正常队列发送消息";
AMQP.BasicProperties properties=new AMQP.BasicProperties().builder()
//设置10秒过期,如果10秒不被消费,就会跑到死信队列上去
.expiration("10000")
.build();
channel.basicPublish(exchangeName,routingkey,properties,msg.getBytes());
channel.close();
connection.close();
}
}
消费者(声明普通队列,声明死信队列。并将死信队列绑定到普通队列上)
public class ConsumerDlx {
public static void main(String[] args) throws IOException, TimeoutException {
Connection connection = ConnectionUtil.getConnection();
Channel channel = connection.createChannel();
//这里是声明正常队列和exchange的信息。并不是死信队列的。要明确这一点
String exchangeName="test_dxl_exchange";
String routingkey="dxl.#";
String queueName="test_dxl_queue";
/**
* *******new 一个hashmap,argument.put("x-dead-letter-exchange","死信的exchange")。
* 这个后面的exchange名字就是真正死信队列的交换机。
* 这个map必须放在正常queue上。千万不要放在exchange上
*/
Map<String,Object> argument=new HashMap<>();
argument.put("x-dead-letter-exchange","dxl_exchange");
//***************将绑定的死信交换机,放在正常的queue上
channel.queueDeclare(queueName,true,false,false,argument);
channel.exchangeDeclare(exchangeName,"topic",true,false,null);
channel.queueBind(queueName,exchangeName,routingkey);
//声明死信交换机和死信队列.这里才是真正的死信队列声明。exchange名称必须跟上面绑定在正常queue上的exchange一样
String dxl_exchangeName="dxl_exchange";
String dxl_queueName="dxl_queue";
channel.queueDeclare(dxl_queueName,true,false,false,null);
channel.exchangeDeclare(dxl_exchangeName,"topic",true,false,null);
channel.queueBind(dxl_queueName,dxl_exchangeName,"#");
DefaultConsumer consumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.out.println("--->" + new String(body, "GBK"));
}
};
channel.basicConsume(queueName,true,consumer);
}
}