生产消息的可靠性之生产者消息消息确认

生产者的消息从发出到最终的队列要经过三个关卡,首先是消息到MQ,消息到MQ如果失败,那么该系统会抛出MQ的Expection,我们可以通过一些配置来然发送到MQ失败的消息再次发送一下,配置如下spring:
rabbitmq:
connection-timeout: 1s # 设置MQ的连接超时时间
template:
retry:
enabled: true # 开启超时重试机制
initial-interval: 1000ms # 失败后的初始等待时间
multiplier: 1 # 失败后下次的等待时长倍数,下次等待时长 = 上次等待时长 * multiplier
max-attempts: 3 # 总共尝试次数
接下来还有俩关,先是消息到达网关,每个消息都有自己的一个Confirm机制消息,正确到达交换机,返回ack。未到达交换机,返回nack。还有就是消息如果通过网关并未到达队列那么会触发Return机制,这个机制全局只有一个,当然开启这俩个机制也还需要一些配置,配置如下
spring:
rabbitmq:
publisher-confirm-type: correlated # 开启publisher confirm机制,并设置confirm类型
publisher-returns: true # 开启publisher return机制
接下来我们只需要通过写一些逻辑代码来显现当生产者消息确实无法发送成功时的落库操作。
首先我们来说return机制,因为它全局只有一个所以我们可以通过config配置来实现。
逻辑代码大概如下
@Configuration
@ConditionalOnProperty(prefix = "rabbit-mq", name = "enable", havingValue = "true")
@Import({RabbitClient.class, FailMsgDaoImpl.class})
@Slf4j
public class RabbitMqConfiguration implements ApplicationContextAware {

/**
 * 并发数量
 */
public static final int DEFAULT_CONCURRENT = 10;

@Autowired(required = false)
private FailMsgDao failMsgDao;



@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
    // 获取RabbitTemplate
    RabbitTemplate rabbitTemplate = applicationContext.getBean(RabbitTemplate.class);
    //定义returnCallback回调方法
    rabbitTemplate.setReturnsCallback(
            new RabbitTemplate.ReturnsCallback() {
                @Override
                public void returnedMessage(ReturnedMessage returnedMessage) {
                    byte[] body = returnedMessage.getMessage().getBody();
                    //消息id
                    String messageId = returnedMessage.getMessage().getMessageProperties().getMessageId();
                    String content = new String(body, Charset.defaultCharset());
                    log.info("消息发送失败,应答码{},原因{},交换机{},路由键{},消息id{},消息内容{}",
                            returnedMessage.getReplyCode(),
                            returnedMessage.getReplyText(),
                            returnedMessage.getExchange(),
                            returnedMessage.getRoutingKey(),
                            messageId,
                            content);
                    if (failMsgDao != null) {
                        failMsgDao.save(messageId, returnedMessage.getExchange(), returnedMessage.getRoutingKey(), content, 0, DateUtils.getCurrentTime()+10, "returnCallback");
                    }
                }
            }
    );
}

FailMsgDao 中封装了落库的实现方法。
接下来我们说说Confirm机制
逻辑代码大致如下
@Retryable(value = MqException.class, maxAttempts = 3, backoff = @Backoff(value = 3000, multiplier = 1.5), recover = "saveFailMag")
public void sendMsg(String exchange, String routingKey, Object msg, Integer delay, String msgId, boolean isFailMsg) {
// 1.发送消息前准备
// 1.1获取消息内容,如果非字符串将其序列化
String jsonMsg = JsonUtils.toJsonStr(msg);
// 1.2.全局唯一消息id,如果调用者设置了消息id,使用调用者消息id,如果为配置,默认雪花算法生成消息id
if(StrUtil.isBlank(msgId)){
msgId = IdUtil.getSnowflakeNextIdStr();
}
// 1.3.设置默认延迟时间,默认立即发送
delay = NumberUtils.null2Default(delay, -1);
log.debug("消息发送!exchange = {}, routingKey = {}, msg = {}, msgId = {}", exchange, routingKey, jsonMsg, msgId);

// 1.4.构建回调
RabbitMqListenableFutureCallback futureCallback = RabbitMqListenableFutureCallback.builder()
        .exchange(exchange)
        .routingKey(routingKey)
        .msg(jsonMsg)
        .msgId(msgId)
        .delay(delay)
        .isFailMsg(isFailMsg)
        .failMsgDao(failMsgDao)
        .build();
// 1.5.CorrelationData设置
CorrelationData correlationData = new CorrelationData(msgId.toString());
correlationData.getFuture().addCallback(futureCallback);

// 1.6.构造消息对象
Message message = MessageBuilder.withBody(StrUtil.bytes(jsonMsg, CharsetUtil.CHARSET_UTF_8))
        //持久化
        .setDeliveryMode(MessageDeliveryMode.PERSISTENT)
        //消息id
        .setMessageId(msgId.toString())
        .build();

try {
    // 2.发送消息
    this.rabbitTemplate.convertAndSend(exchange, routingKey, message, new DelayMessagePostProcessor(delay), correlationData);
} catch (Exception e) {
    log.error("send error:" + e);
    // 3.构建异常回调,并抛出异常
    MqException mqException = new MqException();
    mqException.setMsg(ExceptionUtil.getMessage(e));
    mqException.setMqId(msgId);
    throw mqException;
}

}

public class RabbitMqListenableFutureCallback implements ListenableFutureCallback<CorrelationData.Confirm> {

//记录失败消息service
private FailMsgDao failMsgDao;

private String exchange;
private String routingKey;
private String msg;
private String msgId;
private Integer delay;

//是否是失败消息
private boolean isFailMsg=false;

@Override
public void onFailure(Throwable ex) {
    if(failMsgDao == null) {
        return;
    }
    failMsgDao.save(msgId, exchange, routingKey, msg, delay, DateUtils.getCurrentTime() + 10, ExceptionUtil.getMessage(ex));
}

@Override
public void onSuccess(CorrelationData.Confirm result) {
    if(failMsgDao == null){
        return;
    }
    if(!result.isAck()){
        // 执行失败保存失败信息,如果已经存在保存信息,如果不在信息信息
        failMsgDao.save(msgId, exchange, routingKey, msg, delay,DateUtils.getCurrentTime() + 10, "MQ回复nack");
    }else if(isFailMsg && msgId != null){
        // 如果发送的是失败消息,当收到ack需要从fail_msg删除该消息
        failMsgDao.removeById(msgId);
    }
}

}
这段代码中将消息像交换机发送消息失败的情况与向MQ发送失败的情况都封装成立MQExpection最终在三次失败后会去完成落库操作。

posted @ 2025-03-22 11:09  人生何处不青山啊  阅读(18)  评论(0)    收藏  举报