RocketMQ搭建邮件发送服务器

RocketMQ搭建邮件发送服务器

在我们项目中使用邮件通知的功能非常多,比如用户异常登录需要发送邮件告知用户账号异常,用户注册需要通过邮件发送验证码验证用户合法性等等。但是,在我们实际使用中发现邮件功能与系统其他模块耦合性太高,并且邮件发送是一个相对比较耗时的功能,所以我们为了降低系统间的耦合性,结合RocketMQ搭建了邮件发送服务器异步的发送邮件通知,提升用户体验。

项目简介

基于rocketmq、redis搭建的独立邮件服务器,降低了系统各模块之间的耦合性,同时利用rocketmq消息队列的特性达到异步发送邮件通知的效果,提升用户体验,利用redis天然的幂等性,保证了消息消息的幂等性。

项目地址

项目我已上传到 github 中,欢迎大家下载使用。此项目可以直接导入项目使用,作为已有项目邮件发送服务器。具体详见 github

项目结构在这里插入图片描述技术选型

技术说明
rocketmq邮件服务器核心
redis保证消息消息幂等性
thymeleaf模板引擎生成邮件
springboot项目搭建框架
mybatisORM框架
开发环境
工具版本号
JDK1.8
Mysql5.7
Redis5.0
RocketMQ4.5.2
SpringBoot2.5
Mybatis2.1.1
学习此项目

此项目是基于SpringBoot、Mybatis框架搭建的,在学习此项目开始之前需要提前创建一个名为mailserver 的SpringBoot项目,并创建如下package
在这里插入图片描述

引入项目所需要的jar包

 <!--整合redis-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-pool2</artifactId>
        </dependency>
        <!--整合thymeleaf-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!--jdbc-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>
        <!--整合mybatis-->
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.1.1</version>
        </dependency>
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <version>1.3.2</version>
        </dependency>


        <!--mysql-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.28</version>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>1.2.1</version>
        </dependency>
        <!--mq-->
        <!-- https://mvnrepository.com/artifact/org.apache.rocketmq/rocketmq-spring-boot-starter -->
        <dependency>
            <groupId>org.apache.rocketmq</groupId>
            <artifactId>rocketmq-spring-boot-starter</artifactId>
            <version>2.2.0</version>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <scope>test</scope>
        </dependency>
        <!--mail-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-mail</artifactId>
        </dependency>

配置application.properties文件

  1. 配置端口号,避免端口被占用 server.port=8081
  2. 配置rocketmq相关信息
##mq
rocketmq.name-server=192.168.119.130:9876  #服务器ip和端口号
rocketmq.producer.group=msg-group              #生产者组名
rocketmq.consumer.group=msg-group         #消费者组名
#异步消息投递冲投递次数
rocketmq.producer.retry-times-when-send-async-failed=3 
rocketmq.producer.retry-next-server=true 

#自定义主题
mq.tjoker.topic=mail 
mq.tjoker.tag=tjoker
#超时时间
mq.tjoker.timeout=10000
#数据失败重试时间 分钟
mq.tjoker.retrytime=3
  1. util包中创建RocketmqUtil类作为生产者发送消息到 rocketmq
  @Autowired  //注入rocketmqTemplate
    private  RocketMQTemplate rocketMQTemplate;
    @Autowired 注入mailSendLogsService
    private  MailSendLogsService mailSendLogsService;
    @Value("${mq.tjoker.timeout}") //获取配置文件中参数信息 发送消息超时时间
    private  long  timeout;
    @Value("${mq.tjoker.topic}")  //获取配置文件中参数信息 主题
    private String topic;
    @Value("${mq.tjoker.tag}")
    private String tag;
    @Value("${rocketmq.producer.group}")
    private String producer_group;
    @Value("${mq.tjoker.retrytime}")
    private long retrytime;	 //重试时间 单位 分
  1. 创建两个方法
    recordLogs() 记录消息发送日志保证rocketmq消息发送的高可用
    sendMessage() 消息发送功能实体
    MsgMQ 消息实体、MailSendLogs 消息日志(保证可靠性) 均是 model 包下的实体
/**
     * @Description:记录日志信息,保证rocketmq消息一定发送成功
     * @param msgMQ: 消息实体
     * @return: void
     */
    public void recordLogs(MsgMQ msgMQ){

        //将当前数据存入数据库
        MailSendLogs mailSendLogs= new MailSendLogs();
        mailSendLogs.setCreateTime(new Date());
        System.out.println("时间:"+mailSendLogs.getCreateTime());
        mailSendLogs.setGroup_name(producer_group);
        mailSendLogs.setMsg_status(0);
        mailSendLogs.setMsg_tag(tag);
        mailSendLogs.setMsg_topic(topic);
        mailSendLogs.setCount(0);
        mailSendLogs.setTryTime(new Date(System.currentTimeMillis()+1000 * 60 *retrytime));
        mailSendLogs.setMsgID(msgMQ.getMsgId());
        mailSendLogs.setUserID(Integer.parseInt(msgMQ.getUserId()));
        mailSendLogs.setMsg_body(JSON.toJSONString(msgMQ));
        mailSendLogsService.insert(mailSendLogs);
        //异步发送消息
        try {
            sendMessage(topic,
                    tag,
                    msgMQ.getMsgId(),
                    JSON.toJSONString(msgMQ));
        } catch (Exception e1) {
            e1.printStackTrace();
            log.error("消息"+ msgMQ.getMsgId()+"发送失败");
        }
    }

    /**
     * @Description:消息发送
     * @param topic: 主题:一级目录
     * @param tags: 标签:二级目录
     * @param msgId: 消息id
     * @param body: 主体
     * @return: void
     */
    public  void sendMessage(String topic, String tags, String msgId, String body) throws Exception {
        //判断Topic是否为空
        if (StringUtils.isEmpty(topic)) {
            log.info("mail主题不能为空");
            return ;
        }
        //判断消息内容是否为空
        if (StringUtils.isEmpty(body)) {
            log.info("消息不能为空");
            return ;
        }
        //消息体
        //异步发送消息(destination:没有进行封装)
        rocketMQTemplate.asyncSend(topic + ":" + tags, body, new SendCallback() {
            @Override
            public void onSuccess(SendResult sendResult) {
                log.info("消息发送成功"+sendResult.getSendStatus());
                //更新用户状态
                mailSendLogsService.updateStatus(msgId,1);
            }
            @Override
            public void onException(Throwable e) {
                //消息发送失败,定义一个定时器定时去发送消息
                e.printStackTrace();
                log.info("消息发送失败");

            }
        },timeout);

    }
  1. 创建MailSendTask 定时任务,定时去重发失败消息
    首先需要在properties定义执行任务时间corn表达式
#配置重发失败消息频率 (corn表达式)
scheduled.tjoker.corn=0 0 0/1 * * ?

注入相应类,实现功能

/**
 * @program: mail
 * @description: 消息发送定时器
 * 主要针对发送失败的消息进行重新发送
 * @author: 十字街头的守候
 * @create: 2021-05-29 15:15
 **/
@Component
@Slf4j
public class MailSendTask {
    @Autowired
    private MailSendLogsService mailSendLogsService;
    @Autowired
    RocketmqUtil rocketmqUtil;
    @Scheduled(cron = "${scheduled.tjoker.corn}")
    public void mailResendTask() {
        log.info("针对失败消息重新投递");
        // statues 0 消息投递中   1 投递成功   2投递失败
        List<MailSendLogs> logs = mailSendLogsService.getMailSendLogsByStatus();
        //判断当前结果集是否为空
        if(logs==null||logs.size()==0){
            return ;
        }
        //轮训消息
        logs.forEach(mailSendLogs -> {
            // 判断当前重复次数是否超过3次如果超过3次则认定为失败
            if(mailSendLogs.getCount()>=3){
               //设置发送失败
                 mailSendLogsService.updateStatus(mailSendLogs.getMsgID(),2);
            }else{
                //重新发送
                //更新重复次数和尝试时间
                mailSendLogsService.updateCount(mailSendLogs.getMsgID(),new Date());
                //尝试再次发送
                try {
                   rocketmqUtil.sendMessage(mailSendLogs.getMsg_topic(),
                            mailSendLogs.getMsg_tag(),
                            mailSendLogs.getMsgID(),
                            mailSendLogs.getMsg_body());
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        });

    }

}

6.定义消息消费者,消费消息并发送相应邮件。对应于MailServer 包

@Slf4j
@Component
@RocketMQMessageListener(consumerGroup = "${rocketmq.consumer.group}",
        topic = "${mq.tjoker.topic}",
        selectorExpression = "${mq.tjoker.tag}")
public class MailServer implements RocketMQListener<MessageExt> {
    @Autowired
    JavaMailSender javaMailSender;
    @Autowired
    MailProperties mailProperties;
    @Autowired
    TemplateEngine templateEngine;
    @Autowired
    RedisUtil redisUtil;
    @SneakyThrows
    @Override
    public void onMessage(MessageExt message) {
        log.info("开始接收消息");
        MsgMQ msgMQ = JSON.parseObject(message.getBody(), MsgMQ.class);
        String msgId = msgMQ.getMsgId();
        log.info("dataName:"+ msgMQ.toString());
        //检查当前消息是否被消费,保证消息幂等性
        if(redisUtil.hHasKey(msgId,"mail_send")){
            log.warn(msgId + " 消息已被消费,无法消费");
            return;
        }
        //接收到消息发送邮件
            sendMail(msgMQ);
    }

    private void sendMail(MsgMQ msgMQ)  {
        //收到消息,发送邮件
        MimeMessage msg = javaMailSender.createMimeMessage();
        MimeMessageHelper helper = new MimeMessageHelper(msg);
        Context context = new Context();
        context.setVariable("userName", msgMQ.getUsername());
        //渲染HTML字符串 mail为邮件格式
        String mail = templateEngine.process("mail", context);
        //针对消息的格式验证
        //在前端注册或者登录时系统已经验证过,格式都正确
        //现在这里不做处理,默认消息格式都正确
        try {
            helper.setFrom(mailProperties.getUsername());//发件人
            helper.setTo(msgMQ.getTarget_address());//收件人
            helper.setSubject("账户异常");//主题
            helper.setSentDate(new Date());//发送时间
            helper.setText(mail, true);// 正文
            javaMailSender.send(msg);//发送
            //将当前数据存入redis中
            redisUtil.hset(msgMQ.getMsgId(),"mail_send","tjoker");
            log.info(msgMQ.getMsgId()+" 邮件发送成功");
        } catch (MessagingException e) {
          //  e.printStackTrace();
            log.error("邮件发送失败"+e.getMessage());
        }
    }
}

为了保证消息的幂等性,我们引入了redis,将msgid作为key存入到redis中,进行消费时判断当前msgid是否存在redis中
在这里插入图片描述
如果存在则不消费,如果不存在则消息当前消息,并发送邮件,发送成功后把当前msgid存入redis中
在这里插入图片描述
为了使用redis方便,我们自定义了 redisUtil,详见项目中。

至此,我们邮件发送服务器核心功能已经开发完成,赶快下载下来使用吧。

有问题和更好的改进方案请留言,欢迎大家共同完善此项目
https://github.com/Tjoker-cell/mail

  • 6
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值