问题:
- 解决问题的大方向
- 为什么直接把消息发回消息队列
- 数据保存在本地 VS 保存在mq.platform.ip.fdd
- 保存在本地的好处:
- 保存本地的缺点:
- 保存在mq.platform.ip.fdd的好处:
- 保存在mq.platform.ip.fdd的缺点:
- 最终选择
- 重试方式:定时任务 VS 起工作线程
- 定时任务
- 工作线程
- mq-platform实现方式
- 消费端
- 服务端
问题
我们基本都采用自动应答,但是自动应答默认消费失败会将消息重新放回队列。
如果没有catch异常,则消息会被不断消费,但是又消费不成功,可能导致日志文件将硬盘撑爆。
为了避免这种情况,大家可能会在消费mq的业务代码最外围用try catch包住,等于消费永远成功(这种行为,等价于将defaultRequeueRejected设置为false)。
这种做法,在出现异常的时候,只是简单打一个异常日志,容易丢失mq消息,导致业务问题。
解决问题的大方向
大致的方案是:增加拦截器,消费失败的时候,持久化存储mq消息,到达指定的重消费时间,重新消费mq消息;到达最大重试次数依然不能消费成功,放弃消息。
方案流程图:
为什么直接把消息发回消息队列
如果选择将消息失败的消息保存在本地,那么重新消费mq消息,就会有两种选择:
1、将消息重放回队列,重新触发消息逻辑
2、在保存mq消息到本地的时候,也记录下消费mq消息的对象和方法,重新消费的时候,直接用反射调用处理方法
由于消费mq消息的代码千奇百怪,没办法简单的拿到消费mq消息的对象和方法,使用@RabbitListener注解的情况下,有点希望,但还涉及到数据转换等问题。
而直接将消息重发到队列就简单得多,发回原来的队列就行了。
数据保存在本地 VS 保存在mq.platform.ip.fdd
保存在本地的好处:
1、每个服务的数据都是独立的,不会相互影响
2、使用定时任务来实现重发mq消息的时候,大家可以各自控制频率
保存本地的缺点:
1、每个服务都需要在自己的业务库里创建一个保存消费失败mq的表,不方便升级,有的服务也没有数据库
2、重发mq如果依靠定时任务的方式,那每个服务都需要引入xxl-job
保存在mq.platform.ip.fdd的好处:
1、各个服务不需要建表,只是通过dubbo接口把消息保存到mq.platform.ip.fdd,,方便统一管理
2、各个服务不需要引入xxl-job这些额外的东西
保存在mq.platform.ip.fdd的缺点:
1、mq.platform.ip.fdd挂了,那么会丢失mq消息(不过告警信息里,可以把mq消息体带上,真出问也可以用告警信息里的内容手动重发)
2、业务服务都必须引入dubbo(我们本就就都会引入dubbo服务,感觉没影响)
最终选择
选择保存在mq.platform.ip.fdd,由mq.platform.ip.fdd统一来实现重试
重试方式:定时任务 VS 起工作线程
定时任务
可以选择在mq.platform.ip.fdd中引入xxl-job这个分布式任务调度,定时X每秒查询“待发送”的任务,但这会存在下面的问题:
1、X设置得太大,重试的时间准确性就降低,例如10秒才重试一次,那么最大有可能晚10秒重试
2、X设置得太小,可能上一次的任务还没执行完,下一次重试开始执行,把已经执行过一遍的任务重新执行
工作线程
可以在spring容器启动后,开启一个工作线程。线程在一个死循环里查询“待发送”的任务,然后发送出去。因为线程不停的工作,所以有这样的优势:
1、重发的时间较为准确
2、效率比定时任务高
不过用工作线程需要解决多个实例的问题,多个实体,会启动多个线程,多个线程一起工作的话,可能会导致重复发送的问题。
可以用比较简单的方式解决该问题:
1、引入zk分布式锁
2、线程开始工作前,先获取zk的锁,如果获取不到,就一直等待
3、获取到锁之后,进入while死循环,一直工作到容器退出才退出循环,释放zk的分布式锁
4、这其实就形成了一个主备,如果主服务不挂,就会一直由主服务的线程进行消息的重发;如果主服务挂了,备服务就会获取到zk的锁,成为主服务,然后进行工作
mq-platform实现方式
消费端:
在MqPlatformRabbitConsumerConfiguration配置类中,定义了mq.platform.ip.fdd接口的消费者,用于消费失败的时候,将mq消息保存到mq.platform.ip.fdd
/**
* apache.dubbo的消费者定义
*/
@ConditionalOnClass(org.apache.dubbo.config.spring.context.annotation.EnableDubbo.class)
protected static class ApacheDubboConsumeServiceConfiguration {
private String dubboApplicationName;
private String dubboRegistryAddress;
public ApacheDubboConsumeServiceConfiguration(@Value("${dubbo.application.name:}") String dubboApplicationName,
@Value("${dubbo.registry.address}") String dubboRegistryAddress,
MqPlatformProperties properties) {
this.dubboApplicationName = StringUtils.isEmpty(dubboApplicationName) ? properties.getApplication() :
dubboApplicationName;
this.dubboRegistryAddress = dubboRegistryAddress;
}
/**
* 定义mq-platform-server的ConsumeService消费者
*
* @return ConsumeService消费者
*/
@Bean
public ConsumeService consumeService() {
com.alibaba.dubbo.config.ApplicationConfig applicationConfig = new com.alibaba.dubbo.config.ApplicationConfig();
applicationConfig.setName(dubboApplicationName);
com.alibaba.dubbo.config.ConsumerConfig consumerConfig = new com.alibaba.dubbo.config.ConsumerConfig();
consumerConfig.setCheck(false);
consumerConfig.setTimeout(3000);
consumerConfig.setRetries(0);
consumerConfig.setFilter("consumerCatFilter,default");
com.alibaba.dubbo.config.RegistryConfig registryConfig = new com.alibaba.dubbo.config.RegistryConfig();
registryConfig.setAddress(dubboRegistryAddress);
com.alibaba.dubbo.config.ReferenceConfig<ConsumeService> referenceConfig =
new com.alibaba.dubbo.config.ReferenceConfig<>();
referenceConfig.setApplication(applicationConfig);
referenceConfig.setConsumer(consumerConfig);
referenceConfig.setRegistry(registryConfig);
referenceConfig.setInterface(ConsumeService.class);
return referenceConfig.get();
}
}
/**
* alibaba.dubbo的消费者定义
*/
@ConditionalOnClass(com.alibaba.dubbo.config.spring.context.annotation.EnableDubbo.class)
protected static class AlibabaDubboConsumeServiceConfiguration {
private String dubboApplicationName;
private String dubboRegistryAddress;
public AlibabaDubboConsumeServiceConfiguration(@Value("${dubbo.application.name:}") String dubboApplicationName,
@Value("${dubbo.registry.address}") String dubboRegistryAddress,
MqPlatformProperties properties) {
this.dubboApplicationName = StringUtils.isEmpty(dubboApplicationName) ? properties.getApplication() :
dubboApplicationName;
this.dubboRegistryAddress = dubboRegistryAddress;
}
/**
* 定义mq-platform-server的ConsumeService消费者
*
* @return ConsumeService消费者
*/
@Bean
@ConditionalOnMissingBean(ConsumeService.class)
public ConsumeService consumeService() {
com.alibaba.dubbo.config.ApplicationConfig applicationConfig =
new com.alibaba.dubbo.config.ApplicationConfig();
applicationConfig.setName(dubboApplicationName);
com.alibaba.dubbo.config.ConsumerConfig consumerConfig = new com.alibaba.dubbo.config.ConsumerConfig();
consumerConfig.setCheck(false);
consumerConfig.setTimeout(3000);
consumerConfig.setRetries(0);
consumerConfig.setFilter("consumerCatFilter,default");
com.alibaba.dubbo.config.RegistryConfig registryConfig = new com.alibaba.dubbo.config.RegistryConfig();
registryConfig.setAddress(dubboRegistryAddress);
com.alibaba.dubbo.config.ReferenceConfig<ConsumeService> referenceConfig =
new com.alibaba.dubbo.config.ReferenceConfig<>();
referenceConfig.setApplication(applicationConfig);
referenceConfig.setConsumer(consumerConfig);
referenceConfig.setRegistry(registryConfig);
referenceConfig.setInterface(ConsumeService.class);
return referenceConfig.get();
}
}
因为2.70之后的是apache版本,所以写了两份定义,兼容2.7.x和2.6.x
MqPlatformRabbitConsumerConfiguration中定义了可靠消费的拦截器RelyOperationsInterceptor,当有异常抛出(即消费失败的时候),会触发告警,然后就用上面定义的ConsumeService消费者,将消息保存到mq.platform.ip.fdd
/**
* rabbit可靠消费(消费失败告警和保存)的配置
*/
@ConditionalOnProperty(prefix = "mq-platform.consumer.rely", name = "enabled", havingValue = "true")
@EnableConfigurationProperties(MqPlatformProperties.class)
protected static class MqPlatformRabbitRelyConsumeConfiguration {
private MqPlatformProperties properties;
private MqPlatformConsumerRely relyProperties;
public MqPlatformRabbitRelyConsumeConfiguration(
MqPlatformProperties properties,
MqPlatformConsumerRely relyProperties
) {
this.properties = properties;
this.relyProperties = relyProperties;
}
/**
* 定义消费失败后保存消息的interceptor
*
* @param alarmProvider 告警bean提供者
* @param consumeServiceProvider 消费服务提供者
* @return 失败重试的interceptor
*/
@Bean(BeanNameConstants.INTERNAL_RELY_OPERATIONS_INTERCEPTOR)
@ConditionalOnMissingBean(RelyOperationsInterceptor.class)
public RelyOperationsInterceptor retryOperationsInterceptor(
ObjectProvider<Alarm> alarmProvider,
ObjectProvider<ConsumeService> consumeServiceProvider
) {
return new RelyOperationsInterceptor(properties.getApplication(), relyProperties.getMaxAttempts(),
relyProperties.getInitialInterval().toMillis(), relyProperties.getMaxInterval().toMillis(),
relyProperties.getMultiplier(), consumeServiceProvider, alarmProvider);
}
}
服务端
服务端主要的业务代码在ConsumeRetryRunner,通过zk分布式锁实现主备,然后主服务不断查询数据库中的任务记录,重新发送mq消息
/**
* @author wanglongzhao
* @2020-02-10 11:14
* description mq消息重试机制
*/
@Slf4j
@Component
public class ConsumeRetryRunner implements ApplicationRunner {
private static final String CONSUME_RETRY_DISTRIBUTION_LOCK_KEY = "retryLockKey";
/**
* 使用分布式锁的时候,模块名称
*/
@Value("${app.id: mq.platform.ip.fdd}")
private String moduleName;
/**
* 每次循环最多处理多少个任务
*/
@Value("${mq-platform-server.consume.retry-batch:30}")
private int consumeRetryBatch = 30;
/**
* 没有任务的时候,工作线程的睡眠时间
*/
@Value("${mq-platform-server.consume.sleep-time:1000}")
private long sleepTime = 1000;
/**
* 检查异常任务的周期
*/
@Value("${mq-platform-server.consume.check-period:5000}")
private long checkPeriod = 5000;
private volatile boolean working = true;
@Autowired
private LockManager lockManager;
@Autowired
private RabbitConsumeFailRecordManager manager;
@Autowired
private SendRabbitMqService sendRabbitMqService;
@Override
public void run(ApplicationArguments args) throws Exception {
AsyncHandlerUtil.excute(() -> {
executeConsumeRetry();
return Boolean.TRUE;
});
}
public void stopWorking() {
working = false;
}
private void executeConsumeRetry() {
DistributionLock lock = createLock();
while (!getDistributionLock(lock)) {}
// 获取锁成功,开始执行任务
long lastFetchRetryingJobTime = 0;
try {
while (working) {
try {
long currentTime = System.currentTimeMillis();
// 每过{checkPeriod}毫秒,就检索“重试中”的任务,发送消息
if (currentTime - lastFetchRetryingJobTime > checkPeriod) {
lastFetchRetryingJobTime = currentTime;
fetchRetryingJobAndSendMessage();
continue;
}
// 检索“待重试”的任务,发送消息
fetchToRetryJobAndSendMessage();
} catch (Exception e) {
try {
//延时3s
Thread.sleep(3000);
} catch (InterruptedException ignore) {}
}
}
} finally {
this.releaseDistributionLock(lock);
}
}
private void fetchToRetryJobAndSendMessage() {
//查询失败待重试的任务
ListRabbitConsumeFailRecordFilter toRetryFilter = new ListRabbitConsumeFailRecordFilter();
toRetryFilter.setRetryStatus(ConsumeRetryStatusEnum.TO_RETRY.getValue());
toRetryFilter.setEndNextRetryTime(new Date());
List<RabbitConsumeFailRecord> toRetryRecords = manager.listRecords(toRetryFilter, consumeRetryBatch);
if (CollectionUtils.isEmpty(toRetryRecords)) {
try {
Thread.sleep(sleepTime);
} catch (InterruptedException ignore) {}
return;
}
//更新数据库记录的重试状态,待重试更新为重试中
toRetryRecords.forEach(record -> record.setRetryStatus(ConsumeRetryStatusEnum.RETRYING.getValue()));
manager.updateBatchById(toRetryRecords);
toRetryRecords.forEach(this::sendMessage2Queue);
}
private void fetchRetryingJobAndSendMessage() {
// 查询一分钟前重试中(考虑到有可能上一次重试但是还未回调,所以增加一分钟前的过滤条件)的mq记录
ListRabbitConsumeFailRecordFilter retryingFilter = new ListRabbitConsumeFailRecordFilter();
retryingFilter.setRetryStatus(ConsumeRetryStatusEnum.RETRYING.getValue());
retryingFilter.setEndNextRetryTime(DateUtils.addMinutes(new Date(), -1));
List<RabbitConsumeFailRecord> retryingRecords = manager.listRecords(retryingFilter, consumeRetryBatch);
if (CollectionUtils.isEmpty(retryingRecords)) {
return;
}
retryingRecords.forEach(this::sendMessage2Queue);
}
private void sendMessage2Queue(RabbitConsumeFailRecord record) {
try {
Message message = ServerRabbitUtils.recover2Message(record);
CorrelationData correlationData = new CorrelationData();
correlationData.setId(record.getRecordId().toString());
sendRabbitMqService.send(record.getVirtualHost(), record.getQueue(), message, correlationData);
} catch (Throwable e) {
log.error("fail to requeue message, record=" + JSON.toJSONString(record), e);
}
}
private DistributionLock createLock() {
LockInfo lockInfo = new LockInfo();
lockInfo.setId(CONSUME_RETRY_DISTRIBUTION_LOCK_KEY);
lockInfo.setModule(moduleName);
//单位:ms 超时获取时间
lockInfo.setWaitTime(5000);
lockInfo.setLockProvider(LockProviderTypeEnum.ZOOKEEPER);
return lockManager.createLock(lockInfo);
}
private Boolean getDistributionLock(DistributionLock lock) {
try {
while (!lock.tryLock()) {
}
} catch (Exception e) {
log.error("分布式锁获取失败", e);
return Boolean.FALSE;
}
return Boolean.TRUE;
}
private void releaseDistributionLock(DistributionLock lock) {
lock.unlock();
}
}