1 图示:
2 数据库表
CREATE TABLE `msg_log` (
`msg_id` varchar(255) NOT NULL DEFAULT '' COMMENT '消息唯一标识',
`msg` text COMMENT '消息体, json格式化',
`exchange` varchar(255) NOT NULL DEFAULT '' COMMENT '交换机',
`routing_key` varchar(255) NOT NULL DEFAULT '' COMMENT '路由键',
`status` int(11) NOT NULL DEFAULT '0' COMMENT '状态: 0投递中 1投递成功 2投递失败 3已消费',
`try_count` int(11) NOT NULL DEFAULT '0' COMMENT '重试次数',
`next_try_time` datetime DEFAULT NULL COMMENT '下一次重试时间',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
PRIMARY KEY (`msg_id`),
UNIQUE KEY `unq_msg_id` (`msg_id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='消息投递日志';
3 代码的编写
3.1消息发送前,在数据库中添加一条记录
package fastwave.cloud.demo.fastwavebizpublisher.controller;
import com.alibaba.fastjson.JSON;
import fastwave.cloud.demo.fastwavebizpublisher.domain.MsgLogDO;
import fastwave.cloud.demo.fastwavebizpublisher.services.MsgLogService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.core.MessageProperties;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import java.util.Date;
import java.util.Map;
import java.util.UUID;
@RestController
@RequestMapping("amqp")
public class AmqpController {
@Autowired
RabbitTemplate template;
@Autowired
MsgLogService msgLogService;
@Resource(name = "TemplateReliable")
private RabbitTemplate templateReliable;
private static Logger logger = LoggerFactory.getLogger(AmqpController.class);
@GetMapping("/reliable")
public String reliable(@RequestParam Map<String, Object> params)
{
try
{
String msg = JSON.toJSONString(params);
String uuid = UUID.randomUUID().toString();
Date curTime = new Date();
MsgLogDO msgLogDO = new MsgLogDO();
msgLogDO.setMsgId(uuid);
msgLogDO.setMsg(msg);
msgLogDO.setExchange("EmailExchange");
msgLogDO.setRoutingKey("EmailRouting");
msgLogDO.setStatus(-1);
msgLogDO.setTryCount(0);
msgLogDO.setCreateTime(curTime);
msgLogDO.setUpdateTime(curTime);
msgLogDO.setCreateTime(curTime);
msgLogService.save(msgLogDO);
templateReliable.convertAndSend("EmailExchange", "EmailRouting", msg, new CorrelationData(uuid));
return "已成功发送到broker";
}
catch (Exception e)
{
logger.error(e.getMessage());
return "出现异常,请稍后重试";
}
}
}
3.2编写配置类
1 消息发送到exchange。无论有没有找到交换机。都会执行回调函数。setConfirmCallback。
如果找到,跟新数据库中的status为1 (ack为true )
如果没有找到不进行处理.(ack为false )
其实这么写有漏洞。找到交换机 ,不代表正确的路由到了队列当中。但是此时数据库中的status已经更改为1(投递成功了) 。
这种找到交换机但是没有路由到队列中的错误几率不大。可能是业务代码出现了问题,或者配置时,初始化路由,队列有问题。
为了防止可能是路由不到。可以由setReturnCallback兜底。往数据库中添加一条记录,status设置为4. 设置为4 就需要运维人员进行进行人为的处理数据。
package fastwave.cloud.demo.fastwavebizpublisher.config;
import fastwave.cloud.demo.fastwavebizpublisher.domain.MsgLogDO;
import fastwave.cloud.demo.fastwavebizpublisher.services.MsgLogService;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.connection.ConnectionFactory;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.amqp.support.converter.SerializerMessageConverter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Scope;
import java.util.Date;
import java.util.UUID;
@Configuration
public class TemplateReliableConfig {
@Autowired
MsgLogService msgLogService;
// @Scope("prototype")
@Bean(name = "TemplateReliable")
public RabbitTemplate rabbitTemplate(ConnectionFactory factory) {
RabbitTemplate template = new RabbitTemplate(factory);
template.setMandatory(true);
template.setMessageConverter(new SerializerMessageConverter());
template.setConfirmCallback((CorrelationData correlationData, boolean ack, String cause) -> {
if(ack){
MsgLogDO msgLogDO = new MsgLogDO();
msgLogDO.setMsgId(correlationData.getId());
msgLogDO.setStatus(1);
msgLogService.update(msgLogDO);
}
});
template.setReturnCallback(new RabbitTemplate.ReturnCallback() {
@Override
public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
Date curTime = new Date();
MsgLogDO msgLogDO = new MsgLogDO();
msgLogDO.setMsgId(UUID.randomUUID().toString());
msgLogDO.setMsg(message.toString());
msgLogDO.setExchange(exchange);
msgLogDO.setRoutingKey(routingKey);
msgLogDO.setStatus(4);
msgLogDO.setTryCount(0);
msgLogDO.setCreateTime(curTime);
msgLogDO.setUpdateTime(curTime);
msgLogDO.setCreateTime(curTime);
msgLogService.save(msgLogDO);
}
});
return template;
}
}
3.3 补偿机制代码的编写
3.3.1代码的使用场景
有些状况下,并没有发送成功,需要我们使用补偿机制重新在发送消息,来保证消息的百分之分百发送成功。
什么情况下会发送不成功那??
在发送消息之前的代码(即添加日志记录时发生错误):因为try catch ,可以根据日志查看错误原因。当添加日志记录成功 ,此时status为-1.
1 发送消息时没有连接到mq ,
也会try catch 捕获。
补偿机制就会起作用,因为状态仍然是-1.
2 消息发送到exchange。但是exchange有可能不存在,
无论有没有找到交换机。都会执行回调函数。setConfirmCallback。
如果找到,跟新数据库中的status为1 (ack为true )
如果没有找到不进行处理.(ack为false )补偿机制就会起作用因为状态仍然是-1.
3.3.2代码的编写
package fastwave.cloud.demo.fastwavebizpublisher.job;
import fastwave.cloud.demo.fastwavebizpublisher.domain.MsgLogDO;
import fastwave.cloud.demo.fastwavebizpublisher.services.MsgLogService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@Component
@EnableScheduling
public class MQMessageJob {
@Autowired
private MsgLogService msgLogService;
@Resource(name = "TemplateReliable")
private RabbitTemplate reliableTemplate;
private static Logger logger = LoggerFactory.getLogger(MQMessageJob.class);
//定时扫描记录表,将发送状态为-1且未超重发次数的消息再次发送,超过重发次数,必要时人工干预,生产环境中可以单独部署定时任务
@Scheduled(cron ="10/10 * * * * ?" )
public void scanNoConfirmMsg(){
Map<String, Object> searchParams = new HashMap<String, Object>();
searchParams.put("status", -1);
searchParams.put("tryCount", 3);
try {
List<MsgLogDO> list = msgLogService.list(searchParams);
for(MsgLogDO item : list)
{
item.setTryCount(item.getTryCount() + 1);
msgLogService.update(item);
reliableTemplate.convertAndSend(item.getExchange(), item.getRoutingKey(), item.getMsg(), new CorrelationData(item.getMsgId()));
}
}
catch(Exception e)
{
logger.error("扫描作业出错,信息:" + e.getMessage());
}
}
}
4 broker接受到消息,但是由于网络原因,并没有接受到回调(没有到回调的参数)。存在一个幂等性问题。broker收到了,但仍然会进行补偿机制。就会传递至少两次到broker。