在工作中,我们会经常需要用到定时任务。在分布式情况下,如在dubbo+zk这种框架下,elasticjob是一个非常好的选择。elasticjob的任务分片策略可以确保任务分布的合理性和均衡性。这里主要说一下springboot整合elasticjob。

1.引入依赖

<!-- Elastic-Job-Lite核心依赖 -->
<dependency>
  <groupId>com.dangdang</groupId>
  <artifactId>elastic-job-lite-core</artifactId>
  <version>2.1.5</version>
</dependency>
<dependency>
  <groupId>com.dangdang</groupId>
  <artifactId>elastic-job-lite-spring</artifactId>
  <version>2.1.5</version>
</dependency>

2.appcation.yml配置

引入elasticjob依赖于zookeeper,所以在整合elastic之前需要先安装zk,安装启动zk后配置zk连接地址,和elastic在zk的namespace。

server:
  port: 8888
elasticjob:
  regCenter:
    serverList: localhost:2181
    namespace: elastic-job
simpleJob:
  shardingTotalCount:
    one: 1
    five: 5

3.配置注册中心

package com.example.elsticjob.config;
import com.dangdang.ddframe.job.reg.zookeeper.ZookeeperConfiguration;
import com.dangdang.ddframe.job.reg.zookeeper.ZookeeperRegistryCenter;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
@ConditionalOnExpression("'${elasticjob.regCenter.serverList}'.length() > 0")
public class RegistryCenterConfig {

    @Bean(initMethod = "init")
    public ZookeeperRegistryCenter regCenter(@Value("${elasticjob.regCenter.serverList}") final String serverList, @Value("${elasticjob.regCenter.namespace}") final String namespace) {
        return new ZookeeperRegistryCenter(new ZookeeperConfiguration(serverList, namespace));
    }

}

4.配置定时任务

这里shardingTotalCount分片数量我们配置的是5,正常情况下一个分片就可以了。

package com.example.elsticjob.config;

import com.dangdang.ddframe.job.api.simple.SimpleJob;
import com.dangdang.ddframe.job.config.JobCoreConfiguration;
import com.dangdang.ddframe.job.config.simple.SimpleJobConfiguration;
import com.dangdang.ddframe.job.lite.api.JobScheduler;
import com.dangdang.ddframe.job.lite.config.LiteJobConfiguration;
import com.dangdang.ddframe.job.lite.spring.api.SpringJobScheduler;
import com.dangdang.ddframe.job.reg.zookeeper.ZookeeperRegistryCenter;
import com.example.elsticjob.work.DemoJob;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.annotation.Resource;

/**
 * elastic-job配置
 * @author murphy
 * @date 2024/6/1
 */
@Configuration
public class ElasticJobConfig {

    @Resource
    private ZookeeperRegistryCenter regCenter;

    private LiteJobConfiguration getLiteJobConfiguration(final Class<? extends SimpleJob> jobClass, final String cron,
                                                         final int shardingTotalCount, final String shardingItemParameters, String description, boolean disabled) {
        return LiteJobConfiguration.newBuilder(new SimpleJobConfiguration(
                JobCoreConfiguration.newBuilder(jobClass.getName(), cron, shardingTotalCount)
                        .shardingItemParameters(shardingItemParameters).failover(true).description(description).build(),
                jobClass.getCanonicalName())).overwrite(true).disabled(disabled).build();
    }

    @Bean(initMethod = "init")
    public JobScheduler simpleJobScheduler(final DemoJob demoJob,@Value("0/5 * * * * ?") final String cron,@Value("${simpleJob.shardingTotalCount.five}") final int shardingTotalCount) {
        return new SpringJobScheduler(demoJob, regCenter,
                getLiteJobConfiguration(demoJob.getClass(), cron, shardingTotalCount, null,
                        "定时任务", false));
    }
}

5.任务实现类

假设我们的定时业务需要循环执行一个逻辑,或者是多租户的情况下,可以使用多分片策略执行任务。

代码中的list模仿了多租户的情况下,根据租户id对分片数量进行取模,如果取模值和分片相等,则执行业务逻辑。

@Component
public class DemoJob implements SimpleJob {
  @Override
    public void execute(final ShardingContext shardingContext) {
        List<Integer> list = new ArrayList<>();
        list.add(1);
        list.add(2);
        list.add(3);
        list.add(4);
        list.add(5);
        for (Integer i : list){
            if(i % shardingContext.getShardingTotalCount() == shardingContext.getShardingItem()){
                System.out.println("当前执行分片:" + shardingContext.getShardingItem());
                //todo 业务逻辑
//                doJob(shardingContext.getShardingItem());
            }
        }
    }
}

运行结果如下:

当前执行分片:0
当前执行分片:1
当前执行分片:2
当前执行分片:3
当前执行分片:4

6.优化定时任务中的耗时处理

在定时任务中,我们可能有一些逻辑是比较耗时的,我们可以采用异步的方式去解决问题。

CompletableFuture是 Java 8 引入的一个类,属于 java.util.concurrent 包,它提供了一种处理异步任务的方法,使得编写并发代码更加简洁和易于管理。

处理代码如下:

我们模拟一个耗时操作,需要6秒处理完成。如果5个循环去执行这块逻辑,执行完成需要30多秒。使用CompletableFuture异步执行,最后再合并异步任务,获取最终结果,5个任务只需要6秒多。大大节省了处理时间。

CompletableFuture.supplyAsync方法是有返回值的,CompletableFuture.runAsync方法沒有返回值,它提供了丰富的 API 以简化并发代码的编写和管理。有需要可以查看官方文档:https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/util/concurrent/CompletableFuture.html

private void doJob(int shardingItem){
        List<Integer> result = new ArrayList<>();
        CompletableFuture<List<Integer>>[] futureList = new CompletableFuture[5];
        System.out.println("开始执行耗时任务....................");
        long start = System.currentTimeMillis();
        for (int i = 0; i < 5; i++){
            int finalI = i;
            CompletableFuture<List<Integer>> cf = CompletableFuture.supplyAsync(() -> {
                List<Integer> list = new ArrayList<>();
                list.add(finalI);
                //模拟耗时操作
                try {
                    Thread.sleep(6000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                return list;
            });
            futureList[i] = cf;
        }
        CompletableFuture<Void> allOfFuture = CompletableFuture.allOf(futureList);
        allOfFuture.join();
        CompletableFuture<List<Integer>> mergedFuture = allOfFuture.thenApply(v ->
                Stream.of(futureList)
                        .map(CompletableFuture::join)  // 获取每个 CompletableFuture 的结果
                        .flatMap(List::stream)         // 将多个 List<T> 合并为一个流
                        .collect(Collectors.toList())  // 收集到一个 List<T>
        );
        try {
            result = mergedFuture.get();
            System.out.println("耗时任务执行完毕,耗时:" + (System.currentTimeMillis() - start));
        } catch (Exception e) {
            System.out.println("获取结果失败");
            return;
        }
        System.out.println("分片"+shardingItem+"获取的结果:" + result);
    }