咖啡汪推荐————使用RabbitMQ进行邮件发送,并确保发送成功(带思维导图,源码在github上,需要的自行下载哦)

郑重声明:
本汪作为一名资深的哈士奇,
每天除了闲逛,拆家,就是啃博客了
作为不是在戏精,就是在戏精的路上的二哈
今天就来啃啃RabbitMQ邮件100%发送成功这块小骨头吧

开篇有益:

为什么要用:
压测,20万条数据,每秒2000条邮件发送,连发100秒,不使用RabbitMQ,全走数据库,你懂的
使用RabbitMQ

spring.rabbitmq.listener.simple.concurrency=10
spring.rabbitmq.listener.simple.max-concurrency=20

在这里插入图片描述
20个channel,大家均担,每秒2000/20 = 100条,连发200秒
或者40个channel,大家均担,每秒2000/40=50条,连发200秒
DB压力骤减,有木有

本汪带大家先看看什么是RabbitMQ:
消息队列,他官网是这样吹的:
1.异步消息,支持多种消息传递协议,消息排队,传递确认,到队列的灵活路由,多种交换类型。
2.使用BOSH,Chef,Docker和Puppet进行部署。使用喜欢的编程语言来开发跨语言消息传递,例如Java,.NET,PHP,Python,JavaScript,Ruby,Go 以及许多其他语言。
3.分布式部署,部署为集群以实现高可用性和吞吐量;跨多个可用区域和区域联合。

本汪总结下:轻巧,多协议支持,高可用,总之就是很牛了,号称最受欢迎的开源消息代理之一
在这里插入图片描述

依旧老样子,先附上思维导图和源码链接,然后我们再来谈:
https://github.com/HuskyCorps/distributeMiddleware
本汪建议,真想学的话,源码一定要荡下来去看

思维导图:

在这里插入图片描述

进入实例讲解:
本汪带大家一起去看看,实际操作起来,他是什么样子的

(一)本汪先给大家讲一下RabbitMQTemplate的配置情况

看起来很多,但其实没有什么,就四点
1.自定义RabbitMQ模板
2.配置单一实例消费者(Bean注入)
3.配置多实例消费者(Bean注入)
4.配置邮件发送的交换机、路由及其绑定
本汪尽量将注释标注在代码处,方便大家查看学习
对于大块的注释我也只能另提出去,放在代码最后了,不然实在影响理解逻辑

package com.tencent.bigdata.convenience.server.config;

import com.tencent.bigdata.convenience.server.enums.Constant;
import com.tencent.bigdata.convenience.server.service.log.MsgLogService;
import lombok.SneakyThrows;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.amqp.core.*;
import org.springframework.amqp.rabbit.config.SimpleRabbitListenerContainerFactory;
import org.springframework.amqp.rabbit.connection.CachingConnectionFactory;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.amqp.rabbit.support.CorrelationData;
import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.amqp.SimpleRabbitListenerContainerFactoryConfigurer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;

/**
 * rabbitmq模板操作bean组件示例的自定义注入配置
 *
 * @author Yuezejian
 * @date 2020年 08月23日 10:11:17
 */
//在消息发送、接受时记录DB日志,定时轮训DB日志,
// 查明哪些发送消息没有成功消费,启动重新发送消息机制,进行消息补偿
@Configuration
public class RabbitmqTemplate {
    
    

    private static final Logger log = LoggerFactory.getLogger(RabbitTemplate.class);

    @Autowired
    private MsgLogService msgLogService;

    @Autowired
    private Environment env;

    @Autowired
    private CachingConnectionFactory  connectionFactory;

    //默认配置
    @Autowired
    private SimpleRabbitListenerContainerFactoryConfigurer factoryConfigurer;

    //rabbitmq自定义注入模板操作组件
    @Bean
    public RabbitTemplate rabbitTemplate() {
    
    
        connectionFactory.setPublisherConfirms(env.getProperty("spring.rabbitmq.publisher-confirms",boolean.class));//发送确认
        connectionFactory.setPublisherReturns(env.getProperty("spring.rabbitmq.publisher-returns",boolean.class));//如果消息丢失,需要有所返回
        RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory);
        // 触发setReturnCallback回调必须设置mandatory=true,
        // 否则Exchange没有找到Queue就会丢弃掉消息, 而不会触发回调
        rabbitTemplate.setMandatory(true);
        //确认消息发送成功,还是发送丢失
        //ConfirmCallback  只确认消息是否正确到达 Exchange 中
       rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() {
    
    
           /**
            * @param correlationData 消息唯一标识
            * @param ack 确认结果
            * @param cause 失败原因
            */
            @SneakyThrows
            @Override
            public void confirm(CorrelationData correlationData, boolean ack, String cause) {
    
    
                log.info("消息发送到Exchange————情况反馈:唯一标识:correlationData={},消息确认:ask={},失败原因:cause={}",correlationData,true,cause);
               if (correlationData != null && StringUtils.isNotBlank(correlationData.getId())) {
    
    
                   if (ack) {
    
    
                       //TODO:成功发送到Exchange中后,更新消息发送的状态为投递————成功状态
                       msgLogService.updateStatus(correlationData.getId(), Constant.DELIVER_SUCCESS);
                   } else {
    
    
                       //TODO:发送到Exchange中失败后,更新消息发送的状态为投递————失败状态
                       msgLogService.updateStatus(correlationData.getId(), Constant.DELIVER_FALSE);

                       log.info("消息发送到Exchange--失败:correlationData={},ask={},cause={}", correlationData, true, cause);

                   }
               }
            }
        });
        //ReturnCallback   消息是否从Exchange路由到Queue, 注意: 这是一个失败回调,
        // 只有消息从Exchange路由到Queue失败才会回调这个方法
       rabbitTemplate.setReturnCallback(new RabbitTemplate.ReturnCallback() {
    
    
           /**
            *
            * @param message 标记消息
            * @param replyCode 响应码
            * @param replyText 响应内容
            * @param exchange 交换机
            * @param rountingKey 路由
            */
           @Override
           public void returnedMessage(Message message, int replyCode,
                                       String replyText, String exchange, String rountingKey) {
    
    
                   log.warn("消息由Exchange发送到Queue时失败:message={},replyCode={},replyText={},exchange={},rountingKey={}",
                           message,replyCode,replyText,exchange,rountingKey);

           }

       });

        return rabbitTemplate;
    }

    /**
     * 单一实例消费者(一条队列只会有一个消费者,一条消费者线程监听)
     * 当前仅日志记录用了此例,AcknowledgeMode.NONE
     * @return
     */
    @Bean(name = "singleListenerContainer")
    public SimpleRabbitListenerContainerFactory listenerContainer(){
    
    
        SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory();
        //设置缓存连接,AbstractRabbitListenerContainerFactory
        factory.setConnectionFactory(connectionFactory);
        //生产者服务将JSON类型的数据传递到对应的队列, 而消费端处理器中接收到的数据类型也是JSON类型
        //消息转换器使用了RabbitMQ自带的Jackson2JsonMessageConverter转换器
        factory.setMessageConverter(new Jackson2JsonMessageConverter());
        //确认消费模式-NONE自动确认 -AUTO根据情况确认 -MANUAL手动确认
        //默认情况下消息消费者是自动 ack (确认)消息的,需要设置为手动确认
        // 原因是:自动确认会在消息发送给消费者后立即确认,这样存在丢失消息的可能。
        factory.setAcknowledgeMode(AcknowledgeMode.NONE);
        //设置并发
        factory.setConcurrentConsumers(1);
        //最大并发
        factory.setMaxConcurrentConsumers(1);
        factory.setPrefetchCount(1);
        factory.setTxSize(1);
        return factory;
    }

    /**
     * 多实例消费者(一条队列有多个消费者实例,多个消费者线程进行监听和出路)
     * 当前仅邮件发送用来此例,AcknowledgeMode.MANUAL
     * @return
     */
    @Bean(name = "multiListenerContainer")
    public SimpleRabbitListenerContainerFactory multiListenerContainer(){
    
    
        SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory();
        factoryConfigurer.configure(factory,connectionFactory);
        factory.setMessageConverter(new Jackson2JsonMessageConverter());
        //确认消费模式-NONE自动确认 -AUTO根据情况确认 -MANUAL手动确认
        factory.setAcknowledgeMode(AcknowledgeMode.MANUAL);
        factory.setConcurrentConsumers(env.getProperty("spring.rabbitmq.listener.simple.concurrency",int.class));
        factory.setMaxConcurrentConsumers(env.getProperty("spring.rabbitmq.listener.simple.max-concurrency",int.class));
        factory.setPrefetchCount(env.getProperty("spring.rabbitmq.listener.simple.prefetch",int.class));
        return factory;
    }

    /**
     * 邮件发送————预先创建交换机、路由及其绑定
     * @return
     */
    @Bean
    public DirectExchange mailExchange() {
    
    
        return  new DirectExchange( env.getProperty("mq.email.exchange"),true,false);
    }

    @Bean
    public Queue mailQueue() {
    
    
        return new Queue(env.getProperty("mq.email.queue"),true);
    }

    @Bean
    public Binding mailBinding() {
    
    
        return BindingBuilder.bind(mailQueue()).to(mailExchange()).with(env.getProperty("mq.email.routing.key"));
    }

}

这儿本汪稍微给小白说下,知道的自行跳过: Environment 接口,位于package org.springframework.core.env;包下,继承自PropertyResolver,有获取可利用资源,获取默认资源,接受传入资源三个方法,此处使用到的是其父类PropertyResolver的getProperty()方法底层如下:

public <T> T getProperty(String key, Class<T> targetType) {
    
    
        return this.propertyResolver.getProperty(key, targetType);
    }

此处是以key,和key的类型来获取 application.properties 文件中对rabbitMQ的配置,这边本汪是都提了出去,方便后期的修改
application.properties里配置如下:

#消息发送确认
spring.rabbitmq.publisher-confirms=true
#消息发送队列回调
spring.rabbitmq.publisher-returns=true
mq.email.queue=${
    
    mq.env}.email.queue
mq.email.exchange=${
    
    mq.env}.email.exchange
mq.email.routing.key=${
    
    mq.env}.email.routing.key

到这儿,这块应该就差不多都看懂了

那么本汪继续往下,我们接着给RabbitMQ拆家,看我拆家小能手咖啡汪的厉害

(二)本汪先给大家看一下Controller里都干了什么

controller里,就是对刚发的邮件进行日志记录,然后发他发送到RabbitMQ的队列中,等着被监听消费(把货物摆到货架上,等需要的人来往走拿)
其实依旧很简单,就两点
1.对即将发出的消息做记录,此时邮件状态为发送中
2.把邮件当消息塞入MQ里,把他扔到对应的队列Queue里,等着被消费者拉出去一顿操作,对,就是你想的这样

package com.tencent.bigdata.convenience.server.controller;

import cn.hutool.core.lang.Snowflake;
import com.google.gson.Gson;
import com.tencent.bigdata.convenience.api.response.BaseResponse;
import com.tencent.bigdata.convenience.api.response.StatusCode;
import com.tencent.bigdata.convenience.model.entity.MsgLog;
import com.tencent.bigdata.convenience.server.enums.Constant;
import com.tencent.bigdata.convenience.server.request.MailRequest;
import com.tencent.bigdata.convenience.server.service.log.LogAopAnnotation;
import com.tencent.bigdata.convenience.server.service.log.MsgLogService;
import com.tencent.bigdata.convenience.server.service.mail.ResendMsg;
import com.tencent.bigdata.convenience.server.utils.ValidatorUtil;
import jodd.util.StringUtil;
import org.joda.time.DateTime;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.env.Environment;
import org.springframework.validation.BindingResult;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.UUID;

/**
 * 邮件发送
 *
 * @author Yuezejian
 * @date 2020年 08月24日 19:36:55
 */
@RestController
@RequestMapping("mail")
public class MailController extends AbstractController {
    
    
    //引入RabbitTemplate默认模板,
    //她位于package org.springframework.amqp.rabbit.core;下,
    //提供了一些默认的配置,DEFAULT_REPLY_TIMEOUT = 5000L;
    //String DEFAULT_ENCODING = "UTF-8";等等
    @Autowired
    private RabbitTemplate rabbitTemplate;

    @Autowired
    private Environment env;

    @Autowired
    private MsgLogService msgLogService;

    public static final Snowflake snowflake = new Snowflake(3,2);

    @RequestMapping("send/mq")
    @LogAopAnnotation("发送邮件")
    public BaseResponse stringData(@RequestBody @Validated MailRequest request, BindingResult result) {
    
    
        String checkRes = ValidatorUtil.checkResult(result);
        if (StringUtil.isNotBlank(checkRes)) {
    
    
            return new BaseResponse(StatusCode.InvalidParams.getCode(),checkRes);
        }
        BaseResponse response = new BaseResponse(StatusCode.Success);
        try {
    
    
            //雪花算法,生成全局唯一主键
            //41bit时间戳 + 10bit工作机器id + 12bit序列号
            String msgId = snowflake.nextIdStr();
            request.setCorrelationData(msgId);
            final String msg = new Gson().toJson(request);
            //先置消息发送状态为:Constant.DELIVER_LOADING,即消息发送中
            //设置重试次数为0L
            MsgLog entity = new MsgLog(msgId,msg,env.getProperty("mq.email.exchange"),
                    env.getProperty("mq.email.routing.key"), Constant.DELIVER_LOADING, DateTime.now().toDate(), DateTime.now().toDate());
            entity.setTryCount(0L);
            //TODO:消息发送前,先将记录存入数据库
            msgLogService.recordLog(entity);
            //TODO:将类的对象塞入MQ,设置消息头,监听时用类的方法去接收
            ResendMsg.send(entity, request, rabbitTemplate);
        } catch (Exception e) {
    
    
            log.error("异常信息",e);
            response = new BaseResponse(StatusCode.Fail.getCode(),e.getMessage());
        }
        return response;

    }
}

(三)本汪给大家看一下监听-消费者是怎么配置的

package com.tencent.bigdata.convenience.server.service.mail;

import com.rabbitmq.client.Channel;
import com.tencent.bigdata.convenience.model.entity.MsgLog;
import com.tencent.bigdata.convenience.model.mapper.MsgLogMapper;
import com.tencent.bigdata.convenience.server.enums.Constant;
import com.tencent.bigdata.convenience.server.request.MailRequest;
import com.tencent.bigdata.convenience.server.service.log.MsgLogService;
import org.apache.commons.lang3.StringUtils;
import org.joda.time.DateTime;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.core.MessageProperties;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.io.IOException;

/**
 * 邮件监听器-消费者
 *
 * @author Yuezejian
 * @date 2020年 08月24日 20:23:50
 */
@Component
public class MailMqListener {
    
    
    public static final Logger log = LoggerFactory.getLogger(MailMqListener.class);

    @Autowired
    private MailService mailService;

    @Autowired
    private MsgLogService msgLogService;

    @Autowired
    private MsgLogMapper msgLogMapper;

    //监听邮件发送的消息
    @RabbitListener(queues = {
    
    "${mq.email.queue}"},containerFactory = "multiListenerContainer")
    public void consume(Message message, MailRequest entity, Channel channel) throws IOException, InterruptedException {
    
    
        MessageProperties properties = message.getMessageProperties();
        Long tag = properties.getDeliveryTag();
        Long tryCount=0L;
        String msgId = "";
//        MailRequest entity = new Gson().fromJson(message.getBody().toString(),MailRequest.class);
        msgId    = entity.getCorrelationData();
        log.info("————————————————————监听到消息,消息内容: message={}",entity);
        MsgLog msgLog = msgLogMapper.selectByPrimaryKey(msgId);
        //消费幂等性
        //这儿本汪说一下,
        //什么是需要用定时器重新投递的?1.没有成功发入队列的 2.成功进入队列(但数据丢失等原因,无法被正常监听消费的) -> 才需要重新投递
        //什么是需要进行发送重试的?已经成功发入了队列,并被消费,但是邮件发送出了异常,没有成功收到的 ->才需要重试发送(每10秒重试一次)
        //状态为投递成功的,我们进行邮件发送
        //监听到的消息,肯定都是投递成功的
        try {
    
    
            if (msgLog != null && !Constant.CONSUME_SUCCESS.equals(msgLog.getStatus()) && msgLog.getTryCount() < Constant.MAIL_RESEND_MAX_TIME) {
    
    
                tryCount = msgLog.getTryCount();
                //核心业务逻辑【消息处理~发送邮件(同步;异步)】
                boolean res = mailService.sendSimpleEmail(msgId, entity.getSubject(), entity.getContent(),
                        StringUtils.split(entity.getUserMails(), ","));//用逗号隔开实现邮件的群发
                if (res) {
    
    
                    //确认消费
                    //channel.basicNack(delivery.getEnvelope().getDeliveryTag(), false);
                    //deliveryTag:该消息的index
                    //multiple:是否批量 true:将一次性ack所有小于deliveryTag的消息。
                    channel.basicAck(tag, false);// 消费确认,只确认当前一条
                    return;
                }
            }
        } catch (Exception e) {
    
    
            e.printStackTrace();
            //出现异常时可以走重试机制 ~ 重试次数为3次,每次间隔10s
            // (考虑到RabbitMQ消息延时会有3~4second,可以将休眠时间设为6~7second,确保每10second左右发送一次)
            if (tryCount < Constant.MAIL_RESEND_MAX_TIME) {
    
    
                Thread.sleep(Constant.NEXT_TRY_TIME_AFTER);
                //channel.basicNack(tag, false, true)
                //deliveryTag:该消息的index
                //multiple:是否批量 true:将一次性拒绝所有小于deliveryTag的消息。
                //requeue:被拒绝的是否重新入队列
                //重入队列的消息,会被再次重试发送,每重试一次,重试次数加“1”
                channel.basicNack(tag,false,true);
                tryCount +=1L;
                msgLogMapper.updateTryCount(msgId,tryCount, DateTime.now().toDate());
                return;
            }
        }
        //当走到这一步时,代表消息已被消费(status=3)或者 重试次数达到上限 等情况-但不管是哪种情况,
        // 都需要将消息从队列中移除,防止下次项目重启时重新监听消费
        channel.basicAck(tag,false);//消费确认,只确认当前一条
        }

}
service

(四) 邮件发送service
JavaMailSender 位于package org.springframework.mail.javamail;包下,需要依赖:

 <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-mail</artifactId>
            <version>1.5.7.RELEASE</version>
        </dependency>

Mailservice:

package com.tencent.bigdata.convenience.server.service.mail;

import com.google.gson.Gson;
import com.tencent.bigdata.convenience.model.mapper.MsgLogMapper;
import com.tencent.bigdata.convenience.server.controller.AbstractController;
import com.tencent.bigdata.convenience.server.enums.Constant;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.env.Environment;
import org.springframework.mail.MailException;
import org.springframework.mail.SimpleMailMessage;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.stereotype.Service;
/**
 * 邮件service
 *
 * @author Yuezejian
 * @date 2020年 08月24日 20:23:04
 */
@Service
public class MailService extends AbstractController {
    
    
    @Autowired
    private Environment env;

    @Autowired
    private JavaMailSender mailSender;

    @Autowired
    private MsgLogMapper msgLogMapper;

    //TODO:发送简单的邮件消息
    //@Async("threadPoolTaskExecutor")此处切记不可使用AOP注解,方法进入切面后,由于@Around,返回类型boolean会被设为void,会造成异常
    //Caused by: org.springframework.aop.AopInvocationException: Null return value from advice does not match primitive return type
    //仍想使用异步线程处理,可以修改ACK实现方法,将返回类型设为void即可
    //或对mailSender.send做包装
    public Boolean sendSimpleEmail(final String msgId,final String subject,final String content,final String ... tos) throws Exception {
    
    
           Boolean res = true;
            SimpleMailMessage message=new SimpleMailMessage();
            message.setSubject(subject);
            message.setText(content);
            message.setTo(tos);
            message.setFrom(env.getProperty("mail.send.from"));
        try {
    
    
            mailSender.send(message);
        } catch (MailException e) {
    
    
            log.error("邮件发送失败, to={}, title={}, e={}", new Gson().toJson(tos), subject, e);
            res = false;
            throw e;
        } finally {
    
    
            this.updateMsgSendStatus(msgId,res);
        }
        return res;
    }

    //TODO:更新消息处理的结果
    private void updateMsgSendStatus(final String msgId,Boolean res){
    
    
        if (StringUtils.isNotBlank(msgId)){
    
    
            if (res){
    
    
                msgLogMapper.updateStatus(msgId, Constant.CONSUME_SUCCESS);
            }else{
    
    
                msgLogMapper.updateStatus(msgId, Constant.CONSUME_FALSE);
            }
        }
    }
}

(五)使用定时器,对发送失败邮件进行重新发送

package com.tencent.bigdata.convenience.server.service.mail;

import com.google.gson.Gson;
import com.tencent.bigdata.convenience.model.entity.MsgLog;
import com.tencent.bigdata.convenience.server.enums.Constant;
import com.tencent.bigdata.convenience.server.request.MailRequest;
import com.tencent.bigdata.convenience.server.service.log.MsgLogService;
import lombok.SneakyThrows;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.amqp.AmqpException;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.core.MessageDeliveryMode;
import org.springframework.amqp.core.MessagePostProcessor;
import org.springframework.amqp.core.MessageProperties;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.amqp.rabbit.support.CorrelationData;
import org.springframework.amqp.support.converter.AbstractJavaTypeMapper;
import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

import java.util.List;

/**
 * RabbitMQ邮件定时重发
 *
 * @author Yuezejian
 * @date 2020年 08月29日 22:03:11
 */
@Component
public class ResendMsg {
    
    
    public static final Logger log = LoggerFactory.getLogger(ResendMsg.class);

    @Autowired
    private MsgLogService msgLogService;

    @Autowired
    private RabbitTemplate rabbitTemplate;

    @SneakyThrows
    @Scheduled(cron = "0 0/2 * * * ?")
//    @Scheduled(cron = "0 0 0/1 * * ?")
//    @Scheduled(cron = "0 0/30 * * * ?")
    public void resend() {
    
    
        try {
    
    
            log.info("开始执行定时任务(重新投递邮件)");
            List<MsgLog> msgLogs = null;
            //获取需要重新投递的(状态为投递中/投递失败,或为投递成功,但限定时间内仍未被正常消费的)
            String status = Constant.DELIVER_LOADING + "," + Constant.DELIVER_FALSE + "," + Constant.DELIVER_SUCCESS;
            msgLogs = msgLogService.selectTimeoutMsg(status);
            if (msgLogs != null && !msgLogs.isEmpty()) {
    
    
                msgLogs.forEach(msgLog -> {
    
    
                    if (StringUtils.isNotBlank(msgLog.getMsg()) &&
                            StringUtils.isNotBlank(msgLog.getExchange()) &&
                            StringUtils.isNotBlank(msgLog.getRoutingKey())) {
    
    
                        //反序列化,日志记录中的msg是进过序列化的MailRequest对象
                        MailRequest request = new Gson().fromJson(msgLog.getMsg(),MailRequest.class);
                        //重新投递
                        send(msgLog, request, rabbitTemplate);
                        log.info("重新投递--成功--消息id={}", msgLog.getMsgId());
                    }
                });
            }
        } catch (Exception e) {
    
    
            log.error("定时任务重新拉取投递失败的消息~重新进行投递~发生异常:",e.fillInStackTrace());
        }


    }

    public static void send(MsgLog msgLog, MailRequest request, RabbitTemplate rabbitTemplate) {
    
    
        rabbitTemplate.setExchange(msgLog.getExchange());
        rabbitTemplate.setRoutingKey(msgLog.getRoutingKey());
        rabbitTemplate.setMessageConverter(new Jackson2JsonMessageConverter());
        rabbitTemplate.convertAndSend(request, new MessagePostProcessor() {
    
    
            @Override
            public Message postProcessMessage(Message message) throws AmqpException {
    
    
                //设置消息持久化和消息头的类型
                MessageProperties properties = message.getMessageProperties();
                properties.setDeliveryMode(MessageDeliveryMode.PERSISTENT);//设置持久化
                properties.setHeader(AbstractJavaTypeMapper.DEFAULT_CONTENT_CLASSID_FIELD_NAME, MailRequest.class);
                return message;
            }
        });
    }

}

(六)MsgLogService,如果真的什么都懒的做的话,Ctrl+C,你们心中想的,本汪都懂

package com.tencent.bigdata.convenience.server.service.log;

import com.google.gson.Gson;
import com.tencent.bigdata.convenience.model.entity.MsgLog;
import com.tencent.bigdata.convenience.model.mapper.MsgLogMapper;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.Date;
import java.util.List;

/**
 * rabbitMq消息发送情况日志记录
 *
 * @author Yuezejian
 * @date 2020年 08月27日 14:06:17
 */
@Service
@Transactional(rollbackFor = Exception.class)
public class MsgLogService {
    
    
    private static final Logger logger = LoggerFactory.getLogger(MsgLogService.class);

    @Autowired
    private MsgLogMapper logMapper;

    //记录日志
//    @Async("threadPoolTaskExecutor")
    public void recordLog(MsgLog log) throws Exception {
    
    
        logger.info("正在-新增-消息发送日志 MsgLog={}",new Gson().toJson(log));
        logMapper.insertSelective(log);
        logger.info("新增-消息发送日志----成功 ");
    }

    //更新修改日志
    public int updateLog(MsgLog log){
    
    
        return logMapper.updateByPrimaryKeySelective(log);
    }

    /**
     * 更据邮件发送记录中,邮件的状态和重发的次数,筛选需要重发的邮件
     * (状态不为“3L”即发送不成功,重试次数小于3)
     * @param status  邮件发送状态——邮件投递中+邮件投递失败
     * @return
     * @throws Exception
     */
    public List<MsgLog> selectTimeoutMsg(String status) throws Exception {
    
    
        logger.info("查询超时消息列表");
        String arr[] = StringUtils.split(status,",");
        Long deliverLoading = Long.valueOf(arr[0]);//发送状态为投递中
        Long deliverFalse = Long.valueOf(arr[1]);//发送状态为投递失败
        Long deliverSuccess = Long.valueOf(arr[2]);//发送状态为投递成功,
        // 但数据以及损坏或丢失,而无法被正常消费,比如RabbitMQ崩溃,本汪怎么说,应该懂得啊,不可预料因素
        return logMapper.selectTimeoutMsg(deliverLoading, deliverFalse,deliverSuccess);
    }

    /**
     *
     * @param msgId
     * @param flag
     * @throws Exception
     */
    public void updateStatus(String msgId, long flag) throws Exception {
    
    
        logMapper.updateStatus(msgId,flag);
    }

    public void updateTryCount(String msgId,Long tryCount, Date nextTryTime) throws Exception {
    
    
        logMapper.updateTryCount(msgId,tryCount,nextTryTime);
    }
}

(七)BUG解决,不自己加最后一个构造函数,会有BUG
JSON parse error: Cannot construct instance of XXXXXX (although at least one Creator exists): no String-argument constructor/factory method to deserialize from String value ,rabbitMQ会有消息无法消费,一直积压

package com.tencent.bigdata.convenience.server.request;

import com.google.gson.Gson;
import lombok.Data;
import org.hibernate.validator.constraints.NotBlank;

import java.io.IOException;
import java.io.Serializable;

/**
 * 邮件
 *
 * @author Yuezejian
 * @date 2020年 08月24日 19:39:42
 */
@Data
public class MailRequest implements Serializable {
    
    
    private static final long serialVersionUID = 1L;

    private String correlationData;//RabbitMQ queue唯一标识符

    @NotBlank(message = "用户邮箱不能为空!")
    private String userMails;

    @NotBlank(message = "邮箱主题不能为空!")
    private String subject;

    @NotBlank(message = "邮箱内容不能为空!")
    private String content;

    public MailRequest(String correlationData, String userMails, String subject, String content) {
    
    
        this.correlationData = correlationData;
        this.userMails = userMails;
        this.subject = subject;
        this.content = content;
    }

    /**
     * 为了解决一下问题
     *  Warning:no String-argument constructor/factory
     *  method to deserialize from String value
     * @param json
     * @throws IOException
     */
    public MailRequest(String json) throws IOException {
    
    
        MailRequest param = new Gson().fromJson(json, MailRequest.class);
        this.correlationData = param.getCorrelationData();
        this.userMails = param.getUserMails();
        this.subject = param.getSubject();
        this.content = param.getContent();
    }
}

(八)RabbitMQ邮件发送知识拓展

1.550,503异常
2.怎么申请邮件
3.163邮箱对邮件标题含“test”敏感词汇自动拦截
4.阿里云服务器,62端口占用解决办法

最后,代码中涉及到的BaseResponse,统一响应模型,以及RabbimMQ,AOP日志记录等代码,想了解的可以自行查看本汪相关博客
AOP日志记录:https://blog.csdn.net/weixin_42994251/article/details/108302819
统一响应模型:https://blog.csdn.net/weixin_42994251/article/details/108302377

猜你喜欢

转载自blog.csdn.net/weixin_42994251/article/details/108456901