本节主要涉及到Rabbitmq在SpringBoot的API使用,基本是上一节的一个扩展,也会简单的介绍一下七种模型和Header Exchange的使用,不过使用场景从原生变成了SpringBoot封装。
对于七种模型和Header Exchange的介绍会很简单提及,如果想详细了解,可以看一下上一节内容。
Hello Word 模型采用默认Exchange,以Queue Name作为RoutingKey的方式进行消息发布。
具体实现如下
spring:
application:
name: Rabbitmq-AMQP
rabbitmq:
host: ****** # 这里填写ip
port: 5672
virtual-host: /test # 虚拟主机
username: admin
password: admin
/**
* @author heshuai
* @title: HelloWordConsumer
* @description: HelloWord模型 消息消费者
* 这里需要介绍几个注解
* @Component: Spring IOC注解,注入当前对象实例到ICO容器
* @RabbitListener: Rabbitmq监听器,监听Queue来接受消息,这个注解中包含了常用的接受消息时所涉及到的方法
* id:唯一标识该监听器,可以自动生成,无需特殊设置
* containerFactory:容器工厂,生成RabbitListener容器所用,一般使用默认工厂即可
* queues:所监听Queue的名称,可以为数组,即监听多个Queue;注:所指定Queue必须存在,或者在其它位置开始声明对应的bean
* queuesToDeclare:声明并监听该Queue,可以使用@Queue()去描述Queue
* exclusive:true,单一消息中独占Queue,并且要求并发量为1;false:默认
* bindings: 绑定Exchange和Queue,@QueueBinding可以使用这个注解去定义QueueBinding对象
* @Queue: 定义一个Queue对象,这个注解在queuesToDeclare属性中可以使用
* 在这个对象中,可以定义一个常规的Queue的所有配置,这个和之前Simple-module中的内容基本一致
* @date 2021年01月29日 11:17
*/
@Component
@RabbitListener(queuesToDeclare = @Queue("helloWord"))
public class HelloWordConsumer {
/**
* @RabbitListener 可以使用在类上或者是方法上,如果使用在类上,需要指定类中的一个方法作为接受消息的方法,
* 所以这里使用了@RabbitHandler
* 可以使用Object去接收消息,也可以使用String去接收:如果使用Object,那么接受的是Message对象,里面包含消息体、消息头等参数信息(不可以使用Message对象去直接接受)
* 如果使用String去接收,则只是接收到消息体
* @param message
*/
/*
@RabbitHandler
public void receive(Object message){
System.out.println("收到消息如下:");
System.out.println(new String(((Message)message).getBody()));
}*/
@RabbitHandler
public void receive(String message){
System.out.println("收到消息如下:");
System.out.println(message);
}
}
@SpringBootTest(classes = SpringbootModuleApplication.class)
class SpringbootModuleApplicationTests {
/**
* RabbitTemplate
* SpringFramework 提供一个类似于RedisTemplate、RestTemplate这些的一个高度封装的模板,
* 就是对于amqp-client的封装(没看过底层、猜测)
* 简单介绍一下功能
* 除了可以send、receive,还对这两个重载了很多方法,而且提供了更多如:convertAndSend、convertSendAndReceive这些更加深入的封装方法,
* 基本上也就是对于发送消息和接受消息的各种角度的封装了;
* 这个类中还包含一些特殊的默认值设置,比如:encoding、exchange、routingKey、defaultReceiveQueue,可以更加浓缩代码;
* 这里有个必须设置的配置项就是connectionFactory,不过这个也是可以通过application.yml直接配置的,没什么区别
* 除了这些以外还有一个事务的设置(setChannelTransacted()),这个就不深入了
*/
@Autowired
private RabbitTemplate rabbitTemplate;
/**
* HelloWord 模型 消息发布
*/
@Test
void helloWord() {
/**
* 为了介绍这个方法,使用这个方法重载最多参数的方法
* exchange:交换机名字,“”就是默认交换机
* routingKey:路由Key, 使用AMQP中的默认交换机,默认与所有的queue相绑定,queue名就是他们的绑定key
* object:发送的消息内容,因为convertAndSend方法对这个进行了封装,所以可以直接发送对象,不局限byte数组了
* CorrelationData:这个应该是用于发布者确认模型中所需要用到的参数
*/
rabbitTemplate.convertAndSend("","helloWord",String.format("我是一个简简单单的HelloWord\n你好!这个世界!\n愿世间没有病痛\n"), (CorrelationData)null);
}
}
当多个Consumer监听同一个Queue时,即是所谓的WorkQueue,Rabbitmq会采用轮询的方式将Queue的消息发送给C1、C2,不会造成同一条消息的重复消费!
具体实现如下
/**
* @author heshuai
* @title: WorkQueueConsumer
* @description: 工作模型——WorkQueue
* 因为需要在一个类中写多个监听处理,所以将@RaabitListener直接写到方法上机课
* @date 2021年02月02日 11:05
*/
@Component
public class WorkQueueConsumer {
/**
* @RabbitListener: 可以直接添加到方法上,表示该方法就是Rabbitmq监听器处理函数
*/
@RabbitListener(queuesToDeclare = @Queue(name = "workQueue"))
public void workQueue1(String message){
System.out.println("workQueue1收到消息如下:");
System.out.println(message);
}
@RabbitListener(queuesToDeclare = @Queue(name = "workQueue"))
public void workQueue2(String message){
System.out.println("workQueue2收到消息如下:");
System.out.println(message);
}
}
/**
* WorkQueue 模型 消息发布
*/
@Test
void workQueue(){
for (int i=0; i<10; i++){
rabbitTemplate.convertAndSend("workQueue",String.format("这是第"+i+"条祝福\n"));
}
}
订阅发布模式:官方的解释是一次性发布消息给多个Consumer,这里的Consumer可以理解成Queue,因为Consumer都是监听相应的Queue的!
如果需要实现一次性这个关键的话,那么必须使用AMQP协议中的Exchange概念,前两个模式使用的都是默认的Exchange,在下面这些模式中将去主动声明使用Exchange来进行转发Message。
具体实现如下
/**
* @author heshuai
* @title: SubscribeConsumer
* @description: 发布订阅模式: 这个模式是基于AMQP协议中Exchange概念,所以等下主要是看Exchange
* @Component: 交给Spring管理
* @date 2021年02月08日 16:45
*/
@Component
public class SubscribeConsumer {
/**
* 接受消息方法:
* @RabbitListener: bindings属性代表绑定Exchange和Queue,并且定义他们的RoutingKey
* @Exchange: 定义一个Exchange,根据它的模式定义,如果这个Exchange不存在则主动创建一个Exchange,type类型默认Direct。
* 并且在这个注解中支持x-delayed-message类型,默认 false
* @param message
*/
@RabbitListener(bindings = @QueueBinding(value = @Queue, // 生成一个临时Queue
exchange = @Exchange(name = "PubScrExchange", type = ExchangeTypes.FANOUT),
key = {"publish", "subscribe"}))
public void receive(String message) {
System.out.println("收到消息如下:");
System.out.println(message);
}
}
/**
* Publish/Subscribe模式
*/
@Test
void publishExchange(){
rabbitTemplate.convertAndSend("PubScrExchange","publish","我是Publish");
rabbitTemplate.convertAndSend("PubScrExchange","subscribe","我是Subscribe");
}
Routing模式也称之为路由模式,上一个模式是使用的Exchange的Fanout类型,将消息默认广播给所有相关联的Queue,在这个模式中就是有选择的将消息发送给Queue。
也就是Exchange的Direct类型,通过RoutingKey的方法来区分发送给哪些Queue。
具体实现如下
/**
* @author heshuai
* @title: RoutingConsumer
* @description: Routing模式,根据RoutingKey来选择性地将消息发送到某些Queue
* @date 2021年02月09日 9:46
*/
@Component
public class RoutingConsumer {
@RabbitListener(bindings = @QueueBinding(value = @Queue,
exchange = @Exchange(name = "DIRECT_ROUTING"), // 默认类型Direct
key = {"info","warn"}))
public void receive1(String message) {
System.out.printf("receive1 收到消息如下:\n %s \n",message);
}
@RabbitListener(bindings = @QueueBinding(value = @Queue,
exchange = @Exchange(name = "DIRECT_ROUTING"), // 默认类型Direct
key = {"info"}))
public void receive2(String message) {
System.out.printf("receive2 收到消息如下:\n %s \n",message);
}
@RabbitListener(bindings = @QueueBinding(value = @Queue,
exchange = @Exchange(name = "DIRECT_ROUTING"), // 默认类型Direct
key = {"error"}))
public void receive3(String message) {
System.out.printf("receive3 收到消息如下:\n %s \n",message);
}
}
/**
* Routing模式
*/
@Test
void RoutingExchange() throws InterruptedException {
rabbitTemplate.convertAndSend("DIRECT_ROUTING","info","我是info");
rabbitTemplate.convertAndSend("DIRECT_ROUTING","warn","我是warn");
rabbitTemplate.convertAndSend("DIRECT_ROUTING","error","我是error");
TimeUnit.SECONDS.sleep(1L);
}
Topics模式即是Exchange为topic,基于模式来匹配路由,这个模式中有两个通配符,*
和#
,其中*
代表一个单词,#
代表零个或多个单词。
具体实现如下
/**
* @author heshuai
* @title: TopicsConsumer
* @description: Topics模式,模型匹配模式
* @date 2021年02月09日 10:06
*/
@Component
public class TopicsConsumer {
@RabbitListener(bindings = @QueueBinding(value = @Queue,
exchange = @Exchange(name = "TOPIC_TOPICS",type = ExchangeTypes.TOPIC),
key = {"log.*"}))
public void receive1(String message){
System.out.printf("receive1 收到消息如下\n %s \n",message);
}
@RabbitListener(bindings = @QueueBinding(value = @Queue,
exchange = @Exchange(name = "TOPIC_TOPICS",type = ExchangeTypes.TOPIC),
key = {"log.#"}))
public void receive2(String message){
System.out.printf("receive2 收到消息如下\n %s \n",message);
}
@RabbitListener(bindings = @QueueBinding(value = @Queue,
exchange = @Exchange(name = "TOPIC_TOPICS",type = ExchangeTypes.TOPIC),
key = {"log.info"}))
public void receive3(String message){
System.out.printf("receive3 收到消息如下\n %s \n",message);
}
}
/**
* Topics模式
*/
@Test
void TopicsExchange() throws InterruptedException {
rabbitTemplate.convertAndSend("TOPIC_TOPICS","log.info","我是log.info");
rabbitTemplate.convertAndSend("TOPIC_TOPICS","log.warn.system","我是log.warn.system");
rabbitTemplate.convertAndSend("TOPIC_TOPICS","log.error","我是log.error");
TimeUnit.SECONDS.sleep(1L);
}
RPC模式上一节已经写的非常清晰了,使用Rabbitmq来实现RPC模式,实现发送与接收请求。
这部分内容参考自Spring官网
具体实现如下
/**
* @author heshuai
* @title: RPCServer
* @description: 在RPC模式中,消息的接受处理者一般也称之为服务端,接受处理响应请求的。
* @date 2021年02月09日 10:22
*/
@Component
public class RPCServer {
@Autowired
private RabbitTemplate rabbitTemplate;
/**
*
* @param task
* @param channel
* @throws IOException
*/
@RabbitListener(bindings = @QueueBinding(value = @Queue,
exchange = @Exchange(value = "RPCSERVER", type = ExchangeTypes.TOPIC),
key = {"task.#"}))
public void process(String taskMessage, Message task, Channel channel) throws IOException {
try {
// 获取任务
System.out.printf("收到任务如下:\n %s \n",taskMessage);
// 模拟处理任务
taskMessage +=",该任务已被处理";
// 得到CorrelationId
String correlationId = task.getMessageProperties().getCorrelationId();
// 得到回调Queue
String replyTo = task.getMessageProperties().getReplyTo();
MessageProperties messageProperties = new MessageProperties();
messageProperties.setCorrelationId(correlationId);
// 定义Message;
Message replyMessage = new Message(taskMessage.getBytes(),messageProperties);
// 回复Response结果
rabbitTemplate.send(replyTo, replyMessage);
// 手动确认消息
channel.basicAck(task.getMessageProperties().getDeliveryTag(),false);
}catch (IOException e){
channel.basicNack(task.getMessageProperties().getDeliveryTag(),false,true);
throw e;
}
}
}
/**
* RPC 模式
*/
@Test
void RPCClient() throws InterruptedException {
// convertSendAndReceive: 发送请求并等待相应结果,同步
// 在这个demo中 因为server发送回来的response是byte[]类型,所以这个回复也是byte[]
Object response = rabbitTemplate.convertSendAndReceive("RPCSERVER","task.info","我是一个简简单单的任务");
System.out.printf("收到回复如下:\n %s \n",new String((byte[]) response));
TimeUnit.SECONDS.sleep(1L);
}
注: 这里真的是非常非常简单的一个的demo了,用在生产下是远远不够的,只是辅助于理解Rabbitmq中的RPC模式,就像是本博客标题一样,初步学习,在文档中还有很多优化的实现,如异步处理返回消息,自定义MessageConvert,container容器等。
补充: 如果上面例子有看不懂的,可以留言,建议看一下上一节博客,其实这个和直接使用amqp-client没有太多区别,只是RabbitTemplate封装了消息转换,默认创建了correlationId和返回response 的临时queue来接受消息。就解释到这吧
官方解释:可靠的消息发布
意思就是确保Client端的每一次消息都是可以确保到达Broker。
spring:
application:
name: Rabbitmq-AMQP
rabbitmq:
host: 59.110.41.57
port: 5672
virtual-host: /test # 虚拟主机
username: admin
password: admin
publisher-returns: true # 是否激活发布者返回
publisher-confirm-type: correlated # 消息发布确认类型
/**
* Publisher Confirms 模式
*/
@Test
void PublisherConfirmsClient() throws InterruptedException, TimeoutException, ExecutionException {
// 为当前rabbitTemplate实例设置一个唯一的ConfirmCallback发布者确认异步回调方法
this.rabbitTemplate.setConfirmCallback((CorrelationData correlationData, boolean ack, String cause) ->{
// 打印参数
System.out.printf("\n CorrelationData:%s, \t cause:%s \n", JSON.toJSONString(correlationData),cause);
if(ack){
System.out.println("已确认");
}else{
System.out.println("消息未确认,需记录未确认原因及重新发送消息");
}
});
String messageBody = "模拟发布者确认";
Message message = new Message(messageBody.getBytes(),null);
ReturnedMessage returned = new ReturnedMessage(message,1,null,null,null);
// 发布者返回的数据,可以在这里记录发送的消息体,以备后续逻辑处理
CorrelationData correlationData = new CorrelationData();
correlationData.setReturned(returned);
// 发送消息
this.rabbitTemplate.convertAndSend("helloWord", (Object) "模拟发布者确认",correlationData);
// 也可以不设置统一回调方法,而是使用这种简单的属性future来获取是否发布成功
System.out.println(correlationData.getFuture().get(200,TimeUnit.MILLISECONDS).isAck());
System.out.println(correlationData.getFuture().get(200,TimeUnit.MILLISECONDS).getReason());
TimeUnit.SECONDS.sleep(1L);
}
这里提供了两种方式,其实都是异步进行的,一种是统一为rabbitTemplate实例设置一个回调方法,等待发布确认返回后执行回调方法;第二种是通过CorrelationData 中的future属性来简单获取到是否发布成功及发布失败原因。
能力有限,这块感觉写的并不是很好,但是这个春节还有其他的学习计划,不深入了,有感兴趣的可以看一下Spring AMQP文档,以后我工作中用到或者有时间了也会复盘这部分内容的,到时候再来完善这篇文章。
这里就没有图了,使用Header类型其实和Direct类型类似,都是直接匹配,不过一个是关联路由key,一个是设置关联参数,这个设置对应的关联参数有一个需要注意的特殊值,就是x-match,它是用来定义Header的匹配形式,有两个值,分别是all(默认)、any,如果有多对k-v的参数的话,
x-match值为all,那么必须匹配所有k-v参数才可以路由到这个queue;
x-match值为any,那么只需要匹配其中任意一个k-v参数就可以路由到这个queue了。
大概就是上边这个意思,这个api还是很简单的,没有太多的注意点
具体实现如下
/**
* @author heshuai
* @title: HeaderConsumer
* @description: Exchange Header类型
* @date 2021年02月12日 16:49
*/
@Component
public class HeaderConsumer {
/**
* arguments: Queue和Exchange绑定时所涉及到的参数,一般就是Exchange类型为Header才会用到
* @Argument: 就是专门设置相对应的参数的,k-v的形式,
* 在这个类型中,有一个特殊的参数就是x-match,它是用来定义Header的匹配形式,有两个值,分别是all(默认)、any,如果有多对k-v的参数的话,
* 并且x-match值为all,那么必须匹配所有k-v参数才可以路由到这个queue;
* x-match值为any,那么只需要匹配其中任意一个k-v参数就可以路由到这个queue了
*/
@RabbitListener(bindings = @QueueBinding(value = @Queue,
exchange = @Exchange(name = "EXCHANGE_HEADER", type = ExchangeTypes.HEADERS),
arguments = {@Argument(name = "x-match", value = "all"),
@Argument(name = "key", value = "log.error"),
@Argument(name = "flag", value = "error")}))
public void receive1(String message){
System.out.printf("receive1 收到消息: %s \n",message);
}
@RabbitListener(bindings = @QueueBinding(value = @Queue,
exchange = @Exchange(name = "EXCHANGE_HEADER", type = ExchangeTypes.HEADERS),
arguments = {@Argument(name = "x-match", value = "any"),
@Argument(name = "key", value = "log.info"),
@Argument(name = "flag", value = "log")}))
public void receive2(String message){
System.out.printf("receive2 收到消息: %s \n",message);
}
@RabbitListener(bindings = @QueueBinding(value = @Queue,
exchange = @Exchange(name = "EXCHANGE_HEADER", type = ExchangeTypes.HEADERS),
arguments = {@Argument(name = "x-match", value = "all"),
@Argument(name = "key", value = "info.error"),
@Argument(name = "flag", value = "info")}))
public void receive3(String message){
System.out.printf("receive3 收到消息: %s \n",message);
}
}
/**
* Exchange Header类型
*/
@Test
void exchangeHeader() throws InterruptedException {
this.rabbitTemplate.convertAndSend("EXCHANGE_HEADER","","key=info.error,flag=error消息",
(Message message) ->{
MessageProperties messageProperties = message.getMessageProperties();
messageProperties.setHeader("key","info.error");
messageProperties.setHeader("flag","info");
return message;
});
this.rabbitTemplate.convertAndSend("EXCHANGE_HEADER","","key=我是瞎写的,flag=log消息",
(Message message) ->{
MessageProperties messageProperties = message.getMessageProperties();
messageProperties.setHeader("key","我是瞎写的");
messageProperties.setHeader("flag","log");
return message;
});
this.rabbitTemplate.convertAndSend("EXCHANGE_HEADER","","key=log.error,flag=info消息",
(Message message) ->{
MessageProperties messageProperties = message.getMessageProperties();
messageProperties.setHeader("key","log.error");
messageProperties.setHeader("flag","error");
return message;
});
TimeUnit.SECONDS.sleep(1L);
}
这里简单介绍Rabbitmq的经典应用场景。
举一个网上典型的例子吧
用户注册系统,一般这种系统需要在用户注册成功后给用户发送短信或者邮件,传统是有两种方式去解决,分别是串行、并行。
串行顾名思义就是一条线的去执行,在用户发起注册请求后,系统完成将注册信息录入到数据库中,这个时候其实已经完成了用户注册了,但是因为有额外的任务就是发送短信、邮件,只有这两步骤完成后才能返回给用户。这无疑让用户进行了无意义的等待,如下图:
并行方式就是将发送邮件和短信同时进行,这样就可以减少一个50ms的时间,但是这种方式其实也是有一些无意义的等待
使用broker消息中间件的方式异步处理,其实就是让用户不进行无意义的等待,写入数据库后立即返回。在返回前向broker发送一个请求即可
这样大大增加了效率。
在正常的微服务中,一个系统是由多个子服务组成,可能完成一个业务逻辑就会经过很多的服务逻辑,这个时候这些服务之间就会强耦合在一起,比如A服务调用B服务,那么B服务因为一个特殊的原因失败(注:也可降级处理),就会导致A服务也随之失败!
那么在一些特殊的业务中,B服务不是必要的业务流程,或者不是需要立即执行的业务逻辑,那么可以允许B服务失败,A服务成功执行。后续再重新执行B服务这种情况
下面这种就是通过MQ进行的服务解耦
订单系统只需要将任务发送给了 消息队列即可任务时执行成功了,那么就可以给用户返回成功!这个时候哪怕库存系统执行失败,也不会应用最后的程序效果,因为当处理库存系统所遇到的错误,那么就可以重新将错误的任务从消息队列中订阅到,并且执行!
这个主要针对于高并发的时候,在服务调用的时候,防止A服务调用B服务时太多的请求导致B服务负担不了,而使用消息中间件起到一个缓冲、限流的作用!
这章也是非常基础的API应用,有些地方写的也不够好,主要也是因为目前没有对应的应用场景,以后工作中如果用到再来补充,感觉不管是Rabbitmq文档还是Spring文档,都是很清晰的!
下一节就是Rabbitmq 的集群部署!!!