SpringBoot整合RabbitMQ 企业级实战示例-附源码

507 阅读4分钟

SpringBoot整合RabbitMQ 企业级实战示例-附源码

架构

SpringBoot 整合 RabbitMQ

image.png

1. 生产者、消费者在不同服务中 (推荐)

img

  • 解释

    业务服务 创建MQ生产者并丢消息到MQ队列, 消费者监听MQ队列消费, 消费者 http调用业务服务接口, 业务服务接口 处理业务逻辑。

@startuml title 生产者、消费者在不同服务中 业务服务 -> 业务服务: 创建MQ生产者 业务服务 -> MQ队列: 生产消息 消费者服务 -> MQ队列: 消费者监听MQ队列消费 消费者服务 -> 业务服务: 消费者http调用业务处理接口 业务服务 -> 业务服务: 处理业务逻辑

@enduml

2. 生产者、消费者在同一个服务中

img

  • 解释:

    业务服务 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);

    }

}