同步调用的优点:
时效性较强,可以立即得到结果
同步调用的问题:
异步通信的优点:
异步通信的缺点:
MQ (MessageQueue),中文是消息队列,字面来看就是存放消息的队列。也就是事件驱动架构中的Broker。
方式一:在线拉取
docker pull rabbitmq:3-management
方式二:从本地加载
在课前资料已经提供了镜像包:上传到虚拟机中后,使用命令加载镜像即可:
docker load -i mq.tar
docker run \
-e RABBITMQ_DEFAULT_USER=yrh \
-e RABBITMQ_DEFAULT_PASS=123456 \
--name mq \
--hostname mq1 \
-p 15672:15672 \
-p 5672:5672 \
-d \
rabbitmq:3-management
yrh为用户名,123456即为密码
15672为登录端口,5672则为通信端口。登录进入如下界面
RabbitMQ中的几个概念:
channel:操作MQ的工具
exchange:路由消息到队列中
queue:缓存消息
virtual host:虚拟主机,是对queue、exchange等资源的逻辑分组
基本消息队列
publisher:消息发布者,将消息发送到队列queue
queue:消息队列,负责接受并缓存消息
consumer:订阅队列,处理队列中的消息
基本消息队列的消息发送流程:
public class PublisherTest {
@Test
public void testSendMessage() throws IOException, TimeoutException {
// 1.建立连接
ConnectionFactory factory = new ConnectionFactory();
// 1.1.设置连接参数,分别是:主机名、端口号、vhost、用户名、密码
factory.setHost("192.168.153.129");
factory.setPort(5672);
factory.setVirtualHost("/");
factory.setUsername("yrh");
factory.setPassword("123456");
// 1.2.建立连接
Connection connection = factory.newConnection();
// 2.创建通道Channel
Channel channel = connection.createChannel();
// 3.创建队列
String queueName = "simple.queue";
channel.queueDeclare(queueName, false, false, false, null);
// 4.发送消息
String message = "hello, rabbitmq!";
channel.basicPublish("", queueName, null, message.getBytes());
System.out.println("发送消息成功:【" + message + "】");
// 5.关闭通道和连接
channel.close();
connection.close();
}
}
基本消息队列的消息接收流程:
public class ConsumerTest {
public static void main(String[] args) throws IOException, TimeoutException {
// 1.建立连接
ConnectionFactory factory = new ConnectionFactory();
// 1.1.设置连接参数,分别是:主机名、端口号、vhost、用户名、密码
factory.setHost("192.168.153.129");
factory.setPort(5672);
factory.setVirtualHost("/");
factory.setUsername("yrh");
factory.setPassword("123456");
// 1.2.建立连接
Connection connection = factory.newConnection();
// 2.创建通道Channel
Channel channel = connection.createChannel();
// 3.创建队列
String queueName = "simple.queue";
channel.queueDeclare(queueName, false, false, false, null);
// 4.订阅消息
channel.basicConsume(queueName, true, new DefaultConsumer(channel){
@Override
public void handleDelivery(String consumerTag, Envelope envelope,
AMQP.BasicProperties properties, byte[] body) throws IOException {
// 5.处理消息
String message = new String(body);
System.out.println("接收到消息:【" + message + "】");
}
});
System.out.println("等待接收消息。。。。");
}
}
流程如下:
1、在父工程中引入spring-amqp的依赖
org.springframework.boot
spring-boot-starter-amqp
2、在publisher服务中利用RabbitTemplate发送消息到simple.queue这个队列
添加配置文件
spring:
rabbitmq:
host: 192.168.153.129 # 主机名
port: 5672 # 端口
virtual-host: / # 虚拟主机
username: yrh # 用户名
password: 123456 # 密码
添加测试类
@RunWith(SpringRunner.class)
@SpringBootTest
public class springAmpqTest {
@Autowired
private RabbitTemplate rabbitTemplate;
@Test
public void testSimpleQueue() {
String queueName = "simple.queue";
String message = "hello,spring ampq!";
rabbitTemplate.convertAndSend(queueName, message);
}
}
3、在consumer服务中编写消费逻辑,绑定simple.queue这个队列
添加配置文件
spring:
rabbitmq:
host: 192.168.153.129 # 主机名
port: 5672 # 端口
virtual-host: / # 虚拟主机
username: yrh # 用户名
password: 123456 # 密码
@Component
public class SpringRabbitListener {
@RabbitListener(queues = "simple.queue")
public void listenSimpleQueueMessage(String msg) throws InterruptedException {
System.out.println("spring 消费者接收到消息 :【" + msg + "】");
}
}
Work queue,工作队列,可以提高消息处理速度,避免队列消息堆积
模拟WorkQueue,实现一个队列绑定多个消费者
基本思路如下:
在publisher服务中定义测试方法,每秒产生50条消息,发送到simple.queue
@Test
public void testSimpleQueue2() throws InterruptedException {
String queueName = "simple.queue";
String message = "hello,message__";
for (int i = 0; i <= 50; i++) {
rabbitTemplate.convertAndSend(queueName, message + i);
Thread.sleep(20);
}
}
在consumer服务中定义两个消息监听者,都监听simple.queue队列
@Component
public class SpringRabbitListener {
@RabbitListener(queues = "simple.queue")
public void listenWorkQueue(String msg) throws InterruptedException {
System.out.println("消费者1接收到消息 :【" + msg + "】"+ LocalDateTime.now());
Thread.sleep(20);
}
@RabbitListener(queues = "simple.queue")
public void listenWorkQueue2(String msg) throws InterruptedException {
System.err.println("..消费者2..接收到消息 :【" + msg + "】"+LocalDateTime.now());
Thread.sleep(200);
}
}
消费者1每秒处理50条消息,消费者2每秒处理10条消息
最终结果时并未在1s内处理完成,因为存在消息预取,指的是消费者1和2在未消费完时提前将50个消息进行了平分,使得消息2拿到25个消息,最终耗时5s,因此需要控制预取消息的上限
更改消费者的配置文件如下:
spring:
rabbitmq:
host: 192.168.153.129 # 主机名
port: 5672 # 端口
virtual-host: / # 虚拟主机
username: yrh # 用户名
password: 123456 # 密码
listener:
simple:
prefetch: 1 # 每次只能获取一条消息,处理完成才能获取下一个消息
Work模型的使用:
多个消费者绑定到一个队列,同一条消息只会被一个消费者处理
通过设置prefetch来控制消费者预取的消息数量
常见exchange类型包括:
Fanout:广播
Direct:路由
Topic:话题
注意:exchange负责消息路由,而不是存储,路由失败则消息丢失
Fanout Exchange 会将接收到的消息广播到每一个跟其绑定的queue
实现思路如下:
在consumer服务中,利用代码声明队列、交换机,并将两者绑定
@Component
public class FanoutConfig {
//定义交换机
@Bean
public FanoutExchange fanoutExchange() {
return new FanoutExchange("yrh.fanout");
}
// 定义队列1
@Bean
public Queue fanoutQueue1() {
return new Queue("fanout.queue1");
}
// 绑定队列1到交换机
@Bean
public Binding fanoutBinding1(Queue fanoutQueue1, FanoutExchange fanoutExchange) {
return BindingBuilder.bind(fanoutQueue1).to(fanoutExchange);
}
// 定义队列2
@Bean
public Queue fanoutQueue2() {
return new Queue("fanout.queue2");
}
// 绑定队列2到交换机
@Bean
public Binding fanoutBinding2(Queue fanoutQueue2, FanoutExchange fanoutExchange) {
return BindingBuilder.bind(fanoutQueue2).to(fanoutExchange);
}
}
在consumer服务中,编写两个消费者方法,分别监听fanout.queue1和fanout.queue2
@RabbitListener(queues = "fanout.queue1")
public void listenFanoutQueue1(String msg) throws InterruptedException {
System.out.println("消费者1接收到消息 :【" + msg + "】"+ LocalDateTime.now());
}
@RabbitListener(queues = "fanout.queue2")
public void listenFanoutQueue2(String msg) throws InterruptedException {
System.err.println("..消费者2..接收到消息 :【" + msg + "】"+LocalDateTime.now());
}
在publisher中编写测试方法,向yrh.fanout发送消息
@Test
public void testSendFanoutExchange(){
// 交换机名称
String exchangName="yrh.fanout";
// 消息
String message="hello,every one!";
// 发送消息
rabbitTemplate.convertAndSend(exchangName,"",message);
}
运行之后,消费者打印信息如下:
消费者1接收到消息 :【hello,every one!】2023-03-18T18:10:31.792
..消费者2..接收到消息 :【hello,every one!】2023-03-18T18:10:31.792
将接收到的消息根据规则路由到指定的Queue,因此称为路由模式(routes)。
每一个Queue都与Exchange设置一个BindingKey
发布者发送消息时,指定消息的RoutingKey
Exchange将消息路由到BindingKey与消息RoutingKey一致的队列
实现思路如下:
利用@RabbitListener声明Exchange、Queue、RoutingKey
@RabbitListener(bindings = @QueueBinding(value = @Queue(name="direct.queue1"),
exchange = @Exchange(name="yrh.direct",type = ExchangeTypes.DIRECT),key={"red","blue"}) )
public void listenDirectQueue1(String msg) throws InterruptedException {
System.out.println("消费者1接收到消息 :【" + msg + "】"+ LocalDateTime.now());
}
@RabbitListener(bindings = @QueueBinding(value = @Queue(name="direct.queue2"),
exchange = @Exchange(name="yrh.direct",type = ExchangeTypes.DIRECT),key={"red","yellow"}) )
public void listenDirectQueue2(String msg) throws InterruptedException {
System.out.println("消费者2接收到消息 :【" + msg + "】"+ LocalDateTime.now());
}
在consumer服务中,编写两个消费者方法,分别监听direct.queue1和direct.queue2
在publisher中编写测试方法,向itcast. direct发送消息
@Test
public void testSendDirectExchange(){
// 交换机名称
String exchangName="yrh.direct";
// 消息
String message="hello,blue!";
// 发送消息
rabbitTemplate.convertAndSend(exchangName,"blue",message);
}
发送blue只有消费者1可以接收到
总结;
描述下Direct交换机与Fanout交换机的差异?
- Fanout交换机将消息路由给每一个与之绑定的队列
- Direct交换机根据RoutingKey判断路由给哪个队列
- 如果多个队列具有相同的RoutingKey,则与Fanout功能类似
基于@RabbitListener注解声明队列和交换机有哪些常见注解?
@Queue
@Exchange
TopicExchange与DirectExchange类似,routingKey必须是多个单词的列表,并且以 . 分割。
Queue与Exchange指定BindingKey时可以使用通配符:
#:代指0个或多个单词
*:代指一个单词
实现思路如下:
并利用@RabbitListener声明Exchange、Queue、RoutingKey
在consumer服务中,编写两个消费者方法,分别监听topic.queue1和topic.queue2
@RabbitListener(bindings = @QueueBinding(value = @Queue(name="topic.queue1"),
exchange = @Exchange(name="yrh.topic",type = ExchangeTypes.TOPIC),key="china.#") )
public void listenTopicQueue1(String msg) throws InterruptedException {
System.out.println("消费者1接收到消息 :【" + msg + "】"+ LocalDateTime.now());
}
@RabbitListener(bindings = @QueueBinding(value = @Queue(name="topic.queue2"),
exchange = @Exchange(name="yrh.topic",type = ExchangeTypes.TOPIC),key="#.news") )
public void listenTopicQueue2(String msg) throws InterruptedException {
System.out.println("消费者2接收到消息 :【" + msg + "】"+ LocalDateTime.now());
}
在publisher中编写测试方法,向itcast. topic发送消息
@Test
public void testSendTopictExchange(){
// 交换机名称
String exchangName="yrh.topic";
// 消息
String message="yrh好帅啊啊啊啊";
// 发送消息
rabbitTemplate.convertAndSend(exchangName,"china.news",message);
}
在SpringAMQP的发送方法中,接收消息的类型是Object,也就是说我们可以发送任意对象类型的消息,SpringAMQP会帮我们序列化为字节后发送。
@Bean
public Queue objectQueue(){
return new Queue("object.queue");
}
@Test
public void testSendObjectQueue(){
Map msg=new HashMap<>();
msg.put("name","yrh");
msg.put("age",21);
rabbitTemplate.convertAndSend("object.queue",msg);
}
顺利收到消息,但被序列化了,如下图:
输入消息转换器的使用:
添加依赖
com.fasterxml.jackson.core
jackson-databind
申明服务MessageConverter
@Bean
public MessageConverter messageConverter(){
return new Jackson2JsonMessageConverter();
}
重新运行测试如下:正确输出json格式
输出消息转换器的使用:
添加依赖及申明服务MessageConverter和输入消息转换器保持一致
@RabbitListener(queues = "object.queue")
public void listerObjectQueue(Map msg){
System.out.println("接收到的消息:"+msg);
}
接收到的消息:{name=yrh, age=21}
打印台正确输出
SpringAMQP中消息的序列化和反序列化是怎么实现的?
利用MessageConverter实现的,默认是JDK的序列化
注意发送方与接收方必须使用相同的MessageConverter