目录
前言:
实现逻辑:
创建重试表
消息队列进行异步消费
Elastic-Job进行定时任务调度
Elastic-Job设置
return new SpringJobScheduler()
定义一个JobExceptionHandler,用来处理任务执行过程中的异常。在处理异常时需要根据异常类型来判断是否需要重试。
重试机制实现
在Elastic-Job配置中,可以通过设置JobProperties来实现重试机制的相关配置。常用的设置包括:
Elastic-Job实现流程:
你知道的,笔记!
在面试的时候,可能都会涉及到实战业务场景问题, 今天分享一个,希望面试能够用得上,提供具体实现思路...
下单消息失败,消费者在处理消息时,先判断该订单号在重试的表有没有数据,如果有则直接把当前消息保存重试表,如果没有则进行业务处理,如果出现异常,把该消息保存到重试表,用elastic-job失败重试机制,当然还可以使用其他业务来完成,或者海豚调度也是可以的我记得
id:消息唯一标识
orderNo:订单号
message:消息内容
retryCount:重试次数
nextRetryTime:下次重试时间
createTime:创建时间
updateTime:更新时间
CREATE TABLE retry_order (
id varchar(32) NOT NULL,
order_no varchar(32) NOT NULL,
message varchar(2048) NOT NULL,
retry_count int NOT NULL,
next_retry_time datetime NOT NULL,
create_time datetime NOT NULL,
update_time datetime NOT NULL,
PRIMARY KEY (id)
);
消息队列中注册一个消息监听器,然后根据推送的消息去实现业务逻辑处理
@Service
public class OrderMessageConsumer implements MessageListener {
private static final Logger LOGGER = LoggerFactory.getLogger(OrderMessageConsumer.class);
@Autowired
private OrderService orderService;
@Autowired
private RetryOrderService retryOrderService;
@Override
public Action consume(Message message, ConsumeContext context) {
try {
// 解析消息内容
String orderNo = new String(message.getBody(), Charset.forName("UTF-8"));
// 判断重试表中是否存在该订单号的记录
RetryOrder retryOrder = retryOrderService.getByOrderNo(orderNo);
if (retryOrder != null) {
// 保存消息到重试表中
retryOrder.setMessage(message.getBody());
retryOrder.setNextRetryTime(new Date());
retryOrderService.save(retryOrder);
return Action.CommitMessage;
}
// 处理业务逻辑
orderService.createOrder(orderNo);
return Action.CommitMessage;
} catch (Exception e) {
LOGGER.error("Consume message failed", e);
// 保存消息到重试表中
RetryOrder retryOrder = new RetryOrder();
retryOrder.setId(UUID.randomUUID().toString());
retryOrder.setOrderNo(new String(message.getBody(), Charset.forName("UTF-8")));
retryOrder.setMessage(message.getBody());
retryOrder.setRetryCount(0);
retryOrder.setNextRetryTime(new Date());
retryOrderService.save(retryOrder);
return Action.ReconsumeLater;
}
}
}
优化前的代码中使用了消息队列进行异步消费,因此需要实现MessageListener接口来处理消息。这种方式下,消息队列会自动将消息推送给MessageListener的实现类进行处理,不需要手动触发任务。
优化后的代码中,使用了Elastic-Job进行定时任务调度。由于Elastic-Job提供了任务分片机制,可以将任务分成多个片段,并通过多个作业节点并发执行,因此可以替代消息队列的异步消费方式。
在使用Elastic-Job进行任务调度时,我们一般会使用SimpleJob来处理任务。SimpleJob是Elastic-Job提供的一个接口,只有一个execute方法,用来处理任务业务逻辑。Elastic-Job会自动将任务分片成多个片段,并调用execute方法进行处理。
@Slf4j
@Service
@ElasticJobConf(name = "orderJob", jobType = JobType.SIMPLE)
public class OrderMessageConsumer implements SimpleJob {
@Autowired
private OrderService orderService;
@Autowired
private RetryOrderService retryOrderService;
@Override
@Transactional(rollbackFor = Exception.class)
@ShardingItem(key = "orderNo")
public void execute(ShardingContext context) {
try {
// 解析消息内容
String orderNo = new String(context.getShardingParameter(), Charset.forName("UTF-8"));
// 判断重试表中是否存在该订单号的记录
RetryOrder retryOrder = retryOrderService.getByOrderNo(orderNo);
if (retryOrder != null) {
// 保存消息到重试表中
retryOrder.setMessage(context.getJobName().getBytes());
retryOrder.setNextRetryTime(new Date());
retryOrderService.save(retryOrder);
return;
}
// 处理业务逻辑
orderService.createOrder(orderNo);
} catch (Exception e) {
log.error("Consume message failed", e);
// 保存消息到重试表中
RetryOrder retryOrder = new RetryOrder();
retryOrder.setId(UUID.randomUUID().toString());
retryOrder.setOrderNo(new String(context.getShardingParameter(), Charset.forName("UTF-8")));
retryOrder.setMessage(context.getJobName().getBytes());
retryOrder.setRetryCount(0);
retryOrder.setNextRetryTime(new Date());
retryOrderService.save(retryOrder);
throw new RuntimeException(e);
}
}
}
@Configuration
public class ElasticJobConfiguration {
@Autowired
private ZookeeperRegistryCenter regCenter;
@Autowired
private JobEventConfiguration elasticJobListener;
@Autowired
private OrderMessageConsumer orderMessageConsumer;
@Bean(initMethod = "init")
public JobScheduler orderJobScheduler() {
String cron = "0/10 * * * * ?";
int shardingTotalCount = 3;
String shardingItemParameters = "0=a,1=b,2=c";
String jobParameter = "orderJob";
String description = "订单消息处理任务";
JobProperties jobProperties = new JobProperties();
jobProperties.setProperty("job_exception_handler", CustomJobExceptionHandler.class.getCanonicalName());
jobProperties.setProperty("max_time_diff_seconds", "600");
jobProperties.setProperty("misfire", "false");
jobProperties.setProperty("failover", "true");
JobConfig jobConfig = new JobCoreConfiguration("orderJob", cron, shardingTotalCount)
.jobProperties(jobProperties)
.shardingItemParameters(shardingItemParameters)
.jobParameter(jobParameter)
.description(description);
LiteJobConfiguration liteJobConfiguration = LiteJobConfiguration.newBuilder(jobConfig)
.overwrite(true)
.disabled(false)
.monitorExecution(true)
.maxTimeDiffSeconds(10)
.jobShardingStrategyClass(AverageAllocationJobShardingStrategy.class.getCanonicalName())
.reconcileIntervalMinutes(30)
.build();
return new SpringJobScheduler(orderMessageConsumer, regCenter, liteJobConfiguration, elasticJobListener);
}
}
这段代码用来创建一个SpringJobScheduler对象,用于启动Elastic-Job分布式任务调度框架。
在创建SpringJobScheduler对象时,需要传入以下参数:
- orderMessageConsumer:实现了Elastic-Job的SimpleJob接口的任务处理类。
- regCenter:注册中心实例。
- liteJobConfiguration:作业的配置信息。
- elasticJobListener:作业监听器。
SpringJobScheduler对象中封装了Elastic-Job的JobScheduler对象和Spring的ApplicationContext对象,它可以在Spring容器启动之后自动启动作业,同时在Spring容器关闭之前销毁作业。
public class CustomJobExceptionHandler implements JobExceptionHandler {
private static final Logger LOGGER = LoggerFactory.getLogger(CustomJobExceptionHandler.class);
@Override
public void handleException(String jobName, Throwable cause) {
LOGGER.error(String.format("Job [%s] exception occur in job processing", jobName), cause);
if (cause instanceof BusinessException) {
// 业务异常不需要重试
return;
} else {
// 其他异常需要重试
JobExecutionException jobExecutionException = new JobExecutionException(cause);
jobExecutionException.setRetryable(true);
throw jobExecutionException;
}
}
}
JobProperties jobProperties = new JobProperties();
jobProperties.setProperty("job_exception_handler", CustomJobExceptionHandler.class.getCanonicalName());
jobProperties.setProperty("max_time_diff_seconds", "600");
jobProperties.setProperty("misfire", "false");
jobProperties.setProperty("failover", "true");
jobProperties.setProperty("max_retries", "3");
jobProperties.setProperty("retry_interval", "1000");
JobConfig jobConfig = new JobCoreConfiguration("orderJob", cron, shardingTotalCount)
.jobProperties(jobProperties)
.shardingItemParameters(shardingItemParameters)
.jobParameter(jobParameter)
.description(description);
LiteJobConfiguration liteJobConfiguration = LiteJobConfiguration.newBuilder(jobConfig)
.overwrite(true)
.disabled(false)
.monitorExecution(true)
.maxTimeDiffSeconds(10)
.jobShardingStrategyClass(AverageAllocationJobShardingStrategy.class.getCanonicalName())
.reconcileIntervalMinutes(30)
.build();
JobProperties jobProperties = new JobProperties();
jobProperties.setProperty("max_retries", "3");
jobProperties.setProperty("retry_interval", "1000");
当任务执行失败时,我们需要将异常信息包装成一个JobExecutionException,并抛出。Elastic-Job会根据配置的重试次数和间隔时间进行重试。
需要注意的是,抛出的JobExecutionException需要设置retryable为true,才会进行重试。如下所示。
catch (Exception e) {
LOGGER.error("Consume message failed", e);
JobExecutionException jobExecutionException = new JobExecutionException(e);
jobExecutionException.setRetryable(true);
throw jobExecutionException;
}
在设置retryable为true之后,Elastic-Job就会根据配置的重试次数和间隔时间进行重试,直到任务执行成功或达到最大重试次数。需要注意的是,如果任务执行时间过长,可能会影响重试机制的效果。此时可以适当调整配置中的max_time_diff_seconds参数,使其与任务执行时间相匹配。