一、实现方案
创建一张表记录消息发送的日志,消息发送时插入一条待发送的记录,如果消息发送成功则更新消息状态。发送失败时更新记录表的重发次数,达到重发次数则认定消息发送失败,使用定时任务扫描发出消息一段时间仍未待发送状态的消息,从而避免消息非人为编码原因导致中间件不可用。
二、创建表 和 对应实体,定义实体静态属性
CREATE TABLE `send_log` (
`msgId` varchar(255) DEFAULT NULL,
`serviceId` int(11) DEFAULT NULL COMMENT '业务id',
`status` int(11) DEFAULT '0' COMMENT '0发送中,1发送成功,2发送失败',
`routeKey` varchar(255) DEFAULT NULL COMMENT '绑定交换机和队列的key',
`exchange` varchar(255) DEFAULT NULL COMMENT '交换机名称',
`count` int(11) DEFAULT NULL COMMENT '重试次数',
`tryTime` date DEFAULT NULL COMMENT '第一次重试时间',
`createTime` date DEFAULT NULL,
`updateTime` date DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
private String msgId;
private Integer serviceId;
//0 消息投递中 1 投递成功 2投递失败
private Integer status;
private String routeKey;
private String exchange;
private Integer count;
private Date tryTime;
private Date createTime;
private Date updateTime;
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 QUEUE_NAME = "xiaoxu.queue";
public static final String EXCHANGE_NAME = "xiaoxu.exchange";
public static final String ROUTING_KEY_NAME = "xiaoxu.routing.key";
三、自定义消息发送模板
package com.dong.boot.config;
@Configuration
public class SendLogConfig {
private Logger log = LoggerFactory.getLogger(SendLogConfig.class);
@Autowired
CachingConnectionFactory cachingConnectionFactory;
@Autowired
SendLogService sendLogService;
@Bean
RabbitTemplate rabbitTemplate(){
RabbitTemplate rabbitTemplate = new RabbitTemplate(cachingConnectionFactory);
//消息发送成功 只能保证消息投递到broker
rabbitTemplate.setConfirmCallback((data, ack, cause) -> { //data保存发送的唯一id ack表示消息有没有 发送成功
if (ack){
sendLogService.updateSendLogStatus(data.getId(),1);
log.info("消息发送成功");
}else{
log.info("消息发送失败");
}
});
//交换机往队列投递失败
rabbitTemplate.setReturnsCallback((returnCallBack)->{
log.info("消息发送失败");
});
return rabbitTemplate;
}
//创建队列
@Bean
Queue queue(){
return new Queue(Constants.QUEUE_NAME,true);
}
//创建交换机
@Bean
DirectExchange directExchange(){
return new DirectExchange(Constants.EXCHANGE_NAME,true,false);
}
//交换机和队列通过routingKey绑定
@Bean
Binding bindingBuilder(){
return BindingBuilder.bind(queue()).to(directExchange()).with(Constants.ROUTING_KEY_NAME);
}
}
四、配置开启消息确认和交换机投递失败回调
spring.rabbitmq.publisher-confirm-type=correlated
#之前版本的写法是 spring.rabbitmq.publisher-confirms=true 已废弃
spring.rabbitmq.publisher-returns=true
五、消息发送时的逻辑
SendLog sendLog =new SendLog();
//定时消息唯一id
String msgId = UUID.randomUUID().toString().replace("-", "");
sendLog.setMsgId(msgId);
//对应业务id
sendLog.setServiceId(service.getId());
sendLog.setCount(Constants.MAX_TRY_COUNT);
sendLog.setCreateTime(new Date());
sendLog.setStatus(Constants.DELIVERING);
// 配置在消息发送多长时间仍未发送状态才执行重发,如果消息发送时立刻进行扫描,就会立即重发,不合适
sendLog.setTryTime(new Date(System.currentTimeMillis() + 1000 * 60 * MailConstants.MSG_TIMEOUT));
sendLog.setExchange(Constants.MAIL_EXCHANGE_NAME);
sendLog.setRouteKey(Constants.MAIL_ROUTING_KEY_NAME);
sendLog.setUpdateTime(new Date());
SendLogService.insert(SendLog);
rabbitTemplate.convertAndSend(Constants.EXCHANGE_NAME,Constants.ROUTING_KEY_NAME,service,new CorrelationData(msgId));
六、定时任务扫描需要重发的记录
@Autowired
SendLogService sendLogService;
@Autowired
RabbitTemplate rabbitTemplate;
@Autowired
Service service;
@Scheduled(cron = "0/30 * * * * ?" )
public void mailTask(){
//查询出需要处理的数据
List<SendLog> sendLogList = sendLogService.getSendLogStatus();
for (SendLog sendLog:sendLogList){
//如果到达最大的配置次数仍发出失败,则不再重试
if (sendLog.getCount() >= MailConstants.MAX_TRY_COUNT){
mailSendLogService.updateSendLogStatus(sendLog.getMsgId(),2);
}else {
//重试发送 次数加一
sendLogService.updateCount(sendLog.getMsgId(),new Date());
Service service=service.getServiceById(sendLog.getServiceId());
rabbitTemplate.convertAndSend(Constants.EXCHANGE_NAME,Constants.ROUTING_KEY_NAME,service,new CorrelationData(sendLog.getMsgId()));
}
}
}
七、持久层实现
Integer updateSendLogStatus(@Param("msgId") String msgId, @Param("status") Integer status);
Integer insert(SendLog sendLog);
List<MailSendLog> getSendLogsByStatus();
Integer updateCount(@Param("msgId") String msgId, @Param("date") Date date);
<update id="updateSendLogStatus">
update send_log set status = #{status} where msgId=#{msgId};
</update>
<insert id="insert" parameterType="com.dong.boot.model.SendLog">
insert into send_log (msgId,serviceId,routeKey,exchange,tryTime,createTime) values (#{msgId},#{serviceId},#{routeKey},#{exchange},#{tryTime},#{createTime});
</insert>
<select id="getSendLogsByStatus" resultType="com.dong.boot.model.SendLog">
select * from send_log where status=0 and tryTime < now()
</select>
<update id="updateCount">
update send_log set count=count+1,updateTime=#{date} where msgId=#{msgId};
</update>