SpringBoot整合RabbitMQ 企业级实战示例-附源码
架构
SpringBoot 整合 RabbitMQ
1. 生产者、消费者在不同服务中 (推荐)
-
解释
业务服务 创建MQ生产者并丢消息到MQ队列, 消费者监听MQ队列消费, 消费者 http调用业务服务接口, 业务服务接口 处理业务逻辑。
@startuml title 生产者、消费者在不同服务中 业务服务 -> 业务服务: 创建MQ生产者 业务服务 -> MQ队列: 生产消息 消费者服务 -> MQ队列: 消费者监听MQ队列消费 消费者服务 -> 业务服务: 消费者http调用业务处理接口 业务服务 -> 业务服务: 处理业务逻辑
@enduml
2. 生产者、消费者在同一个服务中
-
解释:
业务服务 http 调用生产者, 生产者丢消息到MQ队列, 消费者监听MQ队列消费, 消费者 http调用业务服务接口, 业务服务接口 处理业务逻辑。
@startuml title 生产者、消费者在同一个服务中 业务服务 -> MQ服务: http 调用生产者 MQ服务 -> MQ队列: 生产者丢消息 MQ服务 -> MQ队列: 消费者监听MQ队列消费 MQ服务 -> 业务服务: 消费者 http调用业务处理接口 业务服务 -> 业务服务: 处理业务逻辑
@enduml
定义延迟队列
@Configuration
public class AmqpConfig {
/**
* rabbitMq里初始化exchange.
*
* @return
*/
@Bean()
public DirectExchange umsExchange() {
return new DirectExchange(RabbitMQConstant.UMS_EXCHANGE);
}
//---------------------声明死信队列------------------------
/**
* 声明死信 Exchange
*
* @return
*/
@Bean
public DirectExchange deadLetterExchangeUMS() {
return new DirectExchange(RabbitMQConstant.DEAD_UMS_EXCHNAGE);
}
/**
* 正常 队列
*
* @return
*/
@Bean
public Queue sendMailNewEmployeeQueue() {
Map<String, Object> map = new HashMap<String, Object>(16);
//消息的 存活时间:毫秒 : 5分钟; 存活时间到期后,消息自动变为死信;死信将送往下方指定的交换机、路由key
map.put("x-message-ttl", 5 * 60 * 1000);
//指定死信送往的交换机: 根据这个交换机找延迟队列
map.put("x-dead-letter-exchange", RabbitMQConstant.DEAD_UMS_EXCHNAGE);
//指定死信的 routingKey: 根据这个路由找延迟队列
map.put("x-dead-letter-routing-key", RabbitMQConstant.DLX_SEND_MAIL_NEW_EMPLOYEE_ROUTING_KEY);
return new Queue(RabbitMQConstant.SEND_MAIL_NEW_EMPLOYEE_QUEUE, true, false, false, map);
}
/**
* 绑定队列交换机,设置路由
*
* @return
*/
@Bean
public Binding sendMailNewEmployeeBinding() {
return BindingBuilder.bind(sendMailNewEmployeeQueue()).to(umsExchange()).with(RabbitMQConstant.SEND_MAIL_NEW_EMPLOYEE_ROUTING_KEY);
}
/**
* 延迟队列
*
* @return
*/
@Bean
public Queue delaySyncWeaverContractUserInfoQueue() {
return new Queue(RabbitMQConstant.DLX_SEND_MAIL_NEW_EMPLOYEE_QUEUE, true, false, false);
}
/**
* 设置路由
*
* @return
*/
@Bean
public Binding delaySyncWeaverContractUserInfoBinding() {
return BindingBuilder.bind(delaySyncWeaverContractUserInfoQueue()).to(deadLetterExchangeUMS()).with(RabbitMQConstant.DLX_SEND_MAIL_NEW_EMPLOYEE_ROUTING_KEY);
}
}
消费者最佳实践
1. MQ消费者 抽象类
/**
* MQ 业务类
*/
@Slf4j
public abstract class MqService {
/**
* 原型注入MQ
*/
@Autowired
public RabbitTemplate rabbitTemplate;
/**
* 延时时间队列
*/
public Integer[] delayTime = {10000, 60000, 600000};
/**
* 间隔时间(ms)
*/
private final long interval = 60000L;
/**
* 间隔系数
*/
private final int multiply = 5;
/**
* 最大尝试次数
*/
private final int maxAttempts = 7;
/**
* 设置信息
*
* @param rabbitTemplate
*/
@Autowired
public void setRabbitTemplate(RabbitTemplate rabbitTemplate) {
this.rabbitTemplate = rabbitTemplate;
rabbitTemplate.setConfirmCallback((CorrelationData correlationData, boolean flag, String info) -> {
log.info("<<<<<<<<发送到消息队列回调:{}", JSON.toJSONString(correlationData));
if (flag) {
log.info(">成功发到队列< : flag:" + flag);
} else {
log.error("发到MQ失败" + info);
}
});
}
/**
* 监听的业务
*
* @param json
* @throws Exception
*/
public abstract void listenerService(String json) throws Exception;
/**
* 重试机制1-高阶用法
* routeKey的组成:waybillcode.delay.100
* waybillcode.延迟标识.延迟时间标识
*
* 准备多个延迟队列,不同的TTL,与delayTime数组中的延迟时间对应
* 交换机可以根据路由key,自动路由到对应的延迟队列
*
* @param message
*/
public void retry(Message message) {
String receivedRoutingKey = message.getMessageProperties().getReceivedRoutingKey();
// 是否延迟
if (receivedRoutingKey.contains(RabbitMQConstant.DELAY)) {
//非第一次进延时队列:(waybillcode.delay.100)的形式
String[] split = receivedRoutingKey.split(RabbitMQConstant.DELAY);
Integer delaySeconds = Integer.parseInt(split[1]);
int index = Arrays.binarySearch(delayTime, delaySeconds);
// 将错误信息推送
if (index + 1 < delayTime.length) {
index++;
String str = split[0] + RabbitMQConstant.DELAY + delayTime[index];
this.sendToMq(new String(message.getBody(), StandardCharsets.UTF_8), RabbitMQConstant.UMS_EXCHNAGE_DELAY, str);
}
} else {
//第一次进延时队列
this.sendToMq(new String(message.getBody(), StandardCharsets.UTF_8), RabbitMQConstant.UMS_EXCHNAGE_DELAY, receivedRoutingKey + RabbitMQConstant.DELAY + delayTime[0]);
}
// 记录信息
log.error("consumer.retry-重试机制,进入延迟队列:routingKey:{}, body:{}" +
message.getMessageProperties().getReceivedRoutingKey(),
new String(message.getBody(), StandardCharsets.UTF_8)
);
}
/**
* 重试机制2-基本用法
*
* @param message 消息对象
* @param channel 确认通道
* @param exchange 交换机名
* @param routeKey 指令
* @throws Exception 抛出异常
*/
void sendToRetry(Message message, Channel channel, String exchange, String routeKey) throws Exception {
MessageProperties messageProperties = message.getMessageProperties();
Map<String, Object> headers = messageProperties.getHeaders();
Integer retryTimes = (Integer) headers.get("retryTimes");
if (retryTimes == null) {
retryTimes = 0;
} else {
retryTimes ++;
}
if (retryTimes < maxAttempts) {
// 计算延迟时间
long delayTime = interval;
if (retryTimes != 0) {
delayTime = retryTimes * multiply * interval;
}
//小于重试次数 丢进回收队列 等待回收
//重试次数
messageProperties.setHeader("retryTimes", retryTimes);
//延迟时间
messageProperties.setExpiration(String.valueOf(delayTime));
//重新生成msg
Message offMessage = new Message(message.getBody(), messageProperties);
//发送到死信队列
rabbitTemplate.send(exchange, routeKey, offMessage);
//确认消费
try {
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
} catch (IOException e1) {
log.info("已经发送到死信队列,但没有在主队列中ack,ID:{}", message.getMessageProperties().getDeliveryTag());
}
} else {
//重试次数太多 丢弃
log.error("ID:{}重试次数太多,已经被丢弃,内容为:{}", message.getMessageProperties().getDeliveryTag(), message.getBody());
channel.basicReject(message.getMessageProperties().getDeliveryTag(), true);
}
}
/**
* 发送到mq
*
* @param json 数据信息
* @param exchange 交换机
* @param routeKey
*/
public void sendToMq(String json, String exchange, String routeKey) {
MessageProperties messageProperties = new MessageProperties();
Message message = new Message(json.getBytes(StandardCharsets.UTF_8), messageProperties);
rabbitTemplate.send(exchange, routeKey, message);
}
/**
* ack消费
*
* @param channel
* @param tag
*/
public void ack(Channel channel, long tag) {
log.info("ums.consumer.ack-info:\nchannel:{},tag:{}", channel, tag);
try {
channel.basicAck(tag, false);
} catch (IOException e) {
log.error("ums.consumer.ack-执行方法时error:\nchannel:{},tag:{}", channel, tag, e);
}
}
/**
* 拒绝消费,重新放入队列消费
* 参考: https://my.oschina.net/dengfuwei/blog/1595047
* 方法 channel.basicNack(*,*,requeue);
* requeue参数为true: 会重新将消息放入队列
* requeue参数为 false:丢弃消息,会将消息变为死信
*
* @param channel
* @param tag
*/
public void nAckToQueues(Channel channel, long tag) {
log.error("ums.consumer.nAck-error:\nchannel:{},tag:{}", channel, tag);
try {
channel.basicNack(tag, false, true);
} catch (IOException e) {
log.error("ums.consumer.basicNack-执行方法时error:\nchannel:{},tag:{};\n 【消息成为死信】", channel, tag, e);
this.nAckToDeadQueues(channel, tag);
}
}
/**
* 拒绝消费,消息成为死信,手动放入死信队列
* <p>
* " 死信 消息会被RabbitMQ进行特殊处理,如果配置了死信队列信息,那么该消息将会被丢进死信队列中,如果没有配置,则该消息将会被丢弃。"
* 死信:使用 channel.basicNack 或 channel.basicReject ,并且此时requeue 属性被设置为false
*
* @param channel
* @param tag
*/
public void nAckToDeadQueues(Channel channel, long tag) {
log.error("ums.consumer.nAckToDeadQueues-error:\nchannel:{},tag:{}", channel, tag);
try {
channel.basicNack(tag, false, false);
} catch (IOException e) {
log.error("ums.consumer.nAckToDeadQueues-执行方法时error:\nchannel:{},tag:{}", channel, tag, e);
}
}
}
2. 业务队列消费者实现类
// 业务队列的消费者
@Slf4j
@Service
public class PushAdMqConsumerServiceimpl extends MqService {
@Value("${constant.url}")
private String url;
@Value("${constant.method.pushad}")
private String pushAdMethod;
/**
* 监控消费者
*
* @param message 消息
* @param channel AMQP通道
* @param tag RabbitMQ 向该 Channel 投递的这条消息的唯一标识 ID
*/
@RabbitListener(queues = RabbitMQConstant.PUSH_AD_QUEUE)
public void getPushAdInfo(Message message, Channel channel, @Header(AmqpHeaders.DELIVERY_TAG) Long tag){
this.deal(message, channel, tag);
}
/**
* 处理消息
*
* @param message
* @param channel
* @param tag
*/
public void deal(Message message, Channel channel, Long tag) {
String json = null;
//处理业务
try {
json = new String(message.getBody());
this.listenerService(json);
} catch (Exception e) {
// 记录日志信息
log.error("dealMessage-error===>routeKey:{}, data:{},error:",
message.getMessageProperties().getReceivedRoutingKey(), new String(message.getBody()), e);
//放入死信队列
super.nAckToDeadQueues(channel, tag);
return;
}
//手动确认
super.ack(channel, tag);
}
/**
* 消费者业务处理
*
* @param json
* @throws Exception
*/
@Override
public void listenerService(String json) throws Exception {
log.info("listenerService-开始消费");
JSONObject jsonObject = JSON.parseObject(json);
log.info("listenerService====>输入参数:{}", jsonObject.toJSONString());
String responseResult = HttpUtil.postForHashHeaderJson(url + pushAdMethod, jsonObject.toJSONString(), null);
log.info("listenerService====>输出参数:{}", responseResult);
JSONObject resultObject = JSONObject.parseObject(responseResult);
if (!resultObject.get(StringConstant.Custom.CODE).equals(StringConstant.Custom.CUSTOM_SUCCESS)){
String message = "接口返回的信息不是success。入参" + json + "\n出参:" + responseResult ;
throw new Http400Exception("接口返回的信息不是success",message);
}
}
}
3. 死信队列消费者实现类
// 死信队列的消费者
@Slf4j
@Service
public class DeadPushAdMqConsumerServiceimpl extends MqService {
@Resource
private AdConsumerErrorLogMapper adConsumerErrorLogMapper;
@Resource
private WeChatGroupMessage weChatGroupMessage;
@Value("${workwechat.chatid}")
private String chatId;
/**
* 监控消费者
*
* @param message 消息
* @param channel AMQP通道
* @param tag RabbitMQ 向该 Channel 投递的这条消息的唯一标识 ID
*/
@RabbitListener(queues = RabbitMQConstant.DEAD_PUSH_AD_QUEUE)
public void getDeadPushAdInfo(Message message, Channel channel, @Header(AmqpHeaders.DELIVERY_TAG) Long tag) {
this.deal(message, channel, tag);
}
/**
* 处理消息
*
* @param message
* @param channel
* @param tag
*/
public void deal(Message message, Channel channel, Long tag) {
String json = null;
//处理业务
try {
json = new String(message.getBody());
this.listenerService(json);
} catch (Exception e) {
// 记录日志信息
log.error("DeadPushAdMqConsumerServiceimpl-死信队列消费异常-error===>routeKey:{}, data:{},error:",
message.getMessageProperties().getReceivedRoutingKey(), new String(message.getBody()), e);
}
//手动确认
super.ack(channel, tag);
}
/**
* 消费者业务处理
*
* @param json
* @throws Exception
*/
@Override
public void listenerService(String json) throws Exception {
log.info("listenerService-开始消费");
/**
* 死信队列的消费者
* 储存到Mysql的表,记录下消费失败的消息
* 并推送消息到企业微信
*
*/
JSONObject jsonObject = JSON.parseObject(json);
Object realName = jsonObject.get("userName");
if (Objects.isNull(realName)){
realName = StringConstant.Custom.NAME_IS_NULL;
}
AdConsumerErrorLog adConsumerErrorLog = new AdConsumerErrorLog();
adConsumerErrorLog.setConsumerType(RabbitMQConstant.PUSH_AD_ROUTING_KEY);
adConsumerErrorLog.setName(realName.toString());
adConsumerErrorLog.setMessageBody(json);
adConsumerErrorLog.setErrorMessage(null);
adConsumerErrorLog.setAddTime(new Date());
adConsumerErrorLog.setAdderName(StringConstant.Custom.DEAD_MESSAGE_ERROR);
adConsumerErrorLog.setDeleteFlag(StringConstant.Custom.CUSTOM_NUMBER_ZONE);
int i = adConsumerErrorLogMapper.insertSelective(adConsumerErrorLog);
log.info("DeadPushAdMqConsumerServiceimpl-死信开始记录到数据库,实体打印:{}\n,储存结果为:{}",
JSON.toJSONString(adConsumerErrorLog),i);
//推送消息到企业微信
PushWeChatMessDto pushWeChatMessDto = new PushWeChatMessDto();
String con = "异常通知:" + realName.toString() +
"\n....异常,请尽快处理!";
pushWeChatMessDto.setContent(con);
pushWeChatMessDto.setGroupChatId(ChatId);
weChatGroupMessage.pushWeChatGroupMessage(pushWeChatMessDto);
}
}