项目中的rabbitmq开启了手动确认机制,然而在手动确认机制的情况下,如果业务代码发生了异常, 则会一直重试, 类似于鬼畜
消息手动确认并实现消息重试:
定义一个切面类:
@Aspect
@Component
@Slf4j
public class MessageProcessAspect {
@Pointcut(value = "@annotation(org.springframework.amqp.rabbit.annotation.RabbitListener) && args(message, channel)")
public void pointcut(Message message, Channel channel){};
}
该切面将会增强带有RabbitListener注解并且参数为Message和Channel的方法, 其余的不会增强
在定义一个自定义异常:
public class MessageRetryException extends RuntimeException {
public MessageRetryException() {
}
public MessageRetryException(String message) {
super(message);
}
}
再切面中增加环绕通知:
@Around(value = "pointcut(message, channel)")
public Object aroundAdvice(ProceedingJoinPoint pjp, Message message, Channel channel) throws Throwable {
long deliveryTag = message.getMessageProperties().getDeliveryTag();
Object result = null;
try{
result = pjp.proceed();
channel.basicAck(deliveryTag, false);
} catch (MessageRetryException e) {
//如果再业务代码中手动抛出了MessageRetryException, 则表明该消息需要进行重试
manualRetry(message, channel, pjp);
} catch (Exception e) {
//如果捕获到了其他的异常, 则表明该条信息即便是重试了也不行,因此直接nack
//手动nack 告诉rabbitmq该消息消费失败 第三个参数:如果被拒绝的消息应该被重新请求,而不是被丢弃或变成死信,则为true
try {
channel.basicNack(deliveryTag, false, false);
} catch (IOException ex) {
ex.printStackTrace();
}
}
return result;
}
补全 manualRetry方法,进行消息重试
private String getInterceptedName(ProceedingJoinPoint joinPoint){
// 获取Signature对象,它包含了方法的元数据
Signature signature = joinPoint.getSignature();
// 检查Signature对象是否确实是一个MethodSignature
if (signature instanceof MethodSignature) {
MethodSignature methodSignature = (MethodSignature) signature;
// 获取被拦截的方法名
return methodSignature.getMethod().getName();
}
return "";
}
private static final String MESSAGE_RETRY = "x-retry-count";
@Value("${spring.rabbitmq.listener.simple.retry.max-attempts}")
private int MAX_ATTEMPT_COUNT; // 配置文件中的最大重试数 = 首次进入队列 (即1次) + 后续重试次数
private static final long RETRY_INTERVAL = 5_000;
private static final double P = 13.0d;
private static final double D = 9.0d;
/**
* 计算下一次的重试间隔
* @param x 重试次数
* @return 睡眠时间(ms)
*/
private static double fx(double x) {
return RETRY_INTERVAL * Math.pow(x, P / D);
}
private void manualRetry(Message message, Channel channel, ProceedingJoinPoint joinPoint) {
long deliveryTag = message.getMessageProperties().getDeliveryTag();
// 头部信息 创建一个header用来记录当前的重试次数
Map<String, Object> headers = message.getMessageProperties().getHeaders();
int retryCount = (int)headers.computeIfAbsent(MESSAGE_RETRY, (header) -> 0);
try {
//超过最大重试次数
if (++retryCount >= MAX_ATTEMPT_COUNT) {
// 不确认信息并丢弃
channel.basicNack(deliveryTag, false, false);
// 此处抛异常会将当前消息 丢入死信队列中 系统暂时还未对死信队列中的消息进行处理,可以注释掉
// throw new RuntimeException();
} else {
String methodName = getInterceptedName(joinPoint);
headers.put(MESSAGE_RETRY, retryCount);
long fx = (long) fx(retryCount);
Thread.sleep(fx);
log.info("消息重新入队, 准备发送信息,准备第 {} 次重新入队,共:{} 次, 重试方法: {}",
retryCount, MAX_ATTEMPT_COUNT - 1 , methodName);
//抛出异常使当前消息重新入队
throw new MessageRetryException();
}
} catch (IOException | InterruptedException ignored) {}
}
我这里的重试是使线程进入睡眠, 最大重试五次, 最后一次重试前需要睡眠近一分钟, 这里我感觉不太好, 但是我也想不出来怎么优化,因此只能暂时先这么用着,而实际上增加了消息重试机制后,对于推送业务数据给第三方平台这种业务,大大降低了第三方平台由于限流和并发问题导致推送一次不能正确得到返回结果的情况,万一真的最后重试机会用完了却依然没有推送成功, 那也只能手动推送数据进行处理了