首先,我们需要了解,RabbitMQ的作用是什么?
- 解耦:例如短信,邮件,订单系统等操作使用rabbitmq作为中间件更为合适,意思就是当用户下了订单时,会存放至mq,再由别的系统例如库存过来调用,这种架构的话,即使库存系统挂掉了,也不会影响我们订单系统的使用
- 异步:假如有一个用户注册功能,注册的时候要发送邮件和短信,此时我们就可以将注册信息写入mq,然后邮件和短信就可以并发去处理
- 削锋:用户的大量请求,例如秒杀或团抢活动,就可以使用rabbitmq作为存储,数据库再慢慢的去处理
缺点:系统的可用性降低,系统引入的外部依赖越多,系统能够越容易挂掉,就假如我们引入了MQ后,MQ一挂掉,我们的整个系统也就会无法使用了
扩展:rabbitMQ为什么是通过通道处理消息而不是链接呢?
主要原因是因为链接是基于TCP协议的,通过三次握手来实现的话是很慢的,而且一开一关开销很大,所以我们就是在TCP协议的基础上使用通道channel来实现长链接,这样我们来处理消息的效率才会更高,并且通道可以是多个
接下来,我们就来使用Java来连接一下我们的RabbitMQ
1.创建一个Maven项目
首先,我们需要先创建一个Maven项目
2.导入RabbitMQ依赖包
<dependency>
<groupId>com.rabbitmq</groupId>
<artifactId>amqp-client</artifactId>
<version>5.10.0</version>
</dependency>
3.通过JAVA实现消息发送
注:这里记得给先我们用户授权virtualhost,由于我使用的virtualhost为'/',用户为admin,所以使用以下命令进行授权
rabbitmqctl set_permissions -p / admin ".*" ".*" ".*"
创建RabbitMQ连接信息的工具类
public class MqConnectionUtils {
public static Connection getConnection() throws IOException, TimeoutException {
//定义一个链接工厂
ConnectionFactory connectionFactory = new ConnectionFactory();
//设置服务地址
connectionFactory .setHost("192.168.36.199");
//设定端口,注意,这里RabbitMQ的端口,不是管理页面的端口
connectionFactory .setPort(5672);
//设定用户名
connectionFactory .setUsername("admin");
//设定密码
connectionFactory .setPassword("123");
//设定虚拟访问节点
connectionFactory .setVirtualHost("/");
return connectionFactory.newConnection();
}
}
创建生产者
注:这里导入的是rabbitmq的包
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
/**
* 生产者
*/
public class Send {
//设定队列名称(已存在的队列)
private static final String QUEUE_NAME="queue1";
public static void main(String[] args) throws IOException, TimeoutException {
//从MQ工具类获取连接信息
Connection connection = MqConnectionUtils.getConnection();
//创建一个通道
Channel channel = connection.createChannel();
//准备发送的消息内容
String msg = "world";
//发送消息给队列
/**
* 参数1:交换机,不定义也会有默认的,因为我们的消息是通过交换机来进行投递给队列的,所以交换机不可能没有
* 参数2:简单模式:队列名称
* 参数3:消息的状态控制
* 参数4:消息内容
*/
channel.basicPublish("",QUEUE_NAME,null,msg.getBytes());
System.out.print("发送成功");
//关闭通道
channel.close();
connection.close();
}
}
此时我们的queue1队列中就可查看到该消息
4.队列的创建
创建一个queue4队列并向其发送消息
/**
* 生产者
*/
public class Send {
//设定队列名称
private static final String QUEUE_NAME="queue4";
public static void main(String[] args) throws IOException, TimeoutException {
//从MQ工具类获取连接信息
Connection connection = MqConnectionUtils.getConnection();
//创建一个通道
Channel channel = connection.createChannel();
/**
* 创建队列
* 参数1:队列的名称
* 参数2:是否要持久化,持久化和非持久化都会存盘,但是非持久化重启服务器会丢失,所以只是单纯的重启服务,队列是不会消失的
* 参数3:排他性,是否是独占独立
* 参数4:是否自动删除,随着最后一个消费者消费完毕后是否删除队列
* 参数5:携带一些附加参数
*/
channel.queueDeclare(QUEUE_NAME,true,false,false,null);
//发送消息给队列
/**
* 参数1:交换机,不定义也会有默认的,因为我们的消息是通过交换机来进行投递给队列的,所以交换机不可能没有
* 参数2:简单模式:队列名称
* 参数3:消息的状态控制
* 参数4:消息内容
*/
for(int i = 1;i <= 5; i++){
//准备发送的消息内容
String msg = "Hello" + i;
//发送消息给队列queue
channel.basicPublish("",QUEUE_NAME,null,msg.getBytes());
}
System.out.print("发送成功");
//关闭通道
channel.close();
connection.close();
}
}
此时再登录我们的管理页面即可看到我们新创建的队列
5.交换机的创建与队列绑定
交换机的创建
//true代表持久性,交换机不会随着服务器重启造成丢失
channel.exchangeDeclare("交换机名称","交换机类型",true);
交换机与队列的绑定
//routekey根据交换机模式判断是否填写
channel.queueBind(“队列名称”,”交换机名称”,”routekey”)
6.实现fanout发布于订阅模式
如果不知道fanout是什么
/**
* 生产者
*/
public class Send {public static void main(String[] args) throws IOException, TimeoutException {
//从MQ工具类获取连接信息
Connection connection = MqConnectionUtils.getConnection();
//创建一个通道
Channel channel = connection.createChannel();
//准备发送的消息内容
String msg = "Hello World";
//准备交换机(已创建的交换机)
String exchangeName = "fanout-exchange";
//准备路由
String routekey = "";
//发送消息给交换机
/**
* 参数1:交换机,不定义也会有默认的,因为我们的消息是通过交换机来进行投递给队列的,所以交换机不可能没有
* 参数2:routekey
* 参数3:消息的状态控制
* 参数4:消息内容
*/
//该模式因为是由交换机发给该交换机绑定的所有队列,所以可以不标明队列名称
channel.basicPublish(exchangeName,routekey,null,msg.getBytes());
System.out.print("发送成功");
//关闭通道
channel.close();
connection.close();
}
}
7.实现direct发布于订阅模式
/**
* 生产者
*/
public class Send {public static void main(String[] args) throws IOException, TimeoutException {
//从MQ工具类获取连接信息
Connection connection = MqConnectionUtils.getConnection();
//创建一个通道
Channel channel = connection.createChannel();
//准备发送的消息内容
String msg = "Hello World";
//准备交换机(已创建的交换机)
String exchangeName = "direct-exchange";
//准备路由
String routekey = "email";
//发送消息给交换机
/**
* 参数1:交换机,不定义也会有默认的,因为我们的消息是通过交换机来进行投递给队列的,所以交换机不可能没有
* 参数2:routekey
* 参数3:消息的状态控制
* 参数4:消息内容
*/
//该模式因为是由交换机发给该交换机绑定的所有队列,所以可以不标明队列名称
channel.basicPublish(exchangeName,routekey,null,msg.getBytes());
System.out.print("发送成功");
//关闭通道
channel.close();
connection.close();
}
}
8.实现topic发布于订阅模式
其实topic模式和direct模式几乎一致,只需要修改交换器名称和routekey即可
/**
* 生产者
*/
public class Send {
public static void main(String[] args) throws IOException, TimeoutException {
//从MQ工具类获取连接信息
Connection connection = MqConnectionUtils.getConnection();
//创建一个通道
Channel channel = connection.createChannel();
//准备发送的消息内容
String msg = "Hello World";
//准备交换机(已创建的交换机)
String exchangeName = "topic-exchange";
//准备路由
String routekey = "kuang.chen.com";
//发送消息给交换机
/**
* 参数1:交换机,不定义也会有默认的,因为我们的消息是通过交换机来进行投递给队列的,所以交换机不可能没有
* 参数2:routekey
* 参数3:消息的状态控制
* 参数4:消息内容
*/
//该模式因为是由交换机发给该交换机绑定的所有队列,所以可以不标明队列名称
channel.basicPublish(exchangeName,routekey,null,msg.getBytes());
System.out.print("发送成功");
//关闭通道
channel.close();
connection.close();
}
}
9.消费者的实现
需要先了解,在消费者中,RabbitMQ为其提供了消息确认机制,分为消息自动确认模式和消息手动确认模式,当消息确认后,我们队列中的消息将会移除
那这两种模式要如何选择呢?
- 如果消息不太重要,丢失也没有影响,那么自动ACK会比较方便。好处就是可以提高吞吐量,缺点就是会丢失消息
- 如果消息非常重要,不容丢失,则建议手动ACK,正常情况都是更建议使用手动ACK。虽然可以解决消息不会丢失的问题,但是可能会造成消费者过载
消息自动确认模式的实现
注:自动确认模式,消费者不会判断消费者是否成功接收到消息,也就是当我们程序代码有问题,我们的消息都会被自动确认,消息被自动确认了,我们队列就会移除该消息,这就会造成我们的消息丢失
/**
* 消费者
*/
public class Recv {
//设定队列名称(已存在的队列)
private static final String QUEUE_NAME = "queue1";
public static void main(String[] args) throws IOException, TimeoutException {
//从mq工具类获取连接信息
Connection connection = MqConnectionUtils.getConnection();
//获取一个通道
Channel channel = connection.createChannel();
//监听该队列,true代表自动确认
channel.basicConsume(QUEUE_NAME,true,new DefaultConsumer(channel){
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties basicProperties, byte[] body) throws IOException{
System.out.println("接收到的消息:"+ new String(body,"UTF-8"));
}
});
}
}
实现效果,消费者会将我们队列中的消息全部接收然后确认,并移除队列
此时我们再来看一下Connections和Channels
Connections
Channels
可以看到在我们启动消费者监听后,通道是一直存在的
消息手动确认模式的实现
/**
* 消费者
*/
public class Recv {
//设定队列名称(已存在的队列)
private static final String QUEUE_NAME = "queue1";
public static void main(String[] args) throws IOException, TimeoutException {
//从mq工具类获取连接信息
Connection connection = MqConnectionUtils.getConnection();
//获取一个通道
Channel channel = connection.createChannel();
//监听该队列,false代表手动确认
channel.basicConsume(QUEUE_NAME,false,new DefaultConsumer(channel){
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties basicProperties, byte[] body) throws IOException{
System.out.println("接收到的消息:"+ new String(body,"UTF-8"));
}
});
}
}
手动确认模式下,当我们消费者成功接收到消息后,在队列中消息会进入Unacked项,也就是待确认模式
所以我们还需要加上下列代码,来实现消息者在成功接收到消息后,手动确认
#添加红色字段
/**
* 消费者
*/
public class Recv {
//设定队列名称(已存在的队列)
private static final String QUEUE_NAME = "queue1";
public static void main(String[] args) throws IOException, TimeoutException {
//从mq工具类获取连接信息
Connection connection = MqConnectionUtils.getConnection();
//获取一个通道
Channel channel = connection.createChannel();
//监听该队列
channel.basicConsume(QUEUE_NAME,false,new DefaultConsumer(channel){
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties basicProperties, byte[] body) throws IOException{
System.out.println("接收到的消息:"+ new String(body,"UTF-8"));
//获取消息的编号,我们需要根据消息的编号来确认消息
long tag = envelope.getDeliveryTag();
//获取当前内部类中的通道
Channel c = this.getChannel();
//手动确认消息,确认以后,则表示消息已经成功处理,消息就会从队列中移除
c.basicAck(tag,true); }
});
}
}
此时,我们的消息才会成功被确认,并移除队列。
这里我们对消息确认机制中的消费者接收消息确认进行了初步的介绍