目录
这篇文章总结一下消费者消费重试的方案
直接重试
一种是消息消费失败然后消费者直接重试,这需要配置消费者重试机制
@Component
public class DirectRetryConsumer {
@RabbitListener(queues = "myQueue")
@RabbitListener(
queues = "myQueue",
containerFactory = "retryContainerFactory"
)
public void processMessage(Message message, Channel channel) {
try {
// 处理消息
processBusinessLogic(message);
// 确认消息
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
} catch (Exception e) {
// 重试处理
handleRetry(message, channel, e);
}
}
private void handleRetry(Message message, Channel channel, Exception e) {
MessageProperties props = message.getMessageProperties();
Long retryCount = props.getHeader("retry-count");
if (retryCount == null) {
retryCount = 0L;
}
if (retryCount < maxRetryCount) {
// 重新入队,等待重试
channel.basicNack(props.getDeliveryTag(), false, true);
} else {
// 超过重试次数,进入死信队列
channel.basicNack(props.getDeliveryTag(), false, false);
}
}
}
@Configuration
public class RetryConfig {
@Bean
public SimpleRabbitListenerContainerFactory retryContainerFactory(
ConnectionFactory connectionFactory) {
SimpleRabbitListenerContainerFactory factory =
new SimpleRabbitListenerContainerFactory();
// 配置重试策略
RetryTemplate retryTemplate = new RetryTemplate();
ExponentialBackOffPolicy backOffPolicy = new ExponentialBackOffPolicy();
backOffPolicy.setInitialInterval(1000);
backOffPolicy.setMultiplier(2.0);
backOffPolicy.setMaxInterval(10000);
retryTemplate.setBackOffPolicy(backOffPolicy);
factory.setRetryTemplate(retryTemplate);
return factory;
}
}
优点:
- 实现简单,开发成本低
- 消息处理实时性高
- 系统复杂度低
- 资源消耗相对较少
缺点:
- 重试策略不够灵活
- 无法保存失败原因和重试历史
- 难以进行人工干预
- 监控和统计困难
保存数据库的重试方案
@Entity
public class MessageRecord {
@Id
private Long id;
private String messageId;
private String payload;
private String status; // PENDING, PROCESSING, FAILED, SUCCESS
private Integer retryCount;
private String errorMessage;
private LocalDateTime createTime;
private LocalDateTime updateTime;
}
@Service
@Transactional
public class MessageProcessService {
@Autowired
private MessageRecordRepository recordRepository;
@RabbitListener(queues = "myQueue")
public void processMessage(Message message, Channel channel) {
MessageRecord record = null;
try {
// 1. 保存消息记录
record = saveMessageRecord(message);
// 2. 处理业务逻辑
processBusinessLogic(message);
// 3. 更新状态为成功
record.setStatus("SUCCESS");
recordRepository.save(record);
// 4. 确认消息
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
} catch (Exception e) {
// 5. 记录失败信息
handleFailure(record, e);
// 6. 拒绝消息
channel.basicNack(
message.getMessageProperties().getDeliveryTag(),
false,
false
);
}
}
}
@Component
@Slf4j
public class FailedMessageRetryJob {
@Autowired
private MessageRecordRepository recordRepository;
@Scheduled(fixedDelay = 300000) // 5分钟
public void retryFailedMessages() {
List<MessageRecord> failedRecords =
recordRepository.findByStatusAndRetryCountLessThan(
"FAILED",
maxRetryCount
);
for (MessageRecord record : failedRecords) {
try {
// 重试处理
processMessage(record);
// 更新状态
record.setStatus("SUCCESS");
recordRepository.save(record);
} catch (Exception e) {
// 更新重试次数和错误信息
record.setRetryCount(record.getRetryCount() + 1);
record.setErrorMessage(e.getMessage());
recordRepository.save(record);
log.error("Retry failed for message: {}", record.getMessageId(), e);
}
}
}
}
将所有的消息都记录在数据库中进行保存,并以消息是否消费成功来更改数据库中消息的状态值,可以开启一个定时任务执行从数据库取出失败的消息进行重新消费,因为可能取出消费的时候还可能会失败,可以设置一次任务取出数据消费条数,若是超出条数则等到下次定时任务再进行消费。
当然也可以通过定时任务给另一个重试队列投递消息,然后消费者收到消息就从数据库中取出失败的记录进行重试。这样的方法其实和刚刚讲的原理差不多。
优点:
- 追踪性强,保留完整的处理历史
- 重试策略灵活可配置
- 支持人工干预和处理
- 便于监控和统计
- 可以实现定制化的重试逻辑
- 数据不会丢失
缺点:
- 实现复杂度高
- 需要额外的数据库存储
- 系统资源消耗较大
- 实时性相对较差
- 需要维护额外的定时任务
选择建议
适合直接重试的场景
简单的消息处理,处理简单的、非关键的消息,失败影响较小,不需要追踪历史
适合数据库记录的场景
复杂的业务处理,处理订单、支付等关键业务,需要完整的处理历史,可能需要人工介入