消息投递案例
这里以推送文档到es,做同步更新索引为例
增加配置
rabbitmq:
# 使confirmCallback回调生效
publisher-confirm-type: correlated
# 使returnsCallback回调生效
publisher-returns: true
# 如果配置为false,交换机没有匹配到队列就会丢弃掉消息,而不会触发returnCallback的回调
template:
mandatory: true
创建消息体
/**
* <p>
* 消息体
* </p>
*
* @author strap
*/
@Data
@Accessors(chain = true)
@AllArgsConstructor
@NoArgsConstructor
public class DocMessage implements Serializable {
private static final long serialVersionUID = 2136809013109440975L;
private String messageId;
/**
* 文档主键
*/
private Long documentId;
/**
* 文档存放的路径
*/
private String filePath;
}
创建消息日志类
/**
* <p>
*
* </p>
*
* @author strap
*/
@TableName("broker_message_log")
@ApiModel(value = "BrokerMessageLog对象", description = "消息记录")
@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
public class BrokerMessageLog extends Model<BrokerMessageLog> implements Serializable {
private static final long serialVersionUID = 1L;
@TableId(type = IdType.INPUT)
private String messageId;
private String message;
private Integer tryCount;
private Integer state;
private LocalDateTime nextRetry;
private LocalDateTime createTime;
private LocalDateTime updateTime;
}
创建历史消息记录类
成功处理完的记录,从消息记录表删除,添加到历史记录表
/**
* <p>
* 已经成功消费的消息转到此处存放,即state=1
* </p>
*
* @author strap
*/
@TableName("broker_message_log_history")
@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
public class BrokerMessageLogHistory extends BrokerMessageLog implements Serializable {
private static final long serialVersionUID = 4922336781826093421L;
@TableId(type = IdType.NONE)
private String messageId;
}
生产者代码
/**
* <p>生产者</p>
*
* @author strap
*/
@Component
@Log4j
public class MqProducer {
@Resource
private RabbitTemplate rabbitTemplate;
@Resource
private BrokerMessageLogMapper brokerMessageLogMapper;
public MqProducer() {
}
public void sendOrder(DocMessage docMessage) throws Exception {
// 消息投递到交换机,不管是否成功投递到交换机都回调,成功投递到交换机则ack=true
rabbitTemplate.setConfirmCallback(
(correlationData, ack, cause) -> {
if (ack) {
log.info("消息已成功投递到了交换机,但不知道是否路由到队列");
} else {
log.info("消息未投递到交换机");
new BrokerMessageLog()
.setState(4) //消息未投递到交换机
.setUpdateTime(LocalDateTimeUtil.now())
.update(
Wrappers.<BrokerMessageLog>lambdaUpdate()
.set(
BrokerMessageLog::getNextRetry, null
).eq(
BrokerMessageLog::getMessageId, correlationData.getId()
)
);
}
}
);
// 消息从交换机分配到队列时失败才回调,假设连交换机都找不到(即只会触发ConfirmCallback返回nack),ReturnsCallback不会触发
rabbitTemplate.setReturnsCallback(
returnedMessage -> {
log.info("消息投递到队列失败,如可能匹配不到路由键");
String messageId = returnedMessage.getMessage().getMessageProperties().getCorrelationId();
new BrokerMessageLog()
.setState(5) //消息投递到队列失败
.setUpdateTime(LocalDateTimeUtil.now())
.update(
Wrappers.<BrokerMessageLog>lambdaUpdate()
.set(
BrokerMessageLog::getNextRetry, null
).eq(
BrokerMessageLog::getMessageId, messageId
)
);
}
);
CorrelationData correlationData = new CorrelationData(docMessage.getMessageId());
rabbitTemplate.convertAndSend(MyDemoConstants.MQConstants.EXCHANGE_NAME, MyDemoConstants.MQConstants.ROUTING_KEY_NAME, docMessage, correlationData);
}
}
消费者代码
/**
* <p>消费者</p>
*
* @author strap
*/
@Component
@Log4j
public class MqConsumer {
@Resource
private DocMessageService docMessageService;
public MqConsumer() {
}
@RabbitListener(
bindings = @QueueBinding(
value = @Queue(value = MyDemoConstants.MQConstants.QUEUE_NAME, durable = "true",
arguments = {
@Argument(name = MyDemoConstants.MQConstants.DEAD_EXCHANGE_LABEL, value = MyDemoConstants.MQConstants.DEAD_EXCHANGE),
@Argument(name = MyDemoConstants.MQConstants.DEAD_ROUTING_KEY_LABEL, value = MyDemoConstants.MQConstants.DEAD_ROUTING_KEY)
}),
exchange = @Exchange(name = MyDemoConstants.MQConstants.EXCHANGE_NAME, type = "topic"),
key = MyDemoConstants.MQConstants.ROUTING_KEY_NAME
)
)
@RabbitHandler
public void consume(@Payload DocMessage docMessage, @Headers Map<String, Object> headers, Channel channel) throws Exception {
log.info("消费者已拿到消息:" + docMessage);
// 消息的下标index,批量确认消息:即确认所有下标小于该值的消息
Long index = Convert.toLong(headers.get(AmqpHeaders.DELIVERY_TAG));
// 是否是重复消费
Boolean isRedeliver = Convert.toBool(headers.get(AmqpHeaders.REDELIVERED));
if (docMessageService.runTask(docMessage)) {
// 业务执行成功, 确认消息,但不批量确认
channel.basicAck(index, false);
log.info("messageId:" + docMessage.getMessageId() + ", 成功被消费");
} else if (!isRedeliver) {
// 不是重复消费,及业务处理未完成,不批量确认,告知MQ重发
channel.basicNack(index, false, true);
log.info("messageId:" + docMessage.getMessageId() + ", 被重新投递");
} else {
channel.basicNack(index, false, false);
log.info("messageId:" + docMessage.getMessageId() + ", 进入死信队列");
}
}
/**
* 处理死信消息
*/
@RabbitListener(
bindings = @QueueBinding(
value = @Queue(value = MyDemoConstants.MQConstants.DEAD_QUEUE, durable = "true"),
exchange = @Exchange(name = MyDemoConstants.MQConstants.DEAD_EXCHANGE, type = "topic"),
key = MyDemoConstants.MQConstants.DEAD_ROUTING_KEY
)
)
@RabbitHandler
public void consumeDeadLetter(@Payload DocMessage docMessage, @Headers Map<String, Object> headers, Channel channel) throws Exception {
// 消息的下标index,批量确认消息:即确认所有下标小于该值的消息
Long index = Convert.toLong(headers.get(AmqpHeaders.DELIVERY_TAG));
channel.basicAck(index, false);
log.info("死信消息已经被处理:" + docMessage.getMessageId());
new BrokerMessageLog()
.setState(3) //死信消息
.setUpdateTime(LocalDateTimeUtil.now())
.update(
Wrappers.<BrokerMessageLog>lambdaUpdate()
.set(
BrokerMessageLog::getNextRetry, null
).eq(
BrokerMessageLog::getMessageId, docMessage.getMessageId()
)
);
}
}
业务处理类
/**
* <p>
* 服务实现类
* </p>
*
* @author strap
*/
@Service
@Log4j
public class DocMessageService {
@Resource
private MqProducer mqProducer;
@Resource
private BrokerMessageLogMapper brokerMessageLogMapper;
@Resource
private BrokerMessageLogHistoryMapper brokerMessageLogHistoryMapper;
public void updateDoc(DocMessage docMessage) throws Exception {
LocalDateTime now = LocalDateTimeUtil.now();
boolean add = new BrokerMessageLog()
.setMessageId(docMessage.getMessageId())
.setMessage(JSONUtil.toJsonStr(docMessage))
.setState(0)
.setCreateTime(now)
.setTryCount(1)
.setUpdateTime(now)
.setNextRetry(now.plus(3, ChronoUnit.MINUTES))// 3分钟后如果状态还是0就取出来重试
.insert();
if (add) {
mqProducer.sendOrder(docMessage);
}
}
// 模拟处理业务,处理成功返回true
@Transactional(rollbackFor = Exception.class)
public boolean runTask(DocMessage docMessage) throws Exception {
BrokerMessageLog brokerMessageLog = brokerMessageLogMapper.selectById(docMessage.getMessageId());
if (brokerMessageLog == null) {
return true;
}
String filePath = docMessage.getFilePath();
log.info("filePath:" + filePath);
// 取路径读取文档获取请求es进行索引更新,这里模拟业务处理 TODO
boolean success = filePath.length() > 3;
if (success) {
// 修改状态并存入历史消息
BrokerMessageLogHistory logHistory = BeanUtil.copyProperties(brokerMessageLog, BrokerMessageLogHistory.class);
logHistory.setNextRetry(null).setState(1).setUpdateTime(LocalDateTimeUtil.now());
brokerMessageLogHistoryMapper.insert(logHistory);
brokerMessageLog.deleteById();
}
return success;
}
}
定时任务处理类
/**
* <p></p>
*
* @author strap
*/
@Log4j
@ConditionalOnExpression("#{T(cn.hutool.core.util.StrUtil).isNotEmpty(\"${cron.job1}\")}")
@Component
public class MySpringQuartzJob {
@Resource
private BrokerMessageLogMapper brokerMessageLogMapper;
@Resource
private MqProducer mqProducer;
public MySpringQuartzJob() {
}
@Scheduled(cron = "${cron.job1}")
public void run() throws Exception {
log.info("定时任务开始执行");
Page<BrokerMessageLog> records = brokerMessageLogMapper.selectPage(
new Page<>(),
Wrappers.<BrokerMessageLog>lambdaQuery()
.eq(BrokerMessageLog::getState, 0)
.lt(BrokerMessageLog::getNextRetry, LocalDateTimeUtil.now())
);
for (BrokerMessageLog record : records.getRecords()) {
if (record.getTryCount() > 2) {
// 已尝试三次,不需要再重发
new BrokerMessageLog()
.setState(2) // 重试多次仍然失败,不再重发
.update(
Wrappers.<BrokerMessageLog>lambdaUpdate()
.set(
BrokerMessageLog::getNextRetry, null
).eq(
BrokerMessageLog::getMessageId, record.getMessageId()
)
);
} else {
new BrokerMessageLog()
.setMessageId(record.getMessageId())
.setTryCount(record.getTryCount() + 1)
.setUpdateTime(LocalDateTimeUtil.now())
.updateById();
mqProducer.sendOrder(JSONUtil.toBean(record.getMessage(), DocMessage.class));
}
}
}
}
业务触发
@GetMapping("/updateDoc")
public void updateDoc() throws Exception {
docMessageService.updateDoc(
new DocMessage(
UUID.randomUUID().toString(true),
10251L,
"/article/20220322/xxx.doc"
)
);
}