一、SpringBoot整合RabbitMQ
1、环境准备
1、由于在上一篇中的创建项目是SpringBoot的,直接修改RabbitMQ依赖即可,删除掉之前使用的RabbitMQ依赖
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-amqpartifactId>
dependency>
2、在yml中添加RabbitMQ的配置
server:
port: 9000
spring:
rabbitmq:
host: 101.26.156.147
virtual-host: test_host
port: 5672
username: test
password: 123456
3、启动类
@ComponentScan(basePackages = {"com.itan.*"})
@SpringBootApplication
public class RabbitmqApplication {
public static void main(String[] args) {
SpringApplication.run(RabbitmqApplication.class, args);
}
}
2、创建交换器与队列并实现绑定关系
1、实现下图中的关系,先将RabbitMQ中已经创建的删除掉
import org.springframework.amqp.core.*;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.HashMap;
import java.util.Map;
@Configuration
public class RabbitMQInitConfig {
private static final String NORMAL_EXCHANGE = "normal_exchange";
private static final String DEAD_EXCHANGE = "dead_exchange";
private static final String NORMAL_QUEUE = "normal_queue";
private static final String DEAD_QUEUE = "dead_queue";
@Bean("normalExchange")
public DirectExchange normalExchange() {
return new DirectExchange(NORMAL_EXCHANGE);
}
@Bean("deadExchange")
public DirectExchange deadExchange() {
return new DirectExchange(DEAD_EXCHANGE);
}
@Bean("normalQueue")
public Queue normalQueue() {
Map<String, Object> arguments = new HashMap<>();
arguments.put("x-message-ttl", 10000);
arguments.put("x-dead-letter-exchange", DEAD_EXCHANGE);
arguments.put("x-dead-letter-routing-key", "b");
return QueueBuilder.durable(NORMAL_QUEUE).withArguments(arguments).build();
}
@Bean("deadQueue")
public Queue deadQueue() {
Queue queue = new Queue(DEAD_QUEUE);
return queue;
}
@Bean
public Binding normalQueueBindingNormalExchange(@Qualifier("normalQueue") Queue normalQueue, @Qualifier("normalExchange") DirectExchange normalExchange) {
return BindingBuilder.bind(normalQueue).to(normalExchange).with("a");
}
@Bean
public Binding deadQueueBindingDeadExchange(@Qualifier("deadQueue") Queue deadQueue, @Qualifier("deadExchange") DirectExchange deadExchange) {
return BindingBuilder.bind(deadQueue).to(deadExchange).with("b");
}
}
3、收发消息测试
import com.alibaba.fastjson.JSON;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@Slf4j
@RestController
@RequestMapping("/send")
public class SendMessageCotroller {
@Autowired
private RabbitTemplate rabbitTemplate;
@GetMapping("message/{message}")
public void sendMessage(@PathVariable String message) {
log.info("发送消息到MQ - 入参 [{}]", JSON.toJSONString(message));
rabbitTemplate.convertAndSend("normal_exchange", "a", ("来自normal_queue的死信消息:" + message).getBytes());
log.info("发送消息到MQ - 完成");
}
@GetMapping("message/{message}/{ttlTime}")
public void sendMessageTTL(@PathVariable String message, @PathVariable String ttlTime) {
log.info("发送带有过期时间的消息到MQ - 消息内容 [{}] - 存活时间 [{}]", JSON.toJSONString(message), JSON.toJSONString(ttlTime));
rabbitTemplate.convertAndSend("normal_exchange", "a", message.getBytes(), correlationData -> {
correlationData.getMessageProperties().setExpiration(ttlTime);
return correlationData;
});
log.info("发送带有过期时间的消息到MQ - 完成");
}
}
import com.rabbitmq.client.Channel;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
@Slf4j
@Component
public class ReceiveMessage {
@RabbitListener(queues = {"dead_queue"})
public void receive1(Message message, Channel channel) {
log.info("收到dead_queue队列的消息:[{}]", new String(message.getBody()));
}
@RabbitListener(queues = {"queue1"})
public void receive1(String msg) {
log.info("接收到queue1队列的消息:[{}]", msg);
}
}
1、启动项目,发送两个请求
-
http://localhost:9000/send/message/测试过期
-
http://localhost:9000/send/message/带有过期时间的消息/5000
2、运行结果
4、消息序列化及传输对象信息
1、前面的都是发送字符串类型的消息,如果发送的消息是个对象,那么需要使用序列化机制将对象写出去,对象必须实现序列化(Serializable)接口
。
2、涉及网络传输的应用,序列化是不可避免的,发送端以某种规则将消息转成byte数组发送,接收端则以约定的规则进行byte数组解析。
3、RabbitMQ的序列化是指Message的body属性(即真正需要传输的内容)。RabbitMQ抽象出一个MessageConverter接口处理消息的序列化,其实现有SimpleMessageConverter(默认)、Jackson2JsonMessageConverter等
。
4、当调用了convertAndSend方法时会使用MessageConvert
进行消息的序列化。SimpleMessageConverter对于要发送的消息体body为byte数组时不进行处理,如果是String则转成字节数组,如果是Java对象,则使用JDK序列化将消息转成字节数组,转出来的结果较大,含class类名,类相应方法等信息。因此性能较差。此时就要考虑使用类似Jackson2JsonMessageConverter等序列化形式以此提高性能。
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import java.io.Serializable;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class Order implements Serializable {
private Integer id;
private String orderNo;
private Float price;
private String remark;
}
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class User implements Serializable{
private Integer id;
private String userName;
private Integer age;
}
import com.alibaba.fastjson.JSON;
import com.itan.entity.Order;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.amqp.utils.SerializationUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.UUID;
@Slf4j
@RestController
@RequestMapping("/order")
public class OrderMessageController {
@Autowired
private RabbitTemplate rabbitTemplate;
@GetMapping("/serializable/{message}")
public void sendMessage1(@PathVariable String message) {
log.info("发送消息到MQ - 入参 [{}]", JSON.toJSONString(message));
Order order = Order.builder().id(1)
.orderNo(UUID.randomUUID().toString())
.price(15000f).remark(message).build();
rabbitTemplate.convertAndSend("queue1", order);
log.info("发送消息到MQ - 完成");
}
@GetMapping("/byte/{message}")
public void sendMessage2(@PathVariable String message) {
log.info("发送消息到MQ - 入参 [{}]", JSON.toJSONString(message));
Order order = Order.builder().id(2)
.orderNo(UUID.randomUUID().toString())
.price(15000f).remark(message).build();
byte[] bytes = SerializationUtils.serialize(order);
rabbitTemplate.convertAndSend("queue2", bytes);
log.info("发送消息到MQ - 完成");
}
}
运行之后查看queue1中存放的消息类型为JAVA对象序列化
queue2中存放的消息类型为二进制字节数组
import com.alibaba.fastjson.JSON;
import com.itan.entity.Order;
import com.rabbitmq.client.Channel;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.amqp.utils.SerializationUtils;
import org.springframework.stereotype.Component;
@Slf4j
@Component
public class ReceiveMessage {
@RabbitListener(queues = {"queue1"})
public void receive1(Order order) {
log.info("接收到queue1队列的消息:[{}]", order);
}
@RabbitListener(queues = {"queue2"})
public void receive2(byte[] bytes) {
Order order = (Order) SerializationUtils.deserialize(bytes);
log.info("接收到queue2队列的消息:[{}]", order);
}
}
1、使用JSON序列化与反序列化,需要实现自定义消息类型转换器,Jackson2JsonMessageConverter支持消息内容JSON序列化与反序列化
2、注意:在接收消息的时候,被序列化对象应提供一个无参的构造函数,否则会抛出异常
。
import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
import org.springframework.amqp.support.converter.MessageConverter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class RabbitMQInitConfig {
@Bean
public MessageConverter messageConverter() {
return new Jackson2JsonMessageConverter();
}
}
@Slf4j
@RestController
@RequestMapping("/order")
public class OrderMessageController {
@Autowired
private RabbitTemplate rabbitTemplate;
@GetMapping("/json/{message}")
public void sendMessage3(@PathVariable String message) {
log.info("发送消息到MQ - 入参 [{}]", JSON.toJSONString(message));
Order order = Order.builder().id(2)
.orderNo(UUID.randomUUID().toString())
.price(15000f).remark(message).build();
rabbitTemplate.convertAndSend("queue3", order);
log.info("发送消息到MQ - 完成");
}
}
实现自定义消息类型转换器后,queue2中存放的消息类型为JSON
import com.alibaba.fastjson.JSON;
import com.itan.entity.Order;
import com.rabbitmq.client.Channel;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.amqp.utils.SerializationUtils;
import org.springframework.stereotype.Component;
@Slf4j
@Component
public class ReceiveMessage {
@RabbitListener(queues = "queue3")
public void receive3(Order order) {
log.info("接收到queue3队列的消息:[{}]", order);
}
}
5、@RabbitListener与@RabbitHandler搭配使用
1、位置及作用
-
@RabbitListener:
类上、方法上,当监听到队列中有消息时则会进行接收并处理。
-
@RabbitHandler:
方法上,需要与@RabbitListener
搭配使用
-
@RabbitListener标注在类上面表示当有收到消息的时候,就交给@RabbitHandler的方法处理,具体使用哪个方法处理,根据MessageConverter转换后的参数类型
@Slf4j
@RestController
@RequestMapping("/order")
public class OrderMessageController {
@Autowired
private RabbitTemplate rabbitTemplate;
@GetMapping("/message")
public void sendMessage4() {
for (int i = 1; i < 5; i++) {
if (i % 2 == 0) {
Order order = Order.builder().id(i)
.orderNo(UUID.randomUUID().toString())
.price(15000f).remark("创建订单").build();
rabbitTemplate.convertAndSend("queue4", order);
} else {
User user = User.builder().id(i).userName(UUID.randomUUID().toString())
.age(20 + i).build();
rabbitTemplate.convertAndSend("queue4", user);
}
}
}
}
import com.itan.entity.Order;
import com.itan.entity.User;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
@Slf4j
@Component
@RabbitListener(queues = {"queue4"})
public class ReceiveMessage1 {
@RabbitHandler
public void receive(Order order) {
log.info("接收到queue4中Order消息:[{}]", order);
}
@RabbitHandler
public void receive(User user) {
log.info("接收到queue4中User消息:[{}]", user);
}
}
启动项目,访问接口,控制台输出如下:
二、SpringBoot中使用消息确认机制
1、概述
1、生产者将消息发送到RabbitMQ服务器,如果消息成功抵达,触发confirmCallback回调
2、交换器将消息投递到队列时候,如果失败了,则触发returnCallback回调
3、消费者消费消息时候手动确认消息
2、消息抵达MQ服务回调confirmCallBack
1、开启生产者确认模式spring.rabbitmq.publisher-confirms=true(默认是false)
,但是很有可能这种配置过时了,使用如下方式spring.rabbitmq.publisher-confirm-type=correlated
-
NONE:禁用发布确认模式(默认)
。
-
CORRELATED:消息成功抵达交换器后会触发回调方法
。
-
SIMPLE:
有两种效果,一是和CORRELATED值一样会触发回调方法
,二是在消息发布成功后使用RabbitTemplate调用waitForConfirms或waitForConfirmsOrDie方法等待Broker节点返回发送结果,根据返回结果来判定下一步的逻辑,要注意的点是waitForConfirmsOrDie方法如果返回false则会关闭Channel,则接下来无法发送消息到Broker。
2、消息只要被Broker接收到就会执行ConfirmCallBack,如果是集群模式,需要所有的Broker接收到才会调用触发ConfirmCallBack回调。
server:
port: 9000
spring:
rabbitmq:
publisher-confirm-type: correlated
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import javax.annotation.PostConstruct;
@Slf4j
@Configuration
public class RabbitTemplateConfig {
@Autowired
private RabbitTemplate rabbitTemplate;
@PostConstruct
public void RabbitTemplateInit() {
rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() {
@Override
public void confirm(CorrelationData correlationData, boolean ack, String cause) {
log.info("消息成功到达MQ");
log.info("当前消息的数据:{}", correlationData);
log.info("消息是否成功抵达:{}", ack);
log.info("失败原因:{}", cause);
}
});
}
}
1、启动服务,访问接口http://localhost:9000/order/message,查看控制台日志,ACK全部为true,表示成功消息抵达
3、消息正确抵达队列回调returnCallback
1、消息被Broker接收到只能表示消息已经到达服务器,并不能保证消息一定会被投递到目标队列中,所以需要returnCallback
,如果消息没有到达目标队列中,那么消息会被直接丢弃,生产者是不知道消息被丢弃的。因此在消息没有路由到目标队列时将回调returnCallback,可以记录下详细的投递数据,定期的巡检这些数据。
2、开启生产者消息抵达队列的确认:
-
spring.rabbitmq.publisher-returns=true
-
spring.rabbitmq.template.mandatory=true
server:
port: 9000
spring:
rabbitmq:
publisher-confirm-type: correlated
publisher-returns: true
template:
mandatory: true
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import javax.annotation.PostConstruct;
@Slf4j
@Configuration
public class RabbitTemplateConfig {
@Autowired
private RabbitTemplate rabbitTemplate;
@PostConstruct
public void RabbitTemplateInit() {
rabbitTemplate.setReturnCallback(new RabbitTemplate.ReturnCallback() {
@Override
public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
log.info("消息抵达队列失败");
log.info("当前消息的内容:{}", message);
log.info("回复的状态码:{}", replyCode);
log.info("回复的文本内容:{}", replyText);
log.info("交换机:{}", exchange);
log.info("路由键:{}", routingKey);
}
});
}
}
@GetMapping("/returnCallback")
public void sendMessage5() {
for (int i = 1; i < 5; i++) {
if (i % 2 == 0) {
Order order = Order.builder().id(i)
.orderNo(UUID.randomUUID().toString())
.price(15000f).remark("创建订单").build();
CorrelationData correlationData = new CorrelationData(UUID.randomUUID().toString());
rabbitTemplate.convertAndSend("direct_exchange","a", order, correlationData);
} else {
User user = User.builder().id(i).userName(UUID.randomUUID().toString())
.age(20 + i).build();
CorrelationData correlationData = new CorrelationData(UUID.randomUUID().toString());
rabbitTemplate.convertAndSend("direct_exchange","c", user, correlationData);
}
}
}
1、启动服务,访问接口http://localhost:9000/order/returnCallback,查看控制台日志
4、消费者确认
1、默认是自动确认的,只要有一个消息被成功处理,就会自动确认所有消息,如果MQ宕机了,就会发生消息丢失,因此需要手动确认
2、开启消费者手动ACK:spring.rabbitmq.listener.simple.acknowledge-mode=manual
server:
port: 9000
spring:
rabbitmq:
publisher-confirm-type: correlated
publisher-returns: true
template:
mandatory: true
listener:
simple:
acknowledge-mode: manual
import com.itan.entity.Order;
import com.rabbitmq.client.Channel;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.core.MessageProperties;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
import java.io.IOException;
@Slf4j
@Component
@RabbitListener(queues = {"queue2","queue3","queue4"})
public class ReceiveMessage1 {
@RabbitHandler
public void receive(Message message, Channel channel, Order order) {
log.info("接收到queue2中User消息:[{}]", order);
MessageProperties properties = message.getMessageProperties();
long deliveryTag = properties.getDeliveryTag();
try {
channel.basicAck(deliveryTag, false);
} catch (IOException e) {
log.info("网络中断...");
e.printStackTrace();
}
}
}
1、以debug方式启动服务,receive方法中打上断点,进行调试,访问接口http://localhost:9000/order/returnCallback
三、常见问题的解决方案
1、幂等性
1、用于对于同一操作发起的一次请求或者多次请求的结果是一致的,不会因为多次操作而产生了副作用,称为幂等。当出现消费者对某条消息重复消费的情况时,重复消费的结果与消费一次的结果是相同的,并且多次消费并未对业务系统产生任何负面影响。
2、常用幂等性保证方法
1、利用数据库的唯一约束实现幂等
-
比如将订单表中的订单编号设置为唯一索引,创建订单时,根据订单编号就可以保证幂等。
2、防重表
-
本质也是根据数据库的唯一性约束来实现。其实现大体思路是:首先在防重表上建唯一索引,其次操作时把业务表和防重表放在同个本地事务中,如果出现重复消费,数据库会抛唯一约束异常,操作就会回滚。
3、利用Redis的原子性
-
每次操作都直接SET到Redis里面,然后将Redis数据定时同步到数据库中。
4、多版本(乐观锁)控制
-
此方案多用于更新的场景下。其实现的大体思路是:给业务数据增加一个版本号属性,每次更新数据前,比较当前数据的版本号是否和消息中的版本一致,如果不一致则拒绝更新数据,更新数据的同时将版本号+1。
5、token机制
-
生产者发送每条数据的时候,增加一个全局唯一的Id,这个Id通常是业务的唯一标识
,比如订单编号。在消费端消费时,则验证该Id是否被消费过,如果还没消费过,则进行业务处理。处理结束后,在把该Id存入Redis,同时设置状态为已消费。如果已经消费过了,则不进行处理。
3、消息丢失
1、消息发送出去,由于网络问题没有抵达服务器,消息丢失了:
-
发送消息时可能会网络失败,做好容错(try-catch),失败后要有重试机制,可以将消息记录到数据库,可以定期扫描重发。
-
做好日志记录,每个消息状态是否都被服务器收到都应该记录。
-
做好定期重发,如果消息发送失败,定期去数据库查询未成功的消息并进行重发。
2、消息抵达Broker,Broker要将消息写入磁盘(持久化)才算成功,若还未持久化完成就出现宕机了,消息丢失了:
-
加入确认回调机制,确认成功的消息,修改数据库状态。
3、消息自动ACK,消费者收到消息,但还没有来得及处理消息就宕机了,消息丢失了:
-
开启手动ACK,消费成功才将队列中的消息移除,失败或者没有来得及处理就noAck并重新入队。
4、消息重复
1、消息成功消费,事务已经提交,ACK时出现宕机,导致ACK失败,Broker的消息重新由unack变为ready,并发送给其他消费者。
-
消费者的业务消费接口应该设计为幂等性
的。
-
使用防重表(redis/mysql),发送每一条消息都应该有业务的唯一标识,处理过后就不处理了。
-
RabbitMQ的每一条消息都有redelivered属性,可以获取是否是被重新投递过来的,而不是第一次投递过来的。
2、消息消费失败,由于重试机制,自动又将消息发送出去。
5、消息积压
1、消费者宕机导致积压,最终会导致性能下降
2、消费者消费能力不足导致积压
3、发送者发送消息流量太大
4、解决方法:
-
上线更多的消费者进行消费。
-
上线专门的队列消费服务,将消息先批量取出来,记录到数据库,离线慢慢处理。