上一篇文章中,我们使用Docker将rabbitmq安装好了,可以参考 docker安装rabbitmq 。下面,基于此rabbitmq服务,做一些实际的操作,包括原生的使用以及spring boot对于rabbitmq的支持。
本文很多概念,截取于rabbitMQ实战指南。
概念
什么是消息中间件?
消息(Message)是指在应用间传送的数据。
消息中间件(Message Queue Middleware,简称MQ)是指利用高效可靠的消息传递机制进行与平台无关的数据交流,并基于数据通信来进行分布式系统的集成。通过提供消息传递和消息排队模型,它可以在分布式环境下扩展进程间的通信。
RabbitMQ是常用的消息中间件,其他的消息中间件还有Kafka、activeMQ、rocketMQ等。
RabbitMQ的整体架构模型如下:
其中包含的组件含义如下:
Producer:生产者,就是投递消息的一方。 消息一般包含两部分,消息体和标签。消息体是要发送的数据,标签是对数据的表述。
Consumer:消费者,就是接收消息的一方。
Broker:消息中间件的服务节点。 RabbitMQ Broker可以看作是一个RabbitMQ服务实例。
Queue:队列,是RabbitMQ的内部对象,用于存储消息。RabbitMQ中消息都只能存储在队列中。
Exchange:交换器。生产者将消息发送到Exchange,由Exchange将消息路由到一个或多个队列中。
RoutingKey:路由键。生产者将消息发送给交换器的时候,一般会指定一个RoutingKey,用来指定这个消息的路由规则。当RoutingKey与BindingKey匹配时,消息会被路由到相应的队列。RoutingKey需与交换器类型和绑定建(BindingKey)联合使用才能生效。
Binding:绑定。RabbitMQ中通过Binding将Exchange和Queue关联起来,在绑定的时候,一般会指定一个绑定键(BindingKey)。
由上面可知,rabbitmq中生产者发送的消息,其实不是直接放到队列中的,而是先放到Exchange中,也就是所谓的交换器。然后再由Exchange去绑定队列,进行下一步的操作。我们花点时间了解一下Exchange。
交换器的类型
常用的交换器类型有 fanout、direct、topic。
fanout:它会把所有发送到该交换器的消息路由到所有与该交换器绑定的队列中,如同“广播”播报一样。“无路由交换机”,说白了就是,使用这个交换机不需要routingkey绑定,和路由没有关系,它是直接绑定到队列的。
direct:它会把消息路由到那些BindingKey 和 Routingkey 完全匹配的队列中。direct 类型的exchange比较常用,模型架构图如下:
topic:topic与direct类型的交换器相似,也是将消息路由到BindingKey和RoutingKey相匹配的队列中,但它不是完全匹配,在匹配规则上进行了拓展。topic类型的exchange也比较常用,模型架构图如下:
RabbitMQ中的交换器、交换器类型、队列、绑定、路由键等都是遵循的AMQP协议中相应的概念。AMQP协议是一个网络通信协议,RabbitMQ就是AMQP协议的Erlang实现。
谈到AMQP,还有两个很重要的概念:connection和channel。
connection:无论是生产者还是消费者,都需要与RabbitMQ Broker建立连接,这个连接就是一条TCP连接,也就是Connection。
channel:channel(信道)是建立在Connection之上的虚拟连接,RabbitMQ处理的每条AMQP指令都是通过信道完成的。
基本的概念以上已经讲述完毕,现在开始进行一些实际的代码操作。
1.pom.xml
com.rabbitmq
amqp-client
3.4.1
2.connectionUtils
public class ConnectionUtil {
public static Connection getConnection() throws Exception {
//定义连接工厂
ConnectionFactory factory = new ConnectionFactory();
//设置服务地址
factory.setHost("192.168.20.131");
//amqp 协议端口,注意端口
factory.setPort(5672);
//设置账号信息,用户名、密码、virtualHost
//virtualHost必须是该用户下存在的,不然报错
factory.setVirtualHost("20200117");
factory.setUsername("guest");
factory.setPassword("guest");
// 通过工程获取连接
Connection connection = factory.newConnection();
return connection;
}
}
virtualHost是一个逻辑上的单位,为了分区使用的。我们可以通过rabbitmq的web控制面板去创建需要的virtualHost。如下:
3.生产者
生产者也就是消息的发送者,他要创建与RabbitMQ的连接,通过连接创建channel,然后在channel上声明转发消息所在的exchange。代码示例如下:
@Slf4j
public class Producer {
public static void main(String[] args) throws Exception {
// 获取到connection、channel
Connection connection = ConnectionUtil.getConnection();
Channel channel = connection.createChannel();
// 声明exchange
String exchangeName = "test_exchange_fanout";
String exchangeType = "fanout";
channel.exchangeDeclare(exchangeName, exchangeType);
// 发送消息到exchange
String message = "2020 01 17";
channel.basicPublish(exchangeName, "", null, message.getBytes());
log.info(" [x] Sent '" + message + "'");
// 关闭连接
channel.close();
connection.close();
}
}
执行上述代码:
执行完后,查看RabbitMQ的web控制台,可以找到刚刚创建的 exchange。
点击“test_exchange_fanout”,查看exchange详情。发现现在还没有队列与之绑定,这也正常,我消费者都还没创建来着。
4.消费者
消费者,就是消费消息的那一端,本质上其实就是消费队列的对象。消费者一直监听着消费的队列,可以通过消费者获取队列中的数据。
我们去写消费者,就是要去指定接收exchange中的数据,因为生产者将数据发送到exchange中。所以要声明一个队列去匹配(说绑定更合适)exchange,让exchange将消息转发到我们声明的队列中。然后,我们再去创建一个消费者去监听(消费)先前创建的队列,去获取生产者发过来的数据。
代码示例如下:
@Slf4j
public class Consumer {
public static void main(String[] args) throws Exception {
// 获取到connection、channel
Connection connection = ConnectionUtil.getConnection();
Channel channel = connection.createChannel();
// 声明队列
String queueName = "test_fanout_queue";
channel.queueDeclare(queueName, false, false, false, null);
// queue与exchange 绑定(匹配)
String exchangeName = "test_exchange_fanout";
channel.queueBind(queueName, exchangeName, "");
// 定义队列的消费者
QueueingConsumer consumer = new QueueingConsumer(channel);
channel.basicConsume(queueName, true, consumer);
// 获取消息
while (true) {
QueueingConsumer.Delivery delivery = consumer.nextDelivery();
String message = new String(delivery.getBody());
System.out.println(" [Recv] Received '" + message + "'");
}
}
}
debug启动:
当我们运行到绑定了exchange和queue后,我们看到RabbitMQ的队列中,出现了我们创建的队列。
上面显示队列中已经有一个消息Ready了,等待消费 。点击查看详情:看到已经和 test_exchange_fanout 绑定好了。
我们继续运行完代码:控制台打印之前在生产者中发送的消息, 消息成功消费。再去看 控制台,消息已经没有了,队列显示为idle 空闲状态, 说明消费成功。
以上我们已经成功的使用 fanout 的 exchange 完成了一个消息队列。下面我们使用 一个用的更多的 direct 类型 exchange做一个小案例。
相对比只要交换机名称即可接收到消息的fanout ,direct模式在其基础上,多加了一层路由(routingKey),也可以称之为 二次校验密码。它除了匹配 exchange的名称,还要匹配 routingKey。其实这也容易理解,fanout 这种模式太广泛,要更精确的指定哪些队列可以接收消息,哪些不可以,这样更符合生产的要求。说到这里,提下topic 类型,这个和direct 又非常像,topic可以说是direct的扩展,他的匹配规则更加灵活,他使用匹配表达式去匹配队列,而direct是绝对的完全匹配。由于篇幅关系,本文末尾使用spring boot说明topic。
1.生产者
代码基于上面的,改动的就是 下面标记的地方,大家可以自己动手试试。
2.消费者
消费者在 队列与exchange的绑定过程中,设置了 routingKey,这样 就与 direct 类型的exchange匹配上了。
@Slf4j
public class Consumer {
public static void main(String[] args) throws Exception {
// 获取到connection、channel
Connection connection = ConnectionUtil.getConnection();
Channel channel = connection.createChannel();
// 声明队列
String queueName = "test_direct_queue";
channel.queueDeclare(queueName, false, false, false, null);
// queue与exchange 绑定(匹配)
String exchangeName = "test_exchange_direct";
channel.queueBind(queueName, exchangeName, "direct_001");
// 定义队列的消费者
QueueingConsumer consumer = new QueueingConsumer(channel);
channel.basicConsume(queueName, true, consumer);
// 获取消息
while (true) {
QueueingConsumer.Delivery delivery = consumer.nextDelivery();
String message = new String(delivery.getBody());
System.out.println(" [Recv] Received '" + message + "'");
}
}
}
与 fanout的改动,如下:
不多说,先后启动生产者、消费者。
控制台:
exchange:
exchange与queue的 绑定:
queue:
以上就是使用 rabbitmq client 执行操作,下面我们再聊聊使用 spring boot 使用rabbitmq。
1.pom.xml
org.springframework.boot
spring-boot-starter-amqp
2.application.yml
spring:
rabbitmq:
host: 192.168.20.131
virtual-host: 20200117
application:
name: rabbitmq-boot
其他的用的默认配置
3.configuration
用的topic类型的exchange,然后绑定了一个队列
@Configuration
public class TopicRabbitMQConfig {
public final static String queueName = "test_topic_queue";
public final static String exchangeName = "test_exchange_topic";
/**
* 设置queue
*/
@Bean
public Queue topicQueue() {
return new Queue(queueName);
}
/**
* 设置exchange
*/
@Bean
TopicExchange exchange() {
return new TopicExchange(exchangeName);
}
/**
* 绑定 exchange 与 queue
*/
@Bean
Binding bindingExchangeMessage1() {
return BindingBuilder.bind(topicQueue()).to(exchange()).with("yi.#");
}
}
4.producer
producer要指定发布消息到的exchangeName 以及 routingKey的值。因为前面的配置类中,我们指定绑定的routingkey的规则为 yi.#,所以下面的消息会发布到 test_topic_queue队列中。
@RestController
@RequestMapping("producer")
public class Producer {
@Autowired
RabbitTemplate rabbitTemplate;
@PostMapping("/send")
public String send(@RequestParam("message") String message) {
Map mes = new HashMap<>();
mes.put("data", message);
rabbitTemplate.convertAndSend("test_exchange_topic", "yi.lei", mes);
return "消息发送成功";
}
}
5.consumer
consumer只要指定监听的队列名称就行。这里指定了test_topic_queue,所以当调用producer的接口后,会将消息发布到名为test_topic_queue的队列中,消费者监听到队列中有了数据后,就会进行了输出操作。
@Component
@RabbitListener(queues = "test_topic_queue")
public class Consumer {
@RabbitHandler
public void process(Map testMessage) {
System.out.println("消费者收到消息 : " + testMessage.toString());
}
}
6.调用接口
再回到 rabbitmq web控制台:
其中的细节,不多说了,前面说的比较清楚。spring boot整合rabbitmq,底层其实和rabbit client操作一样。