业务背景
当添加一名员工时,给其发送入职欢迎邮件,但在复杂环境下,如何保障消息的可靠性?本章将采取一定的技术手段去处理
解决思路
- 添加员工时,把消息的消费情况记录到mail_send_log表中
- 开启消息发送失败回调,路由失败回调
- 开启定时任务巡查,发现有发送失败的消息自动重新投递
实体类
@Data
public class MailSendLog {
private String msgId;
/**
* 员工id
*/
private Integer empId;
/**
* 0 消息投递中 1 投递成功 2投递失败
*/
private Integer status;
private String routeKey;
private String exchange;
/**
* 重试次数
*/
private Integer count;
/**
* 第一次重试时间
*/
private Date tryTime;
private Date createTime;
private Date updateTime;
}
自定义 RabbitTemplate
RabbitMQ回调函数可参考: https://www.cnblogs.com/wangiqngpei557/p/9381478.html
@Slf4j
@Configuration
public class RabbitConfig {
@Autowired
CachingConnectionFactory cachingConnectionFactory;
@Resource
MailSendLogService mailSendLogService;
@Bean
RabbitTemplate rabbitTemplate() {
RabbitTemplate rabbitTemplate = new RabbitTemplate(cachingConnectionFactory);
/**
* 详解: https://www.cnblogs.com/wangiqngpei557/p/9381478.html
*/
//message 从 producer 到 rabbitmq broker cluster 则会返回一个 confirmCallback
rabbitTemplate.setConfirmCallback((data, ack, cause) -> {
String msgId = data.getId();
if (ack) {
log.info(msgId + ":消息发送成功");
log.info("cause:{}", cause);
mailSendLogService.updateMailSendLogStatus(msgId, 1);//修改数据库中的记录,消息投递成功
} else {
log.info(msgId + ":消息发送失败");
}
});
//message 从 exchange->queue 投递失败则会返回一个 returnCallback
rabbitTemplate.setReturnCallback((msg, repCode, repText, exchange, routingkey) -> {
log.info("消息发送失败");
});
return rabbitTemplate;
}
@Bean
Queue mailQueue() {
return new Queue(MailConstants.MAIL_QUEUE_NAME, true);
}
@Bean
DirectExchange mailExchange() {
return new DirectExchange(MailConstants.MAIL_EXCHANGE_NAME, true, false);
}
@Bean
Binding mailBinding() {
return BindingBuilder.bind(mailQueue()).to(mailExchange()).with(MailConstants.MAIL_ROUTING_KEY_NAME);
}
}
注意开启ConfirmCallback和ReturnCallback回调,需要在application.properities添加配置如下
spring.rabbitmq.publisher-confirms=true
spring.rabbitmq.publisher-returns=true
定时任务
定时任务每隔10秒去扫描mail_send_log,把投递未成功的消息进行重新投递,投递最大重试次数为三次
@Component
@Slf4j
public class MailSendTask {
@Resource
MailSendLogService mailSendLogService;
@Autowired
RabbitTemplate rabbitTemplate;
@Autowired
EmployeeService employeeService;
@Scheduled(cron = "0/10 * * * * ?") //10秒
public void mailResendTask() {
log.info("进入定时任务");
List<MailSendLog> logs = mailSendLogService.getMailSendLogsByStatus();
if (logs == null || logs.size() == 0) {
return;
}
logs.forEach(mailSendLog -> {
if (mailSendLog.getCount() >= 3) { //最大重试次数为三次
mailSendLogService.updateMailSendLogStatus(mailSendLog.getMsgId(), 2);//直接设置该条消息发送失败
} else {
mailSendLogService.updateCount(mailSendLog.getMsgId(), new Date());
Employee emp = employeeService.getEmployeeById(mailSendLog.getEmpId());
//重试
rabbitTemplate.convertAndSend(MailConstants.MAIL_EXCHANGE_NAME, MailConstants.MAIL_ROUTING_KEY_NAME, emp, new CorrelationData(mailSendLog.getMsgId()));
}
});
}
}
业务层
发送消息前把消费情况记录到mail_send_log
public int addEmp(Employee employee) {
Date beginContract = employee.getBeginContract();
Date endContract = employee.getEndContract();
double month = (Double.parseDouble(yearFormat.format(endContract)) - Double.parseDouble(yearFormat.format(beginContract))) * 12 + (Double.parseDouble(monthFormat.format(endContract)) - Double.parseDouble(monthFormat.format(beginContract)));
employee.setContractTerm(Double.parseDouble(decimalFormat.format(month / 12)));
int result = employeeMapper.insertSelective(employee);
//如果添加成功则发送mq消息
if (result==1) {
Employee emp = employeeMapper.getEmployeeById(employee.getId());
//生成消息的唯一id
String msgId = UUID.randomUUID().toString();
MailSendLog mailSendLog = new MailSendLog();
mailSendLog.setMsgId(msgId);
mailSendLog.setCreateTime(new Date());
mailSendLog.setExchange(MailConstants.MAIL_EXCHANGE_NAME);
mailSendLog.setRouteKey(MailConstants.MAIL_ROUTING_KEY_NAME);
mailSendLog.setEmpId(emp.getId());
//第一次重试时间设置为一分钟后
mailSendLog.setTryTime(new Date(System.currentTimeMillis() + 1000 * 60 * MailConstants.MSG_TIMEOUT));
mailSendLogService.insert(mailSendLog);
rabbitTemplate.convertAndSend(MailConstants.MAIL_EXCHANGE_NAME, MailConstants.MAIL_ROUTING_KEY_NAME, emp, new CorrelationData(msgId));
}
return result;
}
详细代码请参看:https://github.com/lenve/vhr