RabbitMq使用改进 邮件服务 保证消息可靠性

消息落库,对消息状态进行打标

在这里插入图片描述
实现流程

  1. 发送消息时,将当前消息数据存入数据库,投递状态为消息投递中
  2. 开启消息确认回调机制。确认成功,更新投递状态为消息投递成功
  3. 开启定时任务,重新投递失败的消息。重试超过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.测试

可以看到第一条消息正常消费,第二条消息提示已经被消费
在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值