Centos6下RabbitMQ学习(一)——Java客户端收发消息、可靠性、确认

这部分学习客户端如何和消息队列交互,如何发送消息、接收消息等,官方文档如下

https://www.rabbitmq.com/tutorials/tutorial-one-java.html

0 Java程序准备

我们使用RabbitMQ官方提供的一个客户端包,添加maven依赖,后两个是日志框架,不加会报个警告,也不影响功能

<dependency>
    <groupId>com.rabbitmq</groupId>
    <artifactId>amqp-client</artifactId>
    <version>5.6.0</version>
</dependency>
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-api</artifactId>
    <version>1.7.5</version>
</dependency>
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-log4j12</artifactId>
    <version>1.7.5</version>
 </dependency>

1 发送、接收消息

现在声明一个新队列,并发送Hello World消息

import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;

public class Send {
    private final static String QUEUE_NAME = "hello";

    public static void main(String[] args) throws Exception {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("服务器IP");  // 设置服务器
        factory.setUsername("admin");  // 账号
        factory.setPassword("password");  // 密码
        try (Connection connection = factory.newConnection();  // 创建连接和通道
        Channel channel = connection.createChannel()) {
            channel.queueDeclare(QUEUE_NAME, false, false, false, null);  // 声明队列
            String message = "Hello World";
            channel.basicPublish("", QUEUE_NAME, null, message.getBytes());  // 创建新生产者,发送消息
            System.out.println(" [x] Sent '" + message + "'");
        }
    }
}

现在web页面上能看见这个名为hello的队列

 

消息已经成功在队列里,现在写一个消费端把消息取出来

import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.DeliverCallback;

public class Recv {
    private final static String QUEUE_NAME = "hello";

    public static void main(String[] args) throws Exception {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("服务器IP");  // 设置服务器
        factory.setUsername("admin");  // 账号
        factory.setPassword("password");  // 密码
        Connection connection = factory.newConnection();  // 创建连接和通道
        Channel channel = connection.createChannel();
        System.out.println(" [*] Waiting for messages. To exit press CTRL+C");
        DeliverCallback deliverCallback = (consumerTag, delivery) -> {  // 回调函数,从消息队列接收消息后执行
            String message = new String(delivery.getBody(), "UTF-8");
            System.out.println(" [x] Received '" + message + "'");
        };
        channel.basicConsume(QUEUE_NAME, true, deliverCallback, consumerTag -> {});  // 创建新消费者,等待消息
    }
}

2 消费可靠性

生产者和消费者都需要一个机制来确保消息传输的可靠性,在协议AMQP 0-9-1中称为confirm(生产者)和acknowledgement(消费者)

消息队列给消费者交付一个消息时,需要考虑什么情况下可以判断消息已经成功发送。协议AMQP 0-9-1认为消费者调用了basic.consume方法或者basic.get API就是消息成功发送的标志。RabbitMQ交付消息时,有两种acknowledgement模式。第一种模式叫手工确认,客户端接收消息后,发送一个确认消息给RabbitMQ,然后RabbitMQ才会认为交付完成,删除消息;第二种模式叫自动确认,当RabbitMQ把消息发送出去后即认为消息成功交付,原消息会被删除,如果此时客户端链接断开或者客户端应用程序狗带,这个消息就丢失了,所以自动确认是不安全的一种模式。此外手工确认模式对通道的预取数量有限制(QoS),自动确认模式没有限制,可能会造成一些性能问题,比如客户端赶不上消息交付速度,过多的未确认tag会占节点内存。消息的发送和确认必须在同一个通道中完成

1.1中的代码为自动确认模式

手工确认模式下,如果出现TCP连接断开、消费者程序挂掉等各种异常,那么未被确认的消息会重新入队。因此消费者必须具备处理重复消息的能力,包括一个消费者收到另一个消费者之前接收到的消息这种情况。重新发送的消息redeliver这个属性为true

手工确认有正负之分,正确认表示消息成功接收处理,可以删除该消息,负确认表示消息还没有被处理,但同样可以删除该消息

2.1 手工正确认

手工正确认调用的方法是basic.ack,第一个参数表示要确认的deliveryTag,第二个参数表示是否允许批确认。一个手工正确认的消费者代码如下,开启后,多点几次发送,deliveryTag分别是1、2、3这样递增的正整数。deliveryTag是用来在一个通道内区分各个分发消息的标志

import com.rabbitmq.client.*;

import java.io.IOException;

public class RecvWithAck {
    private final static String QUEUE_NAME = "hello";

    public static void main(String[] args) throws Exception {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("服务器IP");  // 设置服务器
        factory.setUsername("admin");  // 账号
        factory.setPassword("password");  // 密码
        boolean autoAck = false;  // 不使用自动确认
        Connection connection = factory.newConnection();  // 创建连接和通道
        Channel channel = connection.createChannel();
        System.out.println(" [*] Waiting for messages. To exit press CTRL+C");
        Consumer consumer = new DefaultConsumer(channel) {
            @Override
            // 处理消息
            public void handleDelivery(String s, Envelope envelope, AMQP.BasicProperties basicProperties, byte[] bytes) throws IOException {
                long deliveryTag = envelope.getDeliveryTag();
                channel.basicAck(deliveryTag, false);
                System.out.println("deliveryTag is " + deliveryTag);
                System.out.println("message is " + new String(bytes));
            }
        };
        channel.basicConsume(QUEUE_NAME, autoAck, "a-consumer-tag", consumer);  // 创建新消费者,等待消息
    }
}

手工确认也可以批量完成,跟TCP的确认其实有一点类似,例如现在有5、6、7、8四个未确认的tag,这时客户端确认tag 8并设置multiple为true,RabbitMQ认为5、6、7、8都被确认;如果设置multiple为false,只有8被确认,5、6、7不被确认

java实现非常简单,basicAck第二个参数就是multiple,我们先写一个测试,只有delivery tag为4才发送确认信息,启动消费端后,发送4个消息

import com.rabbitmq.client.*;

import java.io.IOException;

public class RecvWithMulAck {
    private final static String QUEUE_NAME = "hello";

    public static void main(String[] args) throws Exception {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("服务器IP");  // 设置服务器
        factory.setUsername("admin");  // 账号
        factory.setPassword("password");  // 密码
        boolean autoAck = false;  // 不使用自动确认
        Connection connection = factory.newConnection();  // 创建连接和通道
        Channel channel = connection.createChannel();
        System.out.println(" [*] Waiting for messages. To exit press CTRL+C");
        Consumer consumer = new DefaultConsumer(channel) {
            @Override
            // 处理消息
            public void handleDelivery(String s, Envelope envelope, AMQP.BasicProperties basicProperties, byte[] bytes) throws IOException {
                long deliveryTag = envelope.getDeliveryTag();
                if (deliveryTag == 4) {  // 只单个确认tag 4
                    channel.basicAck(deliveryTag, false);
                }
                System.out.println("deliveryTag is " + deliveryTag);
                System.out.println("message is " + new String(bytes));
            }
        };
        channel.basicConsume(QUEUE_NAME, autoAck, "a-consumer-tag", consumer);  // 创建新消费者,等待消息
    }
}

消费端接收到了4个消息

但是去web页面上看队列的情况,仍有3个消息在等待接收

现在修改程序

channel.basicAck(deliveryTag, true);  // 开启批确认

同样接收完4个消息,去web页面上看,队列是空的,所有消息发送已经被确认

2.2 手工负确认

负确认有两种方法,basic.reject和basic.nack,basic.reject不包含multiple这个参数,所以为了支持批处理RabbitMQ扩展了协议,有了basic.nack。负确认方法还有另外一个重要的参数是入队requeue,如果这一项为true,表示消费者无力处理这条消息,将这条消息重新入队等待其他消费者处理

现在写一个负确认时丢弃消息的消费者,运行后,发送若干条消息,消息全部被接收,消息队列内没有残余消息

所以不带入队的负确认在实际效果上和正确认是相同的

import com.rabbitmq.client.*;

import java.io.IOException;

public class RecvWithNegative {
    private final static String QUEUE_NAME = "hello";

    public static void main(String[] args) throws Exception {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("服务器IP");  // 设置服务器
        factory.setUsername("admin");  // 账号
        factory.setPassword("password");  // 密码
        boolean autoAck = false;  // 不使用自动确认
        Connection connection = factory.newConnection();  // 创建连接和通道
        Channel channel = connection.createChannel();
        System.out.println(" [*] Waiting for messages. To exit press CTRL+C");
        Consumer consumer = new DefaultConsumer(channel) {
            @Override
            // 处理消息
            public void handleDelivery(String s, Envelope envelope, AMQP.BasicProperties basicProperties, byte[] bytes) throws IOException {
                long deliveryTag = envelope.getDeliveryTag();
                channel.basicReject(deliveryTag, false);  // 丢弃消息
                System.out.println("deliveryTag is " + deliveryTag);
                System.out.println("message is " + new String(bytes));
            }
        };
        channel.basicConsume(QUEUE_NAME, autoAck, "a-consumer-tag", consumer);  // 创建新消费者,等待消息
    }
}

现在看一下入队的负确认,只要修改一个地方,启动后,发送一条消息,会看到十分神奇的情况

channel.basicReject(deliveryTag, true);  // 消息重新入队

因为只有一个消费者,而这名消费者表示自己无力处理这条消息,消息不断重新入队,又不断被这个消费者读出,就造成了循环取到消息的情况,同时队列中有一条消息残余

最后负确认也可以批量完成,要使用basic.nack,其第二个参数就是multiple,第三个参数是requeue

实验效果和正确认是一样的,接收到4条消息,且队列无残余

import com.rabbitmq.client.*;

import java.io.IOException;

public class RecvWithMulNegativeAck {
    private final static String QUEUE_NAME = "hello";

    public static void main(String[] args) throws Exception {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("服务器IP");  // 设置服务器
        factory.setUsername("admin");  // 账号
        factory.setPassword("password");  // 密码
        boolean autoAck = false;  // 不使用自动确认
        Connection connection = factory.newConnection();  // 创建连接和通道
        Channel channel = connection.createChannel();
        System.out.println(" [*] Waiting for messages. To exit press CTRL+C");
        Consumer consumer = new DefaultConsumer(channel) {
            @Override
            // 处理消息
            public void handleDelivery(String s, Envelope envelope, AMQP.BasicProperties basicProperties, byte[] bytes) throws IOException {
                long deliveryTag = envelope.getDeliveryTag();
                if (deliveryTag == 4) {  // 接收到第四个消息后,批量确认
                    channel.basicNack(deliveryTag, true, false);
                }
                System.out.println("deliveryTag is " + deliveryTag);
                System.out.println("message is " + new String(bytes));
            }
        };
        channel.basicConsume(QUEUE_NAME, autoAck, "a-consumer-tag", consumer);  // 创建新消费者,等待消息
    }
}

3 生产可靠性

调用发送消息的接口完成,并不代表消息成功到达了服务器。协议中唯一保证发送成功的方法就是事务,但事务会将吞吐量降低250倍。RabbitMQ用confirm来解决这个问题。使用confirm.select来开启confirm模式,然后开始计数,计数从1开始,1对应的是confirm.select这条信息,服务器每接收到一条信息,就调用basic.ack发送确认,这个确认和消费端的确认很类似,也可以进行批确认等操作。如果服务器除了某些问题,会发送负确认basic.nack

对于routable message来说,消息进入队列后,服务器才会发送basic.ack,例如在镜像队列情况下,所有镜像队列都接收到这个消息才会发送确认,持久化的消息要写到磁盘才发送确认。绝大部分情况下服务器会按照顺序发出确认,但确认可能不按顺序到达

最简单的confirm代码如下,当然这样写效率很低,异步处理confirm信息将提高吞吐量

import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;

public class SendWithConfirm {
    private final static String QUEUE_NAME = "hello";

    public static void main(String[] args) throws Exception {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("服务器IP");  // 设置服务器
        factory.setUsername("admin");  // 账号
        factory.setPassword("password");  // 密码
        try (Connection connection = factory.newConnection();  // 创建连接和通道
        Channel channel = connection.createChannel()) {
            channel.queueDeclare(QUEUE_NAME, false, false, false, null);  // 声明队列
            channel.confirmSelect();  // 设置通道为confirm模式
            String message = "Hello World";
            channel.basicPublish("", QUEUE_NAME, null, message.getBytes());  // 创建新生产者,发送消息
            if (!channel.waitForConfirms()) {  // 等待confirm
                System.out.println(" [x] Sent failed");
            } else {
                System.out.println(" [x] Sent '" + message + "'");
            }
        }
    }
}

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值