消息落库,对消息状态进行打标
实现流程
- 发送消息时,将当前消息数据存入数据库,投递状态为消息投递中
- 开启消息确认回调机制。确认成功,更新投递状态为消息投递成功
- 开启定时任务,重新投递失败的消息。重试超过3次,更新投递状态为投递失败
1. 定义消息状态常量
新建MailConstants.java
package com.xxxx.server.pojo;
/**
* 消息状态
*
* @author zhoubin
* @since 1.0.0
*/
public class MailConstants {
//消息投递中
public static final Integer DELIVERING = 0;
//消息投递成功
public static final Integer SUCCESS = 1;
//消息投递失败
public static final Integer FAILURE = 2;
//最大重试次数
public static final Integer MAX_TRY_COUNT = 3;
//消息超时时间
public static final Integer MSG_TIMEOUT = 1;
//队列
public static final String MAIL_QUEUE_NAME = "mail.queue";
//交换机
public static final String MAIL_EXCHANGE_NAME = "mail.exchange";
//路由键
public static final String MAIL_ROUTING_KEY_NAME = "mail.routing.key";
}
2.修改新增员工的方法,发送消息时,将当前消息数据存入数据库,投递状态为消息投递中
EmployeeServiceImpl.java
/**
* 添加员工
*
* @param employee
* @return
*/
@Override
public RespBean addEmp(Employee employee) {
//处理合同期限,保留2位小数
LocalDate beginContract = employee.getBeginContract();
LocalDate endContract = employee.getEndContract();
long days = beginContract.until(endContract, ChronoUnit.DAYS);//计算有多少天
DecimalFormat decimalFormat = new DecimalFormat("##.00");//保留两位小数
employee.setContractTerm(Double.parseDouble(decimalFormat.format(days / 365.00)));//插入合同期限
if (1 == employeeMapper.insert(employee)) {
Employee emp = employeeMapper.getEmployee(employee.getId()).get(0);
//数据库记录发送的消息
String msgId = UUID.randomUUID().toString();
MailLog mailLog = new MailLog();
mailLog.setMsgId(msgId);
mailLog.setEid(employee.getId());
mailLog.setStatus(0);
mailLog.setRouteKey(MailConstants.MAIL_ROUTING_KEY_NAME);
mailLog.setExchange(MailConstants.MAIL_EXCHANGE_NAME);
mailLog.setCount(0);//重试次数
mailLog.setTryTime(LocalDateTime.now().plusMinutes(MailConstants.MSG_TIMEOUT));//重试时间,一分钟之后
mailLog.setCreateTime(LocalDateTime.now());
mailLog.setUpdateTime(LocalDateTime.now());
mailLogMapper.insert(mailLog);
//发送信息
rabbitTemplate.convertAndSend(MailConstants.MAIL_EXCHANGE_NAME, MailConstants.MAIL_ROUTING_KEY_NAME, emp, new CorrelationData(msgId));
return RespBean.success("添加成功!");
}
return RespBean.error("添加失败!");
}
3. 修改邮件服务,将队列名改为常量定义的队列名
VoaMailApplication.java
package com.yjxxt.mail;
import com.yjxxt.server.pojo.MailConstants;
import org.springframework.amqp.core.Queue;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.context.annotation.Bean;
/**
* 启动类
*/
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
public class VoaMailApplication{
public static void main(String[] args) {
SpringApplication.run(VoaMailApplication.class, args);
}
@Bean
public Queue queue(){
return new Queue(MailConstants.MAIL_QUEUE_NAME);
}
}
MailReceiver.java
package com.yjxxt.mail.receiver;
import com.yjxxt.server.pojo.Employee;
import com.yjxxt.server.pojo.MailConstants;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.mail.MailProperties;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.MimeMessageHelper;
import org.springframework.stereotype.Component;
import org.thymeleaf.TemplateEngine;
import org.thymeleaf.context.Context;
import javax.mail.internet.MimeMessage;
import java.io.IOException;
import java.util.Date;
/**
* 消息接收者
*/
@Component
public class MailReceiver {
private static final Logger LOGGER = LoggerFactory.getLogger(MailReceiver.class);
@Autowired
private JavaMailSender javaMailSender;
@Autowired
private MailProperties mailProperties;
@Autowired
private TemplateEngine templateEngine;
@RabbitListener(queues = MailConstants.MAIL_QUEUE_NAME)
public void handler(Employee employee) {
MimeMessage msg = javaMailSender.createMimeMessage();
MimeMessageHelper helper = new MimeMessageHelper(msg);
//发件人
helper.setFrom(mailProperties.getUsername());
//收件人
helper.setTo(employee.getEmail());
//主题
helper.setSubject("入职欢迎邮件");
//发送日期
helper.setSentDate(new Date());
//邮件内容
Context context = new Context();
context.setVariable("name", employee.getName());
context.setVariable("posName", employee.getPosition().getName());
context.setVariable("joblevelName", employee.getJoblevel().getName());
context.setVariable("departmentName", employee.getDepartment().getName());
String mail = templateEngine.process("mail", context);
helper.setText(mail, true);
//发送邮件
javaMailSender.send(msg);
LOGGER.info("邮件发送成功");
} catch (Exception e) {
LOGGER.error("邮件发送失败=========>{}", e.getMessage());
}
}
}
4.在yeb-server中配置RabbitMQ,开启消息确认回调以及消息失败回调
RabbitMQConfig.java
package com.xxxx.server.config;
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
import com.xxxx.server.pojo.MailConstants;
import com.xxxx.server.pojo.MailLog;
import com.xxxx.server.service.IMailLogService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.DirectExchange;
import org.springframework.amqp.core.Queue;
import org.springframework.amqp.rabbit.connection.CachingConnectionFactory;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* RabbitMQ配置类
*/
@Configuration
public class RabbitMQConfig {
private static final Logger LOGGER = LoggerFactory.getLogger(RabbitMQConfig.class);
@Autowired
private CachingConnectionFactory cachingConnectionFactory;
@Autowired
private IMailLogService mailLogService;
@Bean
public RabbitTemplate rabbitTemplate(){
RabbitTemplate rabbitTemplate = new RabbitTemplate(cachingConnectionFactory);
/**
* 消息确认回调,确认消息是否到达broker
* data:消息唯一标识
* ack:确认结果
* cause:失败原因
*/
rabbitTemplate.setConfirmCallback((data,ack,cause)->{
String msgId = data.getId();//之前存入的msgId
if(ack){
LOGGER.info("{}=======>消息发送成功",msgId);
mailLogService.update(new UpdateWrapper<MailLog>().set("status",1).eq("msgId",msgId));
}else {
LOGGER.error("{}=======>消息发送失败",msgId);
}
});
/**
* 消息失败回调,比如router不到queue时回调
* msg:消息主题
* repCode:响应码
* repText:相应描述
* exchange:交换机
* routingkey:路由键
*/
rabbitTemplate.setReturnCallback((msg,repCode,repText,exchange,routingkey)->{
LOGGER.error("{}=======>消息发送queue时失败",msg.getBody());
});
return rabbitTemplate;
}
@Bean
public Queue queue(){
return new Queue(MailConstants.MAIL_QUEUE_NAME,true);
}
@Bean
public DirectExchange directExchange(){
return new DirectExchange(MailConstants.MAIL_EXCHANGE_NAME);
}
@Bean
public Binding binding(){
return BindingBuilder.bind(queue()).to(directExchange()).with(MailConstants.MAIL_ROUTING_KEY_NAME);
}
}
application.yml
# rabbitmq配置
rabbitmq:
# 用户名
username: guest
# 密码
password: guest
# 服务器地址
host: 你的服务器地址
# 端口
port: 5672
# 消息失败回调
publisher-returns: true
# 消息确认回调
publisher-confirm-type: correlated
5.定时任务重发失败消息
重新投递失败的消息。重试超过3次,更新投递状态为投递失败
MailTask.java
package com.xxxx.server.task;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
import com.xxxx.server.pojo.Employee;
import com.xxxx.server.pojo.MailConstants;
import com.xxxx.server.pojo.MailLog;
import com.xxxx.server.service.IEmployeeService;
import com.xxxx.server.service.IMailLogService;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
import java.util.List;
/**
* 邮件发送定时任务
*/
@Component
public class MailTask {
@Autowired
private IMailLogService mailLogService;
@Autowired
private IEmployeeService employeeService;
@Autowired
private RabbitTemplate rabbitTemplate;
/**
* 邮件发送定时任务
* 10秒执行一次
*/
@Scheduled(cron = "0/10 * * * * ?")
public void mailTask(){
List<MailLog> list = mailLogService.list(new QueryWrapper<MailLog>().eq("status", 0).lt("tryTime",
LocalDateTime.now()));
list.forEach(mailLog -> {
//如果重试次数超过3次,更新状态为投递失败,不再重试
if (3<=mailLog.getCount()){
mailLogService.update(new UpdateWrapper<MailLog>().set("status",2).eq("msgId",mailLog.getMsgId()));
}
mailLogService.update(new UpdateWrapper<MailLog>().set("count",mailLog.getCount()+1).set("updateTime",LocalDateTime.now()).set("tryTime",LocalDateTime.now().plusMinutes(MailConstants.MSG_TIMEOUT)).eq("msgId",mailLog.getMsgId()));
Employee emp = employeeService.getEmployee(mailLog.getEid()).get(0);
//发送消息
rabbitTemplate.convertAndSend(MailConstants.MAIL_EXCHANGE_NAME,MailConstants.MAIL_ROUTING_KEY_NAME,emp,new CorrelationData(mailLog.getMsgId()));
});
}
}
6.在启动类中开启定时任务
package com.xxxx.server;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* 启动类
*/
@SpringBootApplication
@MapperScan("com.xxxx.server.mapper")
@EnableScheduling
public class YebApplication {
public static void main(String[] args) {
SpringApplication.run(YebApplication.class,args);
}
}
7.测试
正常发送
修改发送时的交换机名称,首先出现发送失败。
经过定时任务重新发送成功(定时任务的交换机没改)
如果把定时任务的交换机也改了会出现重试3次都没发送成功,最终状态为消息发送失败
消息接收幂等性的问题,主要使用Redis处理
1.修改配置
除了添加 Redis 相应配置,还要开启 RabbitMQ 的手动确认机制
server:
# 端口
port: 8082
spring:
# 邮件配置
mail:
# 邮件服务器地址
host: smtp.163.com
# 协议
protocol: smtp
# 编码格式
default-encoding: utf-8
# 授权码(在邮箱开通服务时获取)
password: FSMMQTAPBUUBKJWA
# 发送者邮箱地址
username: a2448853433@163.com
# 端口(不同邮箱端口号不同)
port: 25
# rabbitmq配置
rabbitmq:
# 用户名
username: guest
# 密码
password: guest
# 服务器地址
host: 你的服务器地址
# 端口
port: 5672
listener:
simple:
#开启手动确认
acknowledge-mode: manual
redis:
#超时时间
timeout: 10000ms
#服务器地址
host: localhost
#服务器端口
port: 6379
#数据库
database: 0
#密码
password: root
lettuce:
pool:
#最大连接数,默认8
max-active: 1024
#最大连接阻塞等待时间,默认-1
max-wait: 10000ms
#最大空闲连接
max-idle: 200
#最小空闲连接
min-idle: 5
2.修改邮件发送服务
首先去 Redis 查看当前消息id是否存在,如果存在说明已经消费,直接返回。如果不存在,正常
发送消息,并将消息id存入 Reids 。需要手动确认消息
package com.yjxxt.mail;
import com.rabbitmq.client.Channel;
import com.yjxxt.server.pojo.Employee;
import com.yjxxt.server.pojo.MailConstants;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.amqp.support.AmqpHeaders;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.mail.MailProperties;
import org.springframework.data.redis.core.HashOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.MimeMessageHelper;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageHeaders;
import org.springframework.stereotype.Component;
import org.thymeleaf.TemplateEngine;
import org.thymeleaf.context.Context;
import javax.mail.internet.MimeMessage;
import java.io.IOException;
import java.util.Date;
/**
* 消息接收者
*/
@Component
public class MailReceiver {
private static final Logger LOGGER = LoggerFactory.getLogger(MailReceiver.class);
@Autowired
private JavaMailSender javaMailSender;
@Autowired
private MailProperties mailProperties;
@Autowired
private TemplateEngine templateEngine;
@Autowired
private RedisTemplate redisTemplate;
@RabbitListener(queues = MailConstants.MAIL_QUEUE_NAME)
public void handler(Message message, Channel channel) {
Employee employee = (Employee) message.getPayload();
MessageHeaders headers = message.getHeaders();
//消息序号
long tag = (long) headers.get(AmqpHeaders.DELIVERY_TAG);
String msgId = (String) headers.get("spring_returned_message_correlation");//固定格式
HashOperations hashOperations = redisTemplate.opsForHash();
try {
if (hashOperations.entries("mail_log").containsKey(msgId)){
LOGGER.error("消息已经被消费=============>{}",msgId);
/**
* 手动确认消息
* tag:消息序号
* multiple:是否确认多条
*/
channel.basicAck(tag,false);
return;
}
MimeMessage msg = javaMailSender.createMimeMessage();
MimeMessageHelper helper = new MimeMessageHelper(msg);
//发件人
helper.setFrom(mailProperties.getUsername());
//收件人
helper.setTo(employee.getEmail());
//主题
helper.setSubject("入职欢迎邮件");
//发送日期
helper.setSentDate(new Date());
//邮件内容
Context context = new Context();
context.setVariable("name", employee.getName());
context.setVariable("posName", employee.getPosition().getName());
context.setVariable("joblevelName", employee.getJoblevel().getName());
context.setVariable("departmentName", employee.getDepartment().getName());
String mail = templateEngine.process("mail", context);
helper.setText(mail, true);
//发送邮件
javaMailSender.send(msg);
LOGGER.info("邮件发送成功");
//将消息id存入redis
hashOperations.put("mail_log", msgId, "OK");
//手动确认消息
channel.basicAck(tag, false);
} catch (Exception e) {
/**
* 手动确认消息
* tag:消息序号
* multiple:是否确认多条
* requeue:是否退回到队列
*/
try {
channel.basicNack(tag,false,true);
} catch (IOException ex) {
LOGGER.error("邮件发送失败=========>{}", e.getMessage());
}
LOGGER.error("邮件发送失败=========>{}", e.getMessage());
}
}
}
3.测试
可以看到第一条消息正常消费,第二条消息提示已经被消费