前言
本文旨在提供基于SpringBoot2.x与RabbitMQ的快速集成。引入MQ的优缺点,提供了消息不丢失的解决方案。
此处只提供思路,具体的代码需要用户自行实现,比如生产端失败消息的再次投递:可以将失败的消息信息写入数据库,使用定时任务扫描数据库表,将消息重新投递到MQ当中。重复的消息:在消费端可以利用数据表的唯一键约束来去掉重复消息,或者利用redis的天生幂等性去重。
引入MQ的优缺点
MQ的优点是解耦,异步,削峰:
1.解耦:解除了不同模块之间的耦合,数据生产者只需要将消息推送到MQ即可发送消息,而数据消费者也只需根据需要监听MQ即可获取消息,生产者与消费者没有直接连接在一起。
2.异步:生产者将消息推送到MQ之后即可返回,而无需等待消费者执行结束,大大提高了系统响应速度。
3.削峰:假设某个微服务每秒钟能消费2000条消息,而在系统高峰期,每秒产生了5000条消息,多余的3000条将会在MQ中积压下来,等待高峰期过去,慢慢消费积压下来的消息。
MQ的缺点是增加了系统复杂度:
1.需要考虑确保消息 生产者->MQ(broker)->消费者 之间传递过程中消息100%不丢失。
2.在确保消息不丢失的同时,不可避免的带来了消息重复投递的问题,需要确保消息不重复消费。
3.确保系统的高可用
消息100%不丢失需要确保生产者、MQ、消费者三方均不丢失
a. 生产者不丢失消息需要开启confirm机制(此处不考虑事务机制,因为事务机制会造成吞吐量太低)。生产者为每一条消息设置唯一id,当消息成功投递到MQ交换机,交换机成功投递到队列后才会认为该条消息投递成功。
b. MQ需要开启消息数据持久化和元数据持久化,MQ的集群化部署。
c. 消费者需要开启手动ack机制(默认情况下会自动ack,自动删除消息),当消息被成功投递给消费者并且被成功消费后才可以删除该条消息。
代码实现
1.引入pom.xml依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
2.application.yml
spring:
rabbitmq:
port: 5672
host: 127.0.0.1
publisher-confirms: true #生产者将消息投递到mq的确认
publisher-returns: true #交换机将消息成功投递到队列的确认
template:
mandatory: true #优先回调CallReturn()而不是CallConfirm()
listener:
simple:
acknowledge-mode: manual #开启手动ack
3.入口启动类配置 -----> @EnableRabbit
@EnableRabbit
@SpringBootApplication
public class RabbitMqApplication
{
public static void main(String[] args)
{
SpringApplication.run(RabbitMqApplication.class);
}
}
4.MQ配置类
创建交换机、队列、将交换机绑定到队列、将消息设置为json格式,设置消息投递的回调方法
@Slf4j
@Configuration
public class RabbitMqConfiguration
{
@Resource
AmqpAdmin amqpAdmin;
@Autowired
RabbitTemplate rabbitTemplate;
//初始化设置为消息为json格式,如不设置,默认为byte[]格式并要求对象类消息实现Serializable接口
@Bean
public Jackson2JsonMessageConverter initMessageConverter(){
return new Jackson2JsonMessageConverter();
}
@PostConstruct
public void initRabbitTemplate(){
/**
* 只要消息抵达Broker就会调用,ack=true
* @param correlationData 当前消息的唯一id
* @param ack 消息是否成功收到
* @param cause 失败的原因
*/
rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback()
{
@Override
public void confirm(CorrelationData correlationData, boolean ack, String cause)
{
log.info("confirm...correlationData:{}===>ack:{}===>cause:{}",correlationData,ack,cause);
}
});
/**
* 消息抵达队列,但消息没有成功投递给指定的队列就调用
* @param message 投递失败的消息详细信息
* @param replyCode 回复状态码
* @param replyText 失败原因
* @param exchange 这条消息所在的交换机
* @param routingKey 这条消息的路由键
*/
rabbitTemplate.setReturnCallback(new RabbitTemplate.ReturnCallback()
{
@Override
public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey)
{
log.info("Fail message:{}==>replayCode:{}==>replyText:{}==>exchange:{}==>routingKey:{}",message,replyCode,replyText,exchange,routingKey);
}
});
}
@PostConstruct
public void createExchange()
{
DirectExchange directExchange = new DirectExchange("hello-exchange", true, false, null);
amqpAdmin.declareExchange(directExchange);
}
@PostConstruct
public void createQueue()
{
Queue queue = new Queue("hello-queue", true, false, false, null);
amqpAdmin.declareQueue(queue);
}
@PostConstruct
public void createBinding(){
Binding binding = new Binding("hello-queue",Binding.DestinationType.QUEUE,"hello-exchange","hello.java",null);
amqpAdmin.declareBinding(binding);
}
}
5.生产者代码 UUID生成消息的唯一id
@Service
public class ProducerService
{
@Autowired
private RabbitTemplate rabbitTemplate;
public void sendMsg()
{
UserDto userDto = new UserDto();
userDto.setId(1L);
userDto.setUserName("xiaoming");
rabbitTemplate.convertAndSend("hello-exchange","hello.java",userDto,new CorrelationData(UUID.randomUUID().toString()));
TeacherDto teacherDto = new TeacherDto();
teacherDto.setId(1L);
teacherDto.setName("xiaohong");
rabbitTemplate.convertAndSend("hello-exchange","hello.java",teacherDto,new CorrelationData(UUID.randomUUID().toString()));
}
}
6.消费者代码 @RabbitListener可以标注类或方法。@RabbitHanler只可以标注方法,结合@RabbitListener做监听方法重载
@Component
@RabbitListener(queues = {"hello-queue"})
public class ConsumerService
{
@RabbitHandler
public void getUserMsg(Message msg, UserDto userDto, Channel channel)
{
MessageProperties messageProperties = msg.getMessageProperties();
long deliveryTag = messageProperties.getDeliveryTag();
log.info("User Info ==> {}",userDto);
try
{
if(1==1){
channel.basicAck(deliveryTag,false);
log.info("成功签收消息");
}else{
channel.basicNack(deliveryTag,false,true);
log.info("签收消息失败,消息重新进入队列");
}
}
catch (IOException e)
{
log.error(e.getMessage());
}
}
@RabbitHandler
public void getTeacherMsg(Message msg, TeacherDto teacherDto,Channel channel){
MessageProperties messageProperties = msg.getMessageProperties();
long deliveryTag = messageProperties.getDeliveryTag();
log.info("Teacher Info ==> {}",teacherDto);
try
{
if(1 != 1){
channel.basicAck(deliveryTag,false);
log.info("成功签收消息");
}else{
channel.basicNack(deliveryTag,false,true);
log.info("签收消息失败,消息重新进入队列");
}
}
catch (IOException e)
{
log.error(e.getMessage());
}
log.info("Teacher info: ",teacherDto);
}
}
- 例子中用到的Dto类
@Data
public class UserDto implements Serializable
{
private Long id;
private String userName;
}
@Data
public class TeacherDto
{
private Long id;
private String name;
private int age;
}