SpringBoot2.x整合RabbitMQ
依赖
<!-- rabiitmq-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
yml配置
spring:
rabbitmq:
host: 192.168.142.210
username: guest
password: guest
port: 5672
virtual-host: /
#消息发送确认回调
publisher-confirms: true
#发送返回监听回调
publisher-returns: true
#消费者确认--手动ack模式
listener:
simple:
acknowledge-mode: manual
#设置为 true 后 消费者在消息没有被路由到合适队列情况下会被return监听,而不会自动删除
template:
mandatory: true
配置类
设置了消息消费失败重试次数、失败消息的处理方式以及成功或失败的回调方法
@Configuration
@Slf4j
public class RabbitMQConfig {
/**
* 配置消息监听对象
*
* @param connectionFactory
* @return
*/
@Bean
public RabbitListenerContainerFactory<?> rabbitListenerContainerFactory(ConnectionFactory connectionFactory) {
log.info("自动配置RabbitListenerContainerFactory");
SimpleRabbitListenerContainerFactory simpleRabbitListenerContainerFactory = new SimpleRabbitListenerContainerFactory();
simpleRabbitListenerContainerFactory.setConnectionFactory(connectionFactory);
// 消费者数量
simpleRabbitListenerContainerFactory.setConcurrentConsumers(5);
// 最大消费者数量
simpleRabbitListenerContainerFactory.setMaxConcurrentConsumers(10);
// 手动ack,当消息n次重试消费端依旧处理失败时,可重新回归队列
// 重启项目后可再次消费,但是如果未及时处理会导致消息队列中数据越来越多
// simpleRabbitListenerContainerFactory.setAcknowledgeMode(AcknowledgeMode.MANUAL);
// 自动ack,若是n次重试仍旧消费失败后,消息会丢弃,
// 虽然会产生数据丢失,但是可以防止消息队列中数据冗余
simpleRabbitListenerContainerFactory.setAcknowledgeMode(AcknowledgeMode.AUTO);
// 消息转换
simpleRabbitListenerContainerFactory.setMessageConverter(new Jackson2JsonMessageConverter());
// 消费者自动启动
simpleRabbitListenerContainerFactory.setAutoStartup(true);
// 消费者每次从队列获取的消息数量
simpleRabbitListenerContainerFactory.setPrefetchCount(1);
// 重试机制
// 这种方式不生效
// simpleRabbitListenerContainerFactory.setRetryTemplate(retryTemplate());
simpleRabbitListenerContainerFactory.setAdviceChain(
RetryInterceptorBuilder
.stateless()
.recoverer(new RejectAndDontRequeueRecoverer())
.retryOperations(retryTemplate())
.build());
// true时消费者消费失败,自动重新入队;
// 重试次数超过上面的设置之后是否丢弃(false不丢弃时需要写相应代码将该消息加入死信队列)
simpleRabbitListenerContainerFactory.setDefaultRequeueRejected(true);
return simpleRabbitListenerContainerFactory;
}
/**
* 构造RabbitTemplate对象
*
* @param connectionFactory
* @return
*/
@Bean
public RabbitTemplate rabbitTemplate(ConnectionFactory connectionFactory) {
RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory);
rabbitTemplate.setMessageConverter(new Jackson2JsonMessageConverter());
// 必须设置为 true,不然当发送到交换器成功,但是没有匹配的队列,不会触发 ReturnCallback 回调
// 而且 ReturnCallback 比 ConfirmCallback 先回调,意思就是 ReturnCallback 执行完了才会执行 ConfirmCallback
rabbitTemplate.setMandatory(true);
//指定 ConfirmCallback 回调
rabbitTemplate.setConfirmCallback((correlationData, ack, cause) -> {
// 如果发送到交换器都没有成功(比如说删除了交换器),ack 返回值为 false
// 如果发送到交换器成功,但是没有匹配的队列(比如说取消了绑定),ack 返回值为还是 true (这是一个坑,需要注意)
if (ack) {
if (null != correlationData) {
log.info("confirm回调方法>>>消息发送到交换机成功!回调消息ID为:{}", correlationData.getId());
} else {
log.info("confirm回调方法>>>消息发送到交换机成功!");
}
} else {
log.info("confirm回调方法>>>消息发送到交换机失败!,原因 : [{}]", cause);
}
});
// 设置 ReturnCallback 回调 yml需要配置 publisher-returns: true
// 如果发送到交换器成功,但是没有匹配的队列,就会触发这个回调
rabbitTemplate.setReturnCallback((message, replyCode, replyText,
exchange, routingKey) -> {
log.info("returnedMessage回调方法,未匹配对列>>>" + new String(message.getBody(), StandardCharsets.UTF_8) + ",replyCode:" + replyCode
+ ",replyText:" + replyText + ",exchange:" + exchange + ",routingKey:" + routingKey);
});
return rabbitTemplate;
}
@Bean
public RetryTemplate retryTemplate() {
RetryTemplate retryTemplate = new RetryTemplate();
retryTemplate.setBackOffPolicy(backOffPolicy());
retryTemplate.setRetryPolicy(retryPolicy());
// 设置消费消息过程监听(不是必须)
retryTemplate.registerListener(new RetryListener() {
@Override
public <T, E extends Throwable> boolean open(RetryContext retryContext, RetryCallback<T, E> retryCallback) {
// 执行之前调用 (返回false时会终止执行)
log.info("***********open: 开始 count: {}", retryContext.getRetryCount());
return true;
}
@Override
public <T, E extends Throwable> void close(RetryContext retryContext, RetryCallback<T, E> retryCallback, Throwable throwable) {
// 重试结束或消费成功的时候调用
log.info("***********close: 结束: count: {} ", retryContext.getRetryCount());
}
@Override
public <T, E extends Throwable> void onError(RetryContext retryContext, RetryCallback<T, E> retryCallback, Throwable throwable) {
// 异常 都会调用
log.error("***********尝试第{}次重新调用", retryContext.getRetryCount());
}
});
return retryTemplate;
}
/**
* 消费时便重试时间间隔
*
* @return
*/
@Bean
public ExponentialBackOffPolicy backOffPolicy() {
ExponentialBackOffPolicy backOffPolicy = new ExponentialBackOffPolicy();
backOffPolicy.setInitialInterval(3000);
backOffPolicy.setMaxInterval(10000);
return backOffPolicy;
}
/**
* 消费失败重试次数
*
* @return
*/
@Bean
public SimpleRetryPolicy retryPolicy() {
SimpleRetryPolicy retryPolicy = new SimpleRetryPolicy();
// 消息最大发送次数,包括第一次和重试次数
retryPolicy.setMaxAttempts(3);
return retryPolicy;
}
}
创建队列绑定交换机
这里交换机和队列要设置持久化为true,防止宕机后消息丢失
public final static String USER_LOG = "user.log";
public final static String EXCHANGE = "exchange";
@Bean
public Queue userLogQueue() {
/*
durable:队列是否可持久化,默认为true。
exclusive: 队列是否具有排它性,默认为false。
autoDelete:队列没有任何订阅的消费者时是否自动删除,默认为false。
*/
return new Queue(USER_LOG, true, false, false);
}
@Bean(EXCHANGE)
public TopicExchange topicExchange() {
/*
交换机持久化且不自动删除
*/
return new TopicExchange(EXCHANGE,true,false);
}
@Bean
public Binding bindingUserLog(Queue userLogQueue, TopicExchange topicExchange) {
return BindingBuilder.bind(userLogQueue).to(topicExchange).with(USER_LOG);
}
消息发送抽象类
@Slf4j
public abstract class BaseSender {
@Resource
private RabbitTemplate rabbitTemplate;
/**
* 子类重写方法(业务方法调用此方法时可不指定交换机及路由键,由子类方法指定)
*
* @param object
*/
public abstract void sendMessage(Object object);
/**
* 通用消息发送方法
*
* @param object 发送的对象
* @param exchangeName 交换机名称
* @param bindingKey 绑定路由key
*/
void push(Object object, String exchangeName, String bindingKey) {
CorrelationData correlationData = new CorrelationData(UUID.randomUUID().toString());
log.info("【Producer】发送的消息ID = {},消息:{}", correlationData.getId(), new Gson().toJson(object));
try {
rabbitTemplate.convertAndSend(exchangeName, bindingKey, object, correlationData);
} catch (Exception e) {
log.error("发送消息失败,原因:{}", e.getMessage());
}
}
}
消息接收抽象类
@Slf4j
public abstract class BaseReceiver {
/**
* 子类重写方法,实现各自业务逻辑
*
* @param channel 信道
* @param message 消息
*/
protected abstract void receiveMessage(Channel channel, Message message);
/**
* 获取具体对象
*
* @param message 消息
* @param clazz 需要转换的类
* @param <T>
* @return
*/
<T> T getObjectByClass(Message message, Class<T> clazz) {
String body = new String(message.getBody(), StandardCharsets.UTF_8);
log.info("【MQ: Consumer成功接收到消息,Class: {} 】>>> {}", clazz.getName(), body);
return JSONObject.parseObject(body, clazz);
}
/**
* 消息接收确认
*
* @param channel 信道
* @param message 消息
*/
void basicAck(Channel channel, Message message) {
try {
// 确认接收到消息
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 异常手动处理方法,这是最开始自定义的三次重试并且手动ack的方案
* 但是后面spring自己有提供的重试机制
*
* @param channel 信道
* @param message 消息
* @param e 异常
*/
void handelException(Channel channel, Message message, Exception e) {
//是否发生过异常,发生过异常拒绝接受消息
if (message.getMessageProperties().getRedelivered()) {
try {
Map<String, Object> headers = message.getMessageProperties().getHeaders();
Integer retryCount = (Integer) headers.get("retryCount");
if (retryCount == null || retryCount <= 3) {
headers.put("retryCount", retryCount == null ? 1 : ++retryCount);
log.info("【Consumer】消息已经回滚过,重试次数: {} 接收消息 : {}", retryCount, new String(message.getBody()));
channel.basicReject(message.getMessageProperties().getDeliveryTag(), true);
} else {
// 拒绝消息,并且不再重新进入队列
log.info("【Consumer】消息已经回滚过,拒绝接收消息 : {}", new String(message.getBody()));
channel.basicReject(message.getMessageProperties().getDeliveryTag(), false);
}
} catch (Exception e1) {
e1.printStackTrace();
}
} else {
log.info("【Consumer】消息即将返回队列重新处理 :{}", new String(message.getBody()));
//设置消息重新回到队列处理
// requeue表示是否重新回到队列,true重新入队
try {
channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, true);
} catch (Exception e2) {
e2.printStackTrace();
}
}
e.printStackTrace();
}
}
实际应用
- 生产者
// 用户日志生产者,业务代码中引入sender,直接调用sendMessage方法即可
@Component
public class UserLogSender extends BaseSender {
@Override
public void sendMessage(Object object) {
push(object, RabbitMQConfig.EXCHANGE, RabbitMQConfig.USER_LOG);
}
}
- 消费者
当消费者消费时,如果在ack之前抛出异常,则会根据你设定的重试次数,重新发送消息给消费者。如果n次重试后仍旧失败,会根据你之前设定的ack方式,决定这条消息是丢弃还是重新放入队列(如果队列没有被删除,则重启项目后再次被消费)
@Component
public class UserLogReceiver extends BaseReceiver {
@Resource
private IUserLogMongoService userLogMongoService;
@Override
@RabbitListener(queues = {RabbitMQConfig.USER_LOG})
protected void receiveMessage(Channel channel, Message message) {
MongoUserLog userLog = getObjectByClass(message, MongoUserLog.class);
// 业务代码...
// 若是配置的是手动ack
// simpleRabbitListenerContainerFactory.setAcknowledgeMode(AcknowledgeMode.MANUAL);
// 则成功后需要调用下面的方法手动确认
// basicAck(channel, message);
}
}
- 消费成功,可看到生产者这边发送成功,消费者也拿到了消息
- 消费失败
-
在
MANUAL
模式下调用失败,三次重试后回到队列.
-
在
AUTO
模式下调用失败,三次重试后丢弃。可以看见total当时的一个峰值,最终归为0
-