RocketMQ搭建邮件发送服务器
在我们项目中使用邮件通知的功能非常多,比如用户异常登录需要发送邮件告知用户账号异常,用户注册需要通过邮件发送验证码验证用户合法性等等。但是,在我们实际使用中发现邮件功能与系统其他模块耦合性太高,并且邮件发送是一个相对比较耗时的功能,所以我们为了降低系统间的耦合性,结合RocketMQ搭建了邮件发送服务器异步的发送邮件通知,提升用户体验。
项目简介
基于rocketmq、redis搭建的独立邮件服务器,降低了系统各模块之间的耦合性,同时利用rocketmq消息队列的特性达到异步发送邮件通知的效果,提升用户体验,利用redis天然的幂等性,保证了消息消息的幂等性。
项目地址
项目我已上传到 github 中,欢迎大家下载使用。此项目可以直接导入项目使用,作为已有项目邮件发送服务器。具体详见 github
项目结构
技术选型
技术 | 说明 |
---|---|
rocketmq | 邮件服务器核心 |
redis | 保证消息消息幂等性 |
thymeleaf | 模板引擎生成邮件 |
springboot | 项目搭建框架 |
mybatis | ORM框架 |
开发环境
工具 | 版本号 |
---|---|
JDK | 1.8 |
Mysql | 5.7 |
Redis | 5.0 |
RocketMQ | 4.5.2 |
SpringBoot | 2.5 |
Mybatis | 2.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
文件
- 配置端口号,避免端口被占用
server.port=8081
- 配置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
- 在
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; //重试时间 单位 分
- 创建两个方法
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);
}
- 创建
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