RabbitMQ学习笔记(一)概念和基本操作
基本概念
生产者Publisher
投递消息的一方,生产者将消息投递到交换机。
消费者Consumer
接收消息的一方,消费者可以监听一个或多个队列,接收到消息后需要返回应答;多个消费者监听同一队列时,RabbitMQ并不会以广播的形式让每个消费者都接收到全部消息,而是多个消费者以轮询的方式接收并处理消息。
消息
包含两部分,分别是标签(Label,用来描述这则消息如交换机名称、路由键、优先权等)以及消息体(payload,消息的具体内容)。
队列Queue
存放消息的“容器”,每个队列都至少会绑定一个交换机,从交换机接收消息。
交换机Exchange
生产者将消息投递到交换机后,交换机根据路由键、绑定键和交换机类型将消息路由到具体的队列中。
一共有4中类型:
fanout:向所有绑定的队列发送消息。
direct:对路由键和绑定键进行精准匹配,完全一致的队列会收到消息。
topic:对路由键和绑定键进行模糊匹配,符合匹配规则的队列会收到消息,路由键和绑定键都是由“.”分隔的一个字符串,每一个被“.”分隔开的独立的子字符串称作“单词”,绑定键中有两种通配符:①*,星号表示匹配一个单词;②#,井号表示匹配多个单词(可以是0个)。
header:根据绑定队列与交换机时的额外参数进行路由匹配,性能很差,基本不用。
路由键&绑定键
路由键是生产者发送消息给交换机的时候指定的,绑定键是队列与交换机绑定的时候指定的,根据交换机的类型,将会对路由键和绑定键进行精准匹配或模糊匹配,以此确定消息应该发往哪个队列。在direct模式的交换机下,可以将路由键和绑定键看作是一个东西,因为这时执行的是精准匹配。
连接Connection&信道Channel
连接就是一个普通的TCP连接,信道是建立在连接上的虚拟连接,无论消费者还是生产者都是通过信道发送指令,信道复用了TCP连接,节省资源。
虚拟主机Virtual Host
可以看做一个文件夹,里面装了一组交换机、队列和绑定关系等相关对象,每个Broker可以有多个Virtual Host,在建立连接时必须指定一个Virtual Host,默认是“/”。
Broker
消息队列服务器实体。
Rabbitmq的Java客户端
首先保证你的Rabbitmq服务是开启的,并且可以远程访问。接下来去新建一个普通的Maven项目即可,添加Rabbitmq客户端依赖,这个依赖又需要依赖SLF4j日志的依赖,否则在运行时会有警告。
<!--rabbitmq java客户端-->
<dependency>
<groupId>com.rabbitmq</groupId>
<artifactId>amqp-client</artifactId>
<version>5.11.0</version>
</dependency>
<!--日志依赖-->
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.2.3</version>
</dependency>
一些固化操作
创建一个连接工厂,类似于线程池中的那个线程工厂,需要在这里对连接进行一些统一的配置
//创建连接工厂,完成连接配置。
ConnectionFactory connectionFactory = new ConnectionFactory();
//设置ip地址
connectionFactory.setHost("192.168.43.129");
//设置端口
connectionFactory.setPort(5672);
//设置账号和密码
connectionFactory.setUsername("jdq");
connectionFactory.setPassword("1234");
//broker中的虚拟地址,用于进行逻辑隔离
connectionFactory.setVirtualHost("/");
通过连接工厂获得一个连接,再通过连接创建一个信道,这两个方法都会抛出异常,通常将变量定义在try语句块外,方法写在try语句块内
//创建连接connection
Connection connection = connectionFactory.newConnection();
//通过连接获取信道channel
Channel channel = connection.createChannel();
通过信道创建交换机、队列及绑定关系,这部分工作也可以通过web管理界面完成,如果在项目上线之前就已经完成了这些工作,那么可以省去这部分代码,直接使用
/**
* 声明交换机
* @param1:交换机名称
* @param2:交换机类型
* @param3:是否持久化,持久化交换机会存盘,服务重启后不会丢失
*
* 有很多重载方法
*/
channel.exchangeDeclare(exchangeName, "direct", true);
/**
* 声明队列
* @params1:队列名称,重名且完全一致时放弃创建但也不报错
* @params2:是否做队列持久化。持久化队列的消息在服务器重启后依然存在,非持久化队列的消息会随着服务器重启丢失
* @params3:是否排他,若是排他的,那么当前队列不允许其他连接的消费者监听,并在连接断开时自动删除,也不允许其他连接声明同名队列
* @params4:是否自动删除,至少有一个消费者监听的队列在最后一个消费者断开连接后自动删除队列
* @params5:声明队列时携带的额外参数,设置一些属性
*
* 无参的重载方法channel.queueDeclare()会声明一个由Rabbitmq命名的、排他的、非持久化的自动删除的队列
*/
channel.queueDeclare(queueName, true, false, false, null);
/**
* 绑定队列
* @param1:队列名
* @param2:交换机名
* @param3:绑定键(这里用路由键来代替是因为direct模式的交换机不用区别路由键和绑定键)
*/
channel.queueBind(queueName, exchangeName, routingKey);
发送消息
/**
* 发送消息
* @param1:交换机名称
* @param2:路由key
* @param3:设置消息的属性
* @param4:消息内容,是一个字节数组
*/
channel.basicPublish(exchangeName, routingKey, null, message.getBytes(StandardCharsets.UTF_8));
消费消息
消费者可以选择是否自动确认消息,如果设置了自动确认,那么消费者接收到消息后消息会直接出队,不管是否成功的处理了消息;如果不设置自动确认,那么消费者处理完消息后需要根据处理情况主动发送确认或拒绝,队列会保存未收到确认或拒绝的消息。
/**
* 接收消息,自动确认
* @param1:订阅的队列
* @param2:是否自动确认消息
* @param3:处理消息的回调函数
* */
channel.basicConsume("queue1", true, new DefaultConsumer(channel){
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
String message = new String(body);
//处理消息
System.out.println(message);
}
});
/**
* 接收消息,不自动确认
* @param1:订阅的队列
* @param2:是否自动确认消息
* @param3:处理消息的回调函数
* */
channel.basicConsume("queue1", false, new DefaultConsumer(channel){
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
String message = new String(body);
//得到通道,也可以将channel在上面定义成常量,直接使用
Channel c = this.getChannel();
//处理消息
try {
System.out.println(message);
/**
* 确认消息
* @param1:消息编号
* @param2:用于批量确认,设置为true表示确认编号小于等于参数1给定的编号之前的未确认的消息;false即单条消息确认
*/
c.basicAck(envelope.getDeliveryTag(), true);
} catch (IOException e) {
e.printStackTrace();
/**
* 拒绝消息
* @param1:消息编号
* @param2:是否将消息重新入队,设为false的话会直接丢弃消息
*/
c.basicReject(envelope.getDeliveryTag(), false);
/**
* 也可以批量拒绝消息,如下:
* @param1:消息编号
* @param2:是否批量拒绝,设为true表示拒绝参数1编号之前的所有未被确认的消息,设为false的话则与单个拒绝无异
* @param3:是否将消息重新入队
*
* c.basicNack(envelope.getDeliveryTag(),true,false);
*/
}
}
});
关闭连接,可以显示的关闭通道,但这并不是必须的,因为在连接关闭后,通道会自动关闭
finally {
//关闭通道
if (channel != null && channel.isOpen()) {
try {
channel.close();
} catch (IOException | TimeoutException e) {
e.printStackTrace();
}
}
//关闭连接
if (connection != null && connection.isOpen()) {
try {
connection.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
简单模式
生产者:
public static void main(String[] args) {
//创建连接工厂,完成连接配置。
ConnectionFactory connectionFactory = new ConnectionFactory();
//设置ip地址
connectionFactory.setHost("192.168.43.129");
//设置端口
connectionFactory.setPort(5672);
//设置账号和密码
connectionFactory.setUsername("jdq");
connectionFactory.setPassword("1234");
//broker中的虚拟地址,用于进行逻辑隔离
connectionFactory.setVirtualHost("/");
Connection connection = null;
Channel channel = null;
try {
//创建连接connection
connection = connectionFactory.newConnection();
//通过连接获取通道channel
channel = connection.createChannel();
//声明队列
String queueName = "queue1";
channel.queueDeclare(queueName, true, false, false, null);
//准备消息
String message = "Hello World";
//简单模式不需要指定交换机和路由键,用队列名代替路由键,虽然没有指定交换机,但实际上这个队列绑定在默认交换机上
channel.basicPublish("", queueName, null, message.getBytes(StandardCharsets.UTF_8));
System.out.println("hello world!!!");
} catch (IOException | TimeoutException e) {
e.printStackTrace();
} finally {
//关闭通道
if (channel != null && channel.isOpen()) {
try {
channel.close();
} catch (IOException | TimeoutException e) {
e.printStackTrace();
}
}
//关闭连接
if (connection != null && connection.isOpen()) {
try {
connection.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
消费者:
public static void main(String[] args) {
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setHost("192.168.43.129");
connectionFactory.setPort(5672);
connectionFactory.setUsername("jdq");
connectionFactory.setPassword("1234");
connectionFactory.setVirtualHost("/");
Connection connection = null;
Channel channel = null;
try {
//创建连接connection
connection = connectionFactory.newConnection();
//通过连接获取通道channel
channel = connection.createChannel();
//可以在消费者中也声明队列,但这不是必须的,只要保证在监听前队列已经存在即可
String queueName = "queue1";
channel.queueDeclare(queueName, true, false, false, null);
//接收消息,自动确认
channel.basicConsume("queue1", true, new DefaultConsumer(channel){
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
String message = new String(body);
//处理消息
System.out.println(message);
}
});
//持续监听,不要让连接关闭
System.in.read();
} catch (IOException | TimeoutException e) {
e.printStackTrace();
} finally {
//关闭通道
if (channel != null && channel.isOpen()) {
try {
channel.close();
} catch (IOException | TimeoutException e) {
e.printStackTrace();
}
}
//关闭连接
if (connection != null && connection.isOpen()) {
try {
connection.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
direct模式
生产者:
public static void main(String[] args) {
//创建连接工厂,设置连接属性
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setHost("192.168.43.129");
connectionFactory.setPort(5672);
connectionFactory.setUsername("jdq");
connectionFactory.setPassword("1234");
connectionFactory.setVirtualHost("/");
Connection connection = null;
Channel channel = null;
try {
//创建连接connection
connection = connectionFactory.newConnection();
//通过连接获取通道channel
channel = connection.createChannel();
//创建交换机
String exchangeName = "direct-exchange";
channel.exchangeDeclare(exchangeName, "direct", true);
//声明队列
String queueName = "directQueue";
channel.queueDeclare(queueName, true, false, false, null);
//准备消息和路由key
String message = "Hello Direct";
String routeKey1 = "info";
String routeKey2 = "error";
//绑定关系
channel.queueBind(queueName, exchangeName, routeKey1);
channel.queueBind(queueName, exchangeName, routeKey2);
//发送消息
channel.basicPublish(exchangeName, routeKey, null, message.getBytes(StandardCharsets.UTF_8));
System.out.println("发送完毕!!!");
} catch (IOException | TimeoutException e) {
e.printStackTrace();
} finally {
//关闭通道
if (channel != null && channel.isOpen()) {
try {
channel.close();
} catch (IOException | TimeoutException e) {
e.printStackTrace();
}
}
//关闭连接
if (connection != null && connection.isOpen()) {
try {
connection.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
消费者:
public static void main(String[] args) {
//创建连接工厂
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setHost("192.168.43.129");
connectionFactory.setPort(5672);
connectionFactory.setUsername("jdq");
connectionFactory.setPassword("1234");
connectionFactory.setVirtualHost("/");
Connection connection = null;
Channel channel = null;
try {
//创建连接connection
connection = connectionFactory.newConnection();
//通过连接获取通道channel
channel = connection.createChannel();
//创建交换机,非必须但要保证交换机存在
String exchangeName = "direct-exchange";
channel.exchangeDeclare(exchangeName, "direct", true);
//声明队列,非必须但要保证队列存在
String queueName = "directQueue";
channel.queueDeclare(queueName, true, false, false, null);
//定义路由key
String routeKey1 = "info";
String routeKey2 = "error";
//绑定队列,非必须但要保证队列已经绑定
channel.queueBind(queueName, exchangeName, routeKey1);
channel.queueBind(queueName, exchangeName, routeKey2);
//接收消息,自动确认
channel.basicConsume(queueName, true, new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
String message = new String(body);
System.out.println(message);
}
});
//持续监听
System.in.read();
} catch (IOException | TimeoutException e) {
e.printStackTrace();
} finally {
//关闭通道
if (channel != null && channel.isOpen()) {
try {
channel.close();
} catch (IOException | TimeoutException e) {
e.printStackTrace();
}
}
//关闭连接
if (connection != null && connection.isOpen()) {
try {
connection.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
fanout广播模式
fanout广播模式有点特殊,fanout交换机不会检查绑定键和路由键,而是将消息发送给所有与该交换机绑定的队列;同时要想形成广播,需要消费者监听不同的队列,所以声明队列、绑定队列的工作应该是在消费者端完成的,并且队列的名称不能写死,而是随机生成的,先来看消费者。
消费者:
static class MyRunnable implements Runnable{
@Override
public void run() {
//创建连接工厂
ConnectionFactory connectionFactory = new ConnectionFactory();
//设置ip地址
connectionFactory.setHost("192.168.43.129");
//设置端口
connectionFactory.setPort(5672);
//设置账号和密码
connectionFactory.setUsername("jdq");
connectionFactory.setPassword("1234");
//broker中的虚拟地址,用于进行逻辑隔离
connectionFactory.setVirtualHost("/");
Connection connection = null;
Channel channel = null;
try {
//创建连接connection
connection = connectionFactory.newConnection();
//通过连接获取通道channel
channel = connection.createChannel();
//创建fanout类型交换机
String exchangeName = "fanout-exchange";
channel.exchangeDeclare(exchangeName, "fanout", true);
//声明队列,使用使用无参的重载方法,队列名由Rabbitmq随机生成,getQueue获取队列名
String queueName = channel.queueDeclare().getQueue();
//绑定队列,不需要指定绑定键
channel.queueBind(queueName, exchangeName, "");
channel.queueBind(queueName, exchangeName, "");
//接收消息,自动确认
channel.basicConsume(queueName, true, new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
String message = new String(body);
System.out.println(Thread.currentThread().getName()+"\t"+message);
}
});
//持续监听
System.in.read();
} catch (IOException | TimeoutException e) {
e.printStackTrace();
} finally {
//关闭通道
if (channel != null && channel.isOpen()) {
try {
channel.close();
} catch (IOException | TimeoutException e) {
e.printStackTrace();
}
}
//关闭连接
if (connection != null && connection.isOpen()) {
try {
connection.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
public static void main(String[] args) {
new Thread(new MyRunnable(),"t1").start();
new Thread(new MyRunnable(),"t2").start();
new Thread(new MyRunnable(),"t3").start();
}
生产者:
public static void main(String[] args) {
//创建连接工厂,完成连接配置。
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setHost("192.168.43.129");
connectionFactory.setPort(5672);
connectionFactory.setUsername("jdq");
connectionFactory.setPassword("1234");
connectionFactory.setVirtualHost("/");
Connection connection = null;
Channel channel = null;
try {
//创建连接connection
connection = connectionFactory.newConnection();
//通过连接获取通道channel
channel = connection.createChannel();
//声明交换机
String exchangeName = "fanout-exchange";
channel.exchangeDeclare(exchangeName, "fanout", true);
//准备消息
String message = "Hello World";
//发送消息
channel.basicPublish(exchangeName, "", null, message.getBytes(StandardCharsets.UTF_8));
System.out.println("发送成功!!");
} catch (IOException | TimeoutException e) {
e.printStackTrace();
} finally {
//关闭通道
if (channel != null && channel.isOpen()) {
try {
channel.close();
} catch (IOException | TimeoutException e) {
e.printStackTrace();
}
}
//关闭连接
if (connection != null && connection.isOpen()) {
try {
connection.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
topic模式
和direct模式差不多,只不过执行的是模糊匹配。将direct模式中的交换机改为topic类型交换机,在绑定队列时可以在绑定键中使用通配符“*”和“#”。
springboot整合Rabbitmq
首先加入Rabbitmq的起步依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
再到application配置文件中做一些配置
spring:
rabbitmq:
host: 192.168.43.129
port: 5672
username: jdq
password: 1234
direct
创建一个配置类,声明队列、交换机,以及绑定关系,以direct交换机为例:
@Configuration
public class RabbitmqConf {
@Bean
public Queue directQueue() { //注意:这个Queue是spring包的
//创建一个名字叫"direct-queue",持久化、非排他、非自动删除的队列
return new Queue("direct-queue");
}
@Bean
public DirectExchange directExchange() {
//创建一个名字叫"directExchange",持久化、非自动删除的direct类型的交换机
return new DirectExchange("directExchange");
}
@Bean
public Binding directBinding() {
//绑定队列与交换机,设置绑定键
return BindingBuilder.bind(directQueue()).to(directExchange()).with("directKey");
}
}
生产者发送消息
@Service
public class Publisher {
//注入模板工具类,也可以注入AmqpTemplate,AmqpTemplate是RabbitTemplate的上层接口,实际注入的是同一个对象
@Autowired
private RabbitTemplate rabbitTemplate;
public void send(String message) {
/**
* 发送消息,convertAndSend方法可以自动将消息序列化,相当于简化版的send方法
* @param1:交换机名称
* @param2:路由键
* @param3:消息
*/
rabbitTemplate.convertAndSend("directExchange", "directKey", message);
System.out.println("发送成功!");
}
}
消费者相对复杂一点,rabbitTemplate的receive()
和receiveAndConvert()
方法都不能持续的监听队列,需要使用@RabbitListener注解来持续监听队列,异步接收消息。
@Service
public class Consumer {
/**
* RabbitListener注解作用在方法上,表示指定该方法是一个消息消费方法,
* 可以直接指定要监听的队列名(queues属性是一个数组),但是这要保证队列、交换机已存在且正确绑定
*
* @param message 接受到的消息
*/
@RabbitListener(queues = {"direct-queue"})
public void listener(String message) {
System.out.println(message);
}
}
//或者像这样
@Service
@RabbitListener(queues = {"direct-queue"})
public class Consumer02 {
/**
* RabbitListener注解作用在类上,表示这个类用于监听队列,接受消息
* RabbitHandler注解作用在方法上,用来实现具体的消息消费
*
* @param message 消息
*/
@RabbitHandler
public void handler(String message) {
System.out.println(message);
}
}
注意:@RabbitListener接收消息,不是自动应答的,而是底层帮我们做了主动确认,如果在处理消息的过程中出现了异常,那么它采取的策略是将消息重新入队,相当于channel.basicRecover()
将发送消息的方法写在单元测试中,启动springboot项目,发送消息,可以在控制台看到测试结果。
@RunWith(SpringRunner.class)
@SpringBootTest
class DemoApplicationTests {
@Autowired
private Publisher publisher;
@Test
public void test(){
publisher.send("hello!");
}
}
fanout
topic模式和direct操作流程上没有什么本质差异,主要来看一下fanout。和上面一样要想使用fanout交换机做广播,那队列名首先不能写死,而且队列和交换机的绑定要在消费者端完成,如下:
/**
* @RabbitListener注解作用在方法上,表示指定该方法是一个消息消费方法。
* 属性:
* bindings: 用于绑定队列与交换机,是一个@QueueBinding注解数组
* @QueueBinding注解
* 属性:
* value: 是一个@Queue注解,用于声明并使用队列
* exchange: 是一个@Exchange注解,用于声明并使用交换机
* key: 是一个String数组,指定绑定键
* @param message 消息
*/
@RabbitListener(bindings = {@QueueBinding(value = @Queue,exchange = @Exchange(name = "fanout-exchange",type = "fanout"),key = "")})
public void fanoutListener01(String message) {
System.out.println("01===="+message);
}
@RabbitListener(bindings = {@QueueBinding(value = @Queue,exchange = @Exchange(name = "fanout-exchange",type = "fanout"),key = "")})
public void fanoutListener02(String message) {
System.out.println("02===="+message);
}
其余部分与direct类型无异
//生产者发送消息
@Service
public class Publisher {
@Autowired
private RabbitTemplate rabbitTemplate;
public void fanoutSend(String message) {
rabbitTemplate.convertAndSend("fanout-exchange", "", message);
System.out.println("发送成功!!");
}
}
//配置类中声明一个交换机
@Configuration
public class RabbitmqConf {
@Bean
public FanoutExchange fanoutExchange(){
return new FanoutExchange("fanout-exchange");
}
}