同步调用:
异步调用:
MQ(MessageQueue),中文是消息队列,字面来看就是存放消息的队列。也就是异步调用中的Broker。
RabbitMQ | ActiveMQ | RocketMQ | Kafka | |
---|---|---|---|---|
公司/社区 | Rabbit | Apache | 阿里 | Apache |
开发语言 | Erlang | Java | Java | Scala&Java |
协议支持 | AMQP、XMPP、SMTP、STOMP | OpenWire、STOMP、REST、XMPP、AMQP | 自定义协议 | 自定义协议 |
可用性 | 高 | 一般 | 高 | 高 |
单机吞吐量 | 一般 | 差 | 高 | 非常高 |
消息延迟 | 微秒级 | 毫秒级 | 毫秒级 | 毫秒以内 |
消息可靠性 | 高 | 一般 | 高 | 一般 |
RabbitMQ是基于Erlang语言开发的开源消息通信中间件,官网地址
同样基于Docker来安装RabbitMQ,使用下面的命令即可:
docker run \
-e RABBITMQ_DEFAULT_USER=admin \
-e RABBITMQ_DEFAULT_PASS=admin123 \
-v mq-plugins:/plugins \
--name mq \
--hostname mq \
-p 15672:15672 \
-p 5672:5672 \
--network hmall \
-d \
rabbitmq:3.8-management
15672:RabbitMQ提供的管理控制台的端口
5672:RabbitMQ的消息发送处理接口
安装完成后访问管理控制台。首次访问需要登录,默认的用户名和密码在配置文件中已经指定了。 登录后即可看到管理控制台总览页面:
RabbitMQ对应的整体架构核心概念:
SpringAMQP的官方地址
引入spring-amqp依赖,这样publisher和consumer服务都可以使用:
<!——AMQP依赖,包含RabbitMQ-->
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-amqpartifactId>
dependency>
配置RabbitMQ服务端信息
spring:
rabbitmq:
host: 192.168.100.101 #主机名
port: 5672 # 端口
virtual-host: /liner #虚拟主机
username: admin #用户名
password: admin123 #密码
发送消息:SpringAMQP提供了RabbitTemplate工具类,方便我们发送消息。
@Autowired
private RabbitTemplate rabbitTemplate;
@Test
public void testSimpleQueue() {
//队列名称
String queueName = "simple.queue";
//消息
String message = "hello,spring amqp!";
//发送消息
rabbitTemplate.convertAndSend(queueName,message);
}
接收消息:SpringAMQP提供声明式的消息监听,只需通过注解在方法上声明要监听的队列名称,将来SpringAMQP就会把消息传递给当前方法
@slf4j
@Component
public class SpringRabbitListener {
@RabbitListener(queues = "simple.queue")
public void listenSimpleQueueMessage(String msg) throws InterruptedException {
log.info("spring消费者接收到消息:【"+ msg + "】");
if (true) {
throw new MessageconversionException("故意的");
}
log.info("消息处理完成");
}
}
Work queues,任务模型。让多个消费者绑定到一个队列,共同消费队列中的消息。
消费者消息推送限制
默认情况下,RabbitMQ会将消息依次轮询投递给绑定在队列上的每一个消费者。这并没有考虑到消费者是否已经处理完消息,可能出现消息堆积。
因此需要修改application.yml
,设置preFetch
值为1,确保同一时刻最多投递给消费者1条消息
spring:
rabbitmq:
listener:
simple:
prefetch: 1 #每次只能获取一条消息,处理完成才能获取下一个消息
work模型的使用:
真正生产环境都会经过exchange来发送消息,而不是直接发送到队列,交换机的类型有以下三种:
交换机的作用:
Fanout Exchange:会将接收到的消息广播到每一个跟其绑定的queue,所以也叫广播模式。
Direct Exchange :会将接收到的消息根据规则路由到指定的Queue,因此称为定向路由。
TopicExchange:与DirectExchange类似,区别在于routingKey可以是多个单词的列表,并且以 “.” 分割。
Queue与Exchange指定BIndingKey时可以使用通配符:
#
:代指0个或多个单词*
:代指一个单词SpringAMQP提供了几个类,用来声明队列、交换机及其绑定关系:
@Configuration
public class FanoutConfiguration{
@Bean
public FanoutExchange fanoutExchange(){
//ExchangeBuilder.fanoutExchange("").build();
return new FanoutExchange("liner.fanout");
}
@Bean
public Queue fanoutQueue(){
//QueueBuilder.durable("").build();
return new Queue("fanout.queue");
}
@Bean
public Binding fanoutBinding(Queue fanoutQueue,FanoutExchange fanoutExchange){
return BindingBuilder.bind(fanoutQueue).to(fanoutExchange);
}
@Bean
public Binding fanoutBinding2(){
return BindingBuilder.bind(fanoutQueue()).to(fanoutExchange());
}
}
SpringAMQP还提供了基于 @RabbitListener 注解来声明队列和交换机的方式:
@RabbitListener(bindings = @QueueBinding(
value = Queue (name = "direct.queue",durable = "true"),exchange = @Exchange(name = "liner.direct",type = ExchangeTypes.DIRECT),key = {"red","blue"}
))
public void listenDirectQueue(String msg){
System.out.println("消费者1接收到Direct消息:【"+msg+"】");
}
而在数据传输时,它会把你发送的消息序列化为字节发送给MQ,接收消息的时候,还会把字节反序列化为Java对象。 默认情况下Spring采用的序列化方式是JDK序列化。存在问题:数据体积过大、有安全漏洞、可读性差
因此建议采用JSON序列化代替默认的JDK序列化
publisher
和consumer
两个服务中都引入依赖:<dependency>
<groupId>com.fasterxml.jackson.dataformatgroupId>
<artifactId>jackson-dataformat-xmlartifactId>
<version>2.9.10version>
dependency>
注意,如果项目中引入了spring-boot-starter-web
依赖,则无需再次引入Jackson
依赖。
publisher
和consumer
两个服务的启动类中添加Bean:@Bean
public MessageConverter messageConverter(){
// 1.定义消息转换器
Jackson2JsonMessageConverter jackson2JsonMessageConverter = new Jackson2JsonMessageConverter();
// 2.配置自动创建消息id,用于识别不同消息,也可以在业务中基于ID判断是否是重复消息
jackson2JsonMessageConverter.setCreateMessageIds(true);
return jackson2JsonMessageConverter;
}
消息转换器中添加的messageId可以便于我们将来做幂等性判断
有的时候由于网络波动,可能会出现客户端连接MQ失败的情况。通过配置我们可以开启连接失败后的重连机制:
spring:
rabbitmq:
connection-timeout: 1s #设置MQ的连接超时时间
template:
retry:
enabled: true #开启超时重试机制
initial-interval: 1000ms #失败后的初始等待时间
multiplier: 1 # 失败后下次的等待时长倍数,下次等待时长 = initial-interval * multiplier
max-attempts: 3 #最大重试次数
注意:当网络不稳定的时候,利用重试机制可以有效提高消息发送的成功率。不过SpringAMQP提供的重试机制是阻塞式的重试,也就是说多次重试等待的过程中,当前线程是被阻塞的,会影响业务性能。如果对于业务性能有要求,建议禁用重试机制。如果一定要使用,请合理配置等待时长和重试次数,当然也可以考虑使用异步线程来执行发送消息的代码。
RabbitMQ有Publisher Confirm和Publisher Return两种确认机制。开启确机制认后,在MQ成功收到消息后会返回确认消息给生产者。返回的结果有以下几种情况:
在publisher这个微服务的application.yml中添加配置:
spring:
rabbitmq:
publisher-confirm-type: correlated #开启publisher confirm机制,并设置confirm类型
publisher-returns: true #开启publisher return机制
#这里publisher-confirm-type有三种模式可选:
# none: 关闭confirm机制
# simple: 同步阻塞等待MQ的回执消息
# correlated: MQ异步回调方式返回回执消息
每个RabbitTemplate只能配置一个ReturnCallback,因此需要在项目启动过程中配置:
@Slf4j
@Configuration
public class CommonConfig implements ApplicationContextAware {
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
//获取RabbitTemplate
RabbitTemplate rabbitTemplate = applicationContext.getBean(RabbitTemplate.class);
//设置ReturnCallback
rabbitTemplate.setReturnCallback((message,replyCode,replyText,exchange,routingKey) -> {
log.info("消息发送失败,应答码{{},原因{},交换机{},路由键{},消息{}",
replyCode,replyText,exchange,routingKey,message.toString());
});
}
}
发送消息,指定消息ID、消息ConfirmCallback
@Test
void testPublisherConfirm() throws InterruptedException {
//1.创建CorrelationData
CorrelationData cd = new CorrelationData();
//2.给Future添加ConfirmCallback
cd.getFuture().addCallback(new ListenableFutureCallback<CorrelationData.Confirm>() {
@Override
public void onFailure(Throwable ex) {
// 2.1.Future发生异常时的处理逻辑,基本不会触发
log.error("handle message ack fail", ex);
}
@Override
public void onSuccess(CorrelationData.Confirm result) {
// 2.2.Future接收到回执的处理逻辑,参数中的result就是回执内容
if(result.isAck()){ // result.isAck(), boolean类型, true代表ack回执,false代表nack回执
log.debug("发送消息成功,收到ack!");
}else{ // result.getReason(), String类型,返回nack时的异常描述
log.error("发送消息失败,收到nack,reason:{}",result.getReason());
}
}
});
//3.发送消息
rabbitTemplate.convertAndSend("liner.direct","red","hello",cd);
}