RabbitMQ-rabbitMq各个特性的使用(三)
准备
1.引入客户端和配置文件依赖类
<dependency> <groupId>com.rabbitmq</groupId> <artifactId>amqp-client</artifactId> <version>5.4.3</version> </dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
2.properties文件配置
spring.rabbitmq.host=127.0.0.1 spring.rabbitmq.port=5672 spring.rabbitmq.username=liqiang spring.rabbitmq.password=liqiang
3.Test父类
@TestPropertySource("classpath:application.properties") public class BaseTest { ConnectionFactory connectionFactory; @Autowired RabbitMQConfig rabbitMQConfig; public BaseTest(){ } public void initConnectionFactory(){ if(connectionFactory==null) { connectionFactory = new ConnectionFactory(); connectionFactory.setUsername(rabbitMQConfig.getUsername()); connectionFactory.setPassword(rabbitMQConfig.getPassword()); connectionFactory.setHost(rabbitMQConfig.getHost()); connectionFactory.setPort(rabbitMQConfig.getPort()); } } public Connection newConnection() throws IOException, TimeoutException { initConnectionFactory(); return connectionFactory.newConnection(); } }
manadatory参数
说明
当次参数设置为true时 交换器无法根据自身类型和路由键找到符合条件的队列name将通过Basic.Retrun命令将消息返回给生产者 为false则直接丢弃
例子
String exchangeName = "test"; String queueName = "testQueue"; String routingKey = "testRoutingKey"; Connection connection = newConnection(); //声明一个channel一个连接可以监听多个channel 连接复用 Channel channel = connection.createChannel(); //声明一个名字为test 非自动删除的 direct类型的exchange 更多配置书37页 channel.exchangeDeclare(exchangeName, "direct", true); //声明一个持久化,非排他,非自动删除的队列 channel.queueDeclare(queueName, true, false, false, null); //将队列与交换器绑定 channel.queueBind(queueName, exchangeName, routingKey); //mandatory设置为true 如果根据routing key找不到队列则会回调通知 false则直接丢弃(这里将routing key设置为""字符串)运行则会触发通知 channel.basicPublish(exchangeName, "", true, MessageProperties.PERSISTENT_TEXT_PLAIN, "你好呀".getBytes()); //未名字路由的回调 channel.addReturnListener(new ReturnListener() { @Override public void handleReturn(int replyCode, String replyText, String exchange, String routingKey, AMQP.BasicProperties properties, byte[] body) throws IOException { System.out.println("回调通知" + new String(body)); } });
imanadatory参数
当immediate为true时如果队列没有消费者 则会通过Basic.Retrun返回 3.0已经移除
备份交换器
说明
路由不成功的时候 不是返回给生产者 而是存放到指定队列
demo
public void backupsTest() throws IOException, TimeoutException { String exchangeName = "test"; //备份exchange String backupsExchange = "testBackup"; String queueName = "testQueue"; //备份队列名字 String backupsQueueName = "backupsTestQueue"; String routingKey = "testRoutingKey"; //设置参数 Map<String, Object> args = new HashMap<>(); args.put("alternate-exchange", backupsExchange); Connection connection = newConnection(); //声明一个channel一个连接可以监听多个channel 连接复用 Channel channel = connection.createChannel(); //声明一个exchange并指定备份exchange 如果路由失败则路由到备份exchange channel.exchangeDeclare(exchangeName, BuiltinExchangeType.DIRECT, true, false, args); //声明备份exchage fanout类型 是因为 备份不需要路由key channel.exchangeDeclare(backupsExchange, BuiltinExchangeType.FANOUT, true, false, null); //声明一个持久化,非排他,非自动删除的队列 channel.queueDeclare(queueName, true, false, false, null); //将队列与交换器绑定 channel.queueBind(queueName, exchangeName, routingKey); //声明备份队列 channel.queueDeclare(backupsQueueName, true, false, false, null); //备份队列交换器是fanout类型 所以不需要routingkey channel.queueBind(backupsQueueName, backupsExchange, ""); channel.basicPublish(exchangeName, "", true, MessageProperties.PERSISTENT_TEXT_PLAIN, "你好呀".getBytes()); //不会触发回调通知 channel.addReturnListener(new ReturnListener() { @Override public void handleReturn(int replyCode, String replyText, String exchange, String routingKey, AMQP.BasicProperties properties, byte[] body) throws IOException { System.out.println("回调通知" + new String(body)); } }); }
备份交换器几种情况
- 如果设置备份交换器不存在 则消息会丢失 服务器不会报错
- 如果备份交换器没有绑定任何队列客户端和rabbitMq则客户端和服务端都不会出现 消息会丢失
- 如果备份交换器没有绑定任何队列客户端和rabbitMq都不会出现异常情况 消息会丢失
- 如果备份交换器和manadatory一起使用 则manadatory无效
过期消息
/** * mq消息过期测试 书60页 * 通过直接给队列设置 则消息到了过期日期则自动移除 * 通过每条消息单独设置 消息过期不会马上移除,而是消费的时候判断是否过期 才移除 */ //@Test public void messageTTLTest() throws IOException, TimeoutException { String exchangeName = "test"; String queueName = "testQueue"; String routingKey = "testRoutingKey"; Connection connection = newConnection(); //声明一个channel一个连接可以监听多个channel 连接复用 Channel channel = connection.createChannel(); //声明一个名字为test 非自动删除的 direct类型的exchange 更多配置书37页 channel.exchangeDeclare(exchangeName, "direct", true); //设置参数 Map<String, Object> args = new HashMap<>(); //设置单位 毫秒 超时没消费则会被丢弃 如果设置为0 则如果有消费者直接投递 没有消费者则丢弃 可以替代imanadatory
args.put("x-message-ttl", 6000); //声明一个持久化,非排他,非自动删除的队列 并设置整个队列消息的过期时间 channel.queueDeclare(queueName, true, false, false, args); //将队列与交换器绑定 channel.queueBind(queueName, exchangeName, routingKey); //mandatory设置为true 如果根据routing key找不到队列 则会回调通知 false则直接丢弃 channel.basicPublish(exchangeName, routingKey, true, MessageProperties.PERSISTENT_TEXT_PLAIN, "你好呀".getBytes()); //针对单条消息过期时间设置 // AMQP.BasicProperties.Builder builder=new AMQP.BasicProperties.Builder(); // builder.deliveryMode(2);//持久化消息 // builder.expiration("6000");//设置ttl为6000 // AMQP.BasicProperties properties=builder.build(); // channel.basicPublish(exchangeName,routingKey,true,properties,"你好呀".getBytes()); }
死信队列(DLX)
说明
当一个消息由一个交换器变成死信后他会重新发送到另外一个交换器(称之为死信交换器)改交换器绑定的队列就是死信队列
消息变成死信的几种情况
- 消息拒绝Basic/Reject 并设置requeue为false
- 消息过期
- 队列达到最大长度
demo
死信队列可以用于异常消息记录,如因为异常导致消息不能消费的放到死信队列后续排查(不过一般都是记录到消息消费表).或者是实现延迟消息
/** * 死信队列 DLX 书63页 通过ttl加死信 可以实现延迟消息 如果订单半小时没支付关闭 * 说明:死信队列本质也是队列,绑定死信交换器的队列叫死信队列 * 以下三种情况将发送到死信交换器 * 消息被拒绝 并设置 requeue为false * 消息过期(这里以消息过期为例子) * 队列达到最大长度 */ //@Test public void queueDLXTest() throws IOException, TimeoutException { String exchangeName = "test"; //死信交换器名字 String dlxExchangeName = "dlx_exchange"; String queueName = "testQueue"; //死信队列名字 String dlxQueueName = "dlxQueueName"; String routingKey = "testRoutingKey"; Connection connection = newConnection(); //声明一个channel一个连接可以监听多个channel 连接复用 Channel channel = connection.createChannel(); //声明一个名字为test 非自动删除的 direct类型的exchange 更多配置书37页 channel.exchangeDeclare(exchangeName, "direct", true); //声明一个交换器 用于死信队列 channel.exchangeDeclare(dlxExchangeName, "direct", true); //为死信队列绑定一个队列 channel.queueDeclare(dlxQueueName, true, false, false, null); //将队列与交换器绑定 channel.queueBind(dlxQueueName, dlxExchangeName, routingKey); //设置参数 Map<String, Object> args = new HashMap<>(); //设置单位 毫秒 超时没消费则会被丢弃 如果设置为0 则如果有消费者直接投递 没有消费者则丢弃 args.put("x-message-ttl", 6000); //指定对应的死信交换器 args.put("x-dead-letter-exchange", dlxExchangeName); //可以为死信交换器指定路由key如果不指定 则默认使用原routingkey //args.put("x-dead-letter-routing-key","dlx-routing-key"); //声明一个持久化,非排他,非自动删除的队列 并设置整个队列消息的过期时间 channel.queueDeclare(queueName, true, false, false, args); //将队列与交换器绑定 channel.queueBind(queueName, exchangeName, routingKey); //mandatory设置为true 如果根据routing key找不到队列 则会回调通知 false则直接丢弃 channel.basicPublish(exchangeName, routingKey, true, MessageProperties.PERSISTENT_TEXT_PLAIN, "你好呀".getBytes()); }
队列过期
指定时间没有被使用则自动移除,队列上没有任何消费者,队列没有被重新声明,并且在过期时间段内也没有调用Basic.Get命令
/** * 队列过期测试 * 队列指定时间没有被使用则移除 */ //@Test public void queueTTLTest() throws IOException, TimeoutException { //设置参数 Map<String, Object> args = new HashMap<>(); //如果队列6秒没被使用则移除 args.put("x-expires", 6000); String exchangeName = "test"; String queueName = "queueTTl"; String routingKey = "testRoutingKey"; Connection connection = newConnection(); //声明一个channel一个连接可以监听多个channel 连接复用 Channel channel = connection.createChannel(); //队列6秒没被使用则移除 channel.queueDeclare(queueName, true, false, false, args); }
延迟队列
说明
处理类似订单30分钟未支付自动关闭这种需求,或者延迟发短信 通过TTL+DLX实现
也可以通过Delayed Message 插件实现
demo
/** * 死信队列 DLX 书63页 通过ttl加死信 可以实现延迟消息 如果订单半小时没支付关闭 * 说明:死信队列本质也是队列,绑定死信交换器的队列叫死信队列 * 以下三种情况将发送到死信交换器 * 消息被拒绝 并设置 requeue为false * 消息过期(这里以消息过期为例子) * 队列达到最大长度 */ //@Test public void queueDLXTest() throws IOException, TimeoutException { String exchangeName = "test"; //死信交换器名字 String dlxExchangeName = "dlx_exchange"; String queueName = "testQueue"; //死信队列名字 String dlxQueueName = "dlxQueueName"; String routingKey = "testRoutingKey"; Connection connection = newConnection(); //声明一个channel一个连接可以监听多个channel 连接复用 Channel channel = connection.createChannel(); //声明一个名字为test 非自动删除的 direct类型的exchange 更多配置书37页 channel.exchangeDeclare(exchangeName, "direct", true); //声明一个交换器 用于死信队列 channel.exchangeDeclare(dlxExchangeName, "direct", true); //为死信队列绑定一个队列 channel.queueDeclare(dlxQueueName, true, false, false, null); //将队列与交换器绑定 channel.queueBind(dlxQueueName, dlxExchangeName, routingKey); //设置参数 Map<String, Object> args = new HashMap<>(); //设置单位 毫秒 超时没消费则会被丢弃 如果设置为0 则如果有消费者直接投递 没有消费者则丢弃 args.put("x-message-ttl", 6000); //指定对应的死信交换器 args.put("x-dead-letter-exchange", dlxExchangeName); //可以为死信交换器指定路由key如果不指定 则默认使用原routingkey //args.put("x-dead-letter-routing-key","dlx-routing-key"); //声明一个持久化,非排他,非自动删除的队列 并设置整个队列消息的过期时间 channel.queueDeclare(queueName, true, false, false, args); //将队列与交换器绑定 channel.queueBind(queueName, exchangeName, routingKey); //mandatory设置为true 如果根据routing key找不到队列 则会回调通知 false则直接丢弃 channel.basicPublish(exchangeName, routingKey, true, MessageProperties.PERSISTENT_TEXT_PLAIN, "你好呀".getBytes()); }
优先级队列
说明
1.基于队列维度,优先级高的具有高的优先权
2.基于消息,优先级高的消息具有优先被消费的特权(注:仅限于消息堆积的情况)
基于消息例子
/** * 只针对队列里面有阻塞情况下 优先级 不然发送一条消费一条优先级就没有意义 * 可以在管理页面同时get10条看是否是有序的 */ @Test public void priorityTest() throws IOException, TimeoutException { String exchangeName = "test"; String queueName = "testQueue"; String routingKey = "testRoutingKey"; Connection connection = newConnection(); //设置参数 Map<String, Object> args = new HashMap<>(); //优先级最大标识 args.put("x-max-priority", 10); //声明一个channel一个连接可以监听多个channel 连接复用 Channel channel = connection.createChannel(); //声明一个名字为test 非自动删除的 direct类型的exchange 更多配置书37页 channel.exchangeDeclare(exchangeName, "direct", true); //声明一个持久化,非排他,非自动删除的队列 channel.queueDeclare(queueName, true, false, false, args); //将队列与交换器绑定 channel.queueBind(queueName, exchangeName, routingKey); // for (int i = 0; i <= 10; i++) { AMQP.BasicProperties.Builder builder = new AMQP.BasicProperties.Builder(); int priority = new Random().nextInt(10); builder.priority(priority);//用于优先级序列 AMQP.BasicProperties properties = builder.build(); //mandatory设置为true 如果根据routing key找不到队列 则会回调通知 false则直接丢弃 channel.basicPublish(exchangeName, routingKey, true, properties, ("你好呀" + priority).getBytes()); channel.addReturnListener(new ReturnListener() { @Override public void handleReturn(int replyCode, String replyText, String exchange, String routingKey, AMQP.BasicProperties properties, byte[] body) throws IOException { System.out.println("回调通知" + new String(body)); } }); } }
基于队列例子
Map<String,Object>args=new HashMap<String,Object>(); args.put("x-max-priority",10); channel.queue Declare("queue.priority",true,false,false,args) ;
消息持久化
说明
- 交换器持久化(未设置持久化,那么在重启后相关交换器的元数据会丢失,但是相关队列不会丢失)
- 队列持久化(未设置持久化话,重启后队列会消失,队列消息也会丢失)
- 消息持久化(如果队列是持久化 消息也是持久化重启后消息不会消失)
注意:如果将交换器/队列/消息都设置持久化 会降低消息的吞吐量 需要在可靠性和吞吐量做权衡
场景1:autoack 消费者受到消息还没来得及处理就挂掉了
场景2:服务发送方发送消息到mq 消息还没来得及写入磁盘就挂了(写入磁盘和写入内存是异步的)
解决方案
1:解决此功能是引入MQ的镜像队列 如果master挂了 快速且切到从(并不能完全保证 但是可靠性会高很多 生产环境一般都是镜像队列)
2.通过事物消息 但是事物消息性能很低因为发送消息比普通发送消息多了几个步骤(书:75)
3.通过发送方ack确认的方式(需要保证消费端幂等性 因为网络原因可能未能正确收到ack)
4.ack有2种 一种是同步一种是异步 异步性能优于同步(队列和消息设置了持久化 name将在成功落盘后才能收到ack)
事物消息
解决服务器异常崩溃而导致的消息丢失,或者没有正确到达服务器
可以保证消息不会丢失但是性能很低
public Channel createChannel() throws IOException, TimeoutException { String exchangeName = "test"; String queueName = "testQueue"; String routingKey = "testRoutingKey"; Connection connection = newConnection(); //声明一个channel一个连接可以监听多个channel 连接复用 Channel channel = connection.createChannel(); //声明一个名字为test 非自动删除的 direct类型的exchange 更多配置书37页 channel.exchangeDeclare(exchangeName, "direct", true); //声明一个持久化,非排他,非自动删除的队列 channel.queueDeclare(queueName, true, false, false, null); //将队列与交换器绑定 channel.queueBind(queueName, exchangeName, routingKey); return channel; }
public void transactionCommit() throws IOException, TimeoutException { Channel channel = createChannel(); try { //向broker发送tx.select指令 将信道设置为事物模式 broker响应tx.select-ok表示设置成功 channel.txSelect(); //在事物信道执行发送消息指令 可以多个 channel.basicPublish("test","testRoutingKey",MessageProperties.PERSISTENT_TEXT_PLAIN,"滴滴".getBytes()); //向broker发送tx.commit执行 broker响应tx.commit-OK表示成功成功才会落盘 channel.txCommit(); } catch (Exception e) { e.printStackTrace(); //向broker发送tx.rollback broker响应tx.rollback-ok表示成功 channel.txRollback(); } }
AMQP交互过程
1.客户端发送Tx.Select 将信道设置为事物模式
2.Broker恢复Tx.Select-Ok确认已将信道设置为事物模式
3.在发送完消息之后,客户端发送Tx.Commit提交事物
4.Broker恢复Tx.Commit-Ok确认事物提交
生产者ACK
解决服务器异常崩溃而导致的消息丢失,或者没有正确到达服务器
与事物消息不同的是
1.事物消息发送后会使发送端阻塞,等待信道返回确认后 同时继续发送一下一条消息。
2.ACK模式消息发送后,可以在等待信道返回消息确认的同时继续发送下一条消息
原理
将信道设置为ack模式 所有在此信道上面发送的消息都会分配一个唯一id 当消息投递到指定队列后 将会在回传的deliveryTag包含此消息
channel.basicAck(消费端ack)的 multiple参数表示这个序号之前的都已经确认进行批量确认
如果设置了持久化 将在写入磁盘后ack通知
同步ack
/** * 同步ack * 注意事物信道和confirm信道不能共存 */ //@Test public void synchroAck() throws IOException, TimeoutException, InterruptedException { Channel channel = createChannel(); //发送tx.configSelect 将信道设置为publisher confirm模式 channel.confirmSelect(); //在confirm信道发送消息指令 如果多个 则将channel.basicPublish channel.waitForConfirms包在循环里面 channel.basicPublish("test","testRoutingKey",MessageProperties.PERSISTENT_TEXT_PLAIN,"滴滴".getBytes()); //同步等待ack如果非confirm模式 调用此方法会报错 4个重载 书79页 if(channel.waitForConfirms()){ System.out.println("发送消息成功"); } }
批量发送
/** * 批量发送 * 性能优于上面一种方式 * @throws IOException * @throws TimeoutException */ //@Test public void batchSynchroAck() throws IOException, TimeoutException { Channel channel=createChannel(); AMQP.Confirm.SelectOk selectOk=channel.confirmSelect(); try{ for(int i=0;i<10;i++){ channel.basicPublish("test","testRoutingKey",MessageProperties.PERSISTENT_TEXT_PLAIN,("滴滴"+i).getBytes()); } /** * 发送一条消息会生成一个ID(从1开始) mq会回传ack或者nack 客户端里面做了处理 basicpublish内部维护一个SortedSet 回传一个ack则移除一个 实现批量监听ack消息 * 其中有一条未确认就会抛异常 * 缺点是其中一条失败 则全部要重发,所以批次不能太大 */ if(channel.waitForConfirms()){ System.out.println("发送消息成功"); } }catch (InterruptedException e){ /** * 重发逻辑 */ } }
异步ack
demo异步方式优于前面2种方式
public void AsynchroAck() throws IOException, TimeoutException, InterruptedException { Channel channel=createChannel(); channel.confirmSelect(); SortedSet confirmSet=new TreeSet(); /** * 注意 比如你发10条 不一定回调10次 因为id从1开始 如果回调一次10 表示签名都被确认 */ channel.addConfirmListener(new ConfirmListener() { @Override public void handleAck(long deliveryTag, boolean multiple) throws IOException { System.out.println("已确认"+deliveryTag); //为true表示批量确认 if(multiple){ //小于e 之前的元素不包括e SortedSet ackSet=confirmSet.headSet(deliveryTag+1); ackSet.clear(); }else{ //删除deliveryTag对象 confirmSet.remove(deliveryTag); } } @Override public void handleNack(long deliveryTag, boolean multiple) throws IOException { //为true表示批量NACK if(multiple){ //清除之前的 /** * confirmSet.headSet(deliveryTag+1)重发只需要获得当前元素和之前的就行了 */ //小于e 之前的元素不包括e SortedSet ackSet=confirmSet.headSet(deliveryTag+1); ackSet.clear(); }else{ confirmSet.remove(deliveryTag); } //处理消息重发逻辑 } }); channel.addReturnListener(new ReturnListener() { @Override public void handleReturn(int replyCode, String replyText, String exchange, String routingKey, AMQP.BasicProperties properties, byte[] body) throws IOException { System.out.println("returnListener"+replyCode); } }); for(int i=0;i<10;i++){ System.out.println("消息"+i+"正在发送"); channel.basicPublish("test","testRoutingKey",MessageProperties.PERSISTENT_BASIC,("滴滴"+i).getBytes()); confirmSet.add(channel.getNextPublishSeqNo());//获得生成的序号 } Thread.sleep(10000); System.out.println("未确认"+confirmSet.size()); }
消息分发
/** * 消息分发 * 当一个队列拥有多个消费者的时候.模式是通过轮询的方式分发(有消费者n 当前第m条消息发送给 m%n的方式确认消费者) * 但是有个缺点,当某个消费者任务繁重来不及消费消息 则uack消息会堆叠再那里 导致整体消息处理吞吐量下降 *可以通过设置Qos 当uack消息到达一定限量后将不再给当前消息发送消息 每次ack后-1 才继续发 * 此参数对拉模式的消费模式无效 */ // @Test public void basicQos() throws IOException, TimeoutException, InterruptedException { Channel channel=createChannel(); //内部会维护一个计数每推送一条消息+1 ack后-1 到达上限后将不推送 /** * 一个 channel可以定义多个消费者 重载可以通过global来确认是否是用于整个信道 * channel.basicQos(3,true); * channel.basicQos(5,false); * 如果设置了true和false呢 那么表示多个消费者最多收到3个 +起来不超过5 */ channel.basicQos(5);//默认0表示不限量 channel.basicConsume("testQueue",false,new DefaultConsumer(channel){ @Override public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException { super.handleDelivery(consumerTag, envelope, properties, body); System.out.println(new String(body)+"已消费,deliveryTag:"+envelope.getDeliveryTag()); //channel.basicAck(envelope.getDeliveryTag(),false); } }); //可以通过管理页面发现unack是5 就没有再接收消息了 Thread.sleep(5000); }
消息顺序性
方案:消费端做处理
方案二 多少个消费端拆分成多少个队列一个队列被一个消费端消费.然后格局业务id 取%计算投递到具体队列