图片来源:
作者:不喝奶茶的Programmer
链接:juejin.cn/post/699797…
来源:稀土掘金\
Demo
创建数据表msg_log
create table msg_log
(
msg_id varchar(255) default '' not null comment '消息唯一标识',
msg text null comment '消息体',
exchange varchar(255) default '' not null comment '交换机',
routing_key varchar(255) default '' not null comment '路由键',
status int default 0 not null comment '状态: 0投递中 1投递成功 2投递失败 3已消费',
try_count int default 0 not null comment '重试次数',
next_try_time datetime null comment '下一次重试时间',
create_time datetime null comment '创建时间',
update_time datetime null comment '更新时间',
constraint unq_msg_id
unique (msg_id)
)
comment '消息投递日志' charset = utf8mb4;
alter table msg_log
add primary key (msg_id);
POJO
@Getter
@Setter
@TableName("msg_log")
public class MsgLog implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 消息唯一标识
*/
@TableId
private String msgId;
/**
* 消息体, json格式化
*/
private String msg;
/**
* 交换机
*/
private String exchange;
/**
* 路由键
*/
private String routingKey;
/**
* 状态: 0投递中 1投递成功 2投递失败 3已消费
*/
private Integer status;
/**
* 重试次数
*/
private Integer tryCount;
/**
* 下一次重试时间
*/
private LocalDateTime nextTryTime;
/**
* 创建时间
*/
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;
/**
* 更新时间
*/
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updateTime;
}
消费者
@Component
@Slf4j
public class Consumer {
@Autowired
private IMsgLogService msgLogService;
@RabbitListener(queues = RabbitmqConfig.DIRECT_QUEUE)
public void consume(Message message, Channel channel) throws IOException {
log.info("收到消息: {}", message.toString());
MessageProperties properties = message.getMessageProperties();
long tag = properties.getDeliveryTag();
//业务逻辑是否成功
boolean success = true;
if (success) {
//更新消息状态为"消费成功"
log.info("收到消息ID: {}", getCorrelationId(message));
MsgLog entity = MsgLog.builder().status(MsgConstant.CONSUMED_SUCCESS)
.msgId(getCorrelationId(message))
.build();
msgLogService.saveOrUpdate(entity);
// 消费确认
channel.basicAck(tag, false);
} else {
//true 表示重试投递
channel.basicNack(tag, false, true);
}
}
private String getCorrelationId(Message message) {
String correlationId = null;
MessageProperties properties = message.getMessageProperties();
Map<String, Object> headers = properties.getHeaders();
for (String key : headers.keySet()) {
if (key.equals("spring_returned_message_correlation")) {
correlationId = (String) headers.get(key);
}
}
return correlationId;
}
}
发送消息
@GetMapping("/test")
@SysLog
@ApiOperation("测试")
public ServerResponse testPost(String msg){
String msgId = RandomUtil.randomUUID();
MsgLog msgLog = MsgLog.builder().msgId(msgId)
.msg(msg)
.exchange(RabbitmqConfig.DIRECT_EXCHANGE)
.routingKey(RabbitmqConfig.DIRECT_ROUTING_KEY)
.build();
// 第一次消息入库,并发送消息
msgLogService.save(msgLog);
CorrelationData correlationData = new CorrelationData(msgId);
rabbitTemplate.convertAndSend(RabbitmqConfig.DIRECT_EXCHANGE, RabbitmqConfig.DIRECT_ROUTING_KEY, msg, correlationData);// 发送消息
return ServerResponse.success("成功");
}
定时任务补偿机制
@Configuration
@EnableScheduling
@Slf4j
public class resendTask {
@Autowired
private IMsgLogService msgLogService;
@Autowired
private RabbitTemplate rabbitTemplate;
//每10S调用一次
@Scheduled(cron = "0/10 * * * * ?")
public void retryTask(){
//从数据库中找到status为 0消息投递中 的消息,重试
log.info("执行任务的时间------" + LocalDateTime.now());
List<MsgLog> msgLogs = msgLogService.list(new QueryWrapper<MsgLog>()
.eq("status",0)
.lt("next_try_time", LocalDateTime.now()));
msgLogs.forEach(e->{
if (e.getTryCount() >= 3){
log.info("投递失败的id号为:{}",e.getMsgId());
//如果投递超过3次,则算作投递失败
msgLogService.update(new UpdateWrapper<MsgLog>()
.eq("msg_id",e.getMsgId())
.set("status",2)
.set("next_try_time", LocalDateTime.now())
.set("update_time",LocalDateTime.now()));
}else {
//重新发送
log.info("重新投递的id号为:{}",e.getMsgId());
rabbitTemplate.convertAndSend(RabbitmqConfig.DIRECT_EXCHANGE,RabbitmqConfig.DIRECT_ROUTING_KEY,e.getMsg(),
new CorrelationData(e.getMsgId()));
//更新数据库,重试+1,并更新重试时间 +1分钟
msgLogService.update(new UpdateWrapper<MsgLog>()
.eq("msg_id",e.getMsgId())
.set("update_time",LocalDateTime.now())
.set("try_count",e.getTryCount() + 1)
.set("next_try_time",LocalDateTime.now().plusMinutes(1)));
}
});
}
}
消费成功,并且数据库状态更新成“已消费”
验证消息确认机制
生产者 --> Broker失败
将交换机名称修改为"12",发送请求,因为交换机不存在,所以控制台输出如下,ACK为false。
当投递失败时,定时任务执行正确的发送流程,这时消息发送成功
Exchange --> Queue失败
把路由key修改成一个不存在key
通过定时任务重新发送
过程中发现一个问题,会先回调ReturnCallback再回调ConfirmCallback
目前想到的解决方法是confirmcallback中加入状态判断
//回调会先执行ReturnCallback,这时判断一下状态,如果是4路由失败的话,就不做更新了
if (msgLogService.getById(correlationData.getId()).getStatus() != 4){
msgLogService.saveOrUpdate(msgLogEntity);
}