Spring的任務調度處理

Spring封裝了JDK的任務調度線程池和任務調用,並使用標籤就可以開啓一個任務調用。

先進行一個Spring的任務調度線程池的配置,此時是多線程執行任務,如果不配置則默認爲單線程串行執行任務。

@Configuration
@EnableScheduling
@Slf4j
public class ScheduleConfiguration implements SchedulingConfigurer {
    @Override
    public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
        ThreadPoolTaskScheduler taskScheduler = new ThreadPoolTaskScheduler();
        taskScheduler.setPoolSize(Runtime.getRuntime().availableProcessors() * 2);
        taskScheduler.initialize();
        log.info("ThreadPoolTaskScheduler init poolSize");
        taskRegistrar.setTaskScheduler(taskScheduler);
    }
}

不進行上述配置的話,需要將@EnableScheduling配置到Springboot主啓動類上(一般這麼配置,但其實可以配置到任意一個@Configuration標記的配置類上)

@SpringBootApplication
@EnableScheduling
public class RediscachingApplication {

   public static void main(String[] args) {
      SpringApplication.run(RediscachingApplication.class, args);
   }

}

但一個系統有多個任務執行的時候,最好使用多線程配置,這裏暫時不牽扯分佈式任務調度的問題。

現在我們來測試每隔10秒進行一次打印

@Component
@Slf4j
public class TestScheduler {
    @Scheduled(fixedDelay = 1000 * 10)
    public void print() {
        log.info("測試打印");
    }
}

這種設置時間會等方法體執行完的第10秒開始執行,比如print()在第0秒開始執行,而print()方法本身執行了12秒,則下一次執行會在第22秒。

@Component
@Slf4j
public class TestScheduler {
    @Scheduled(fixedRate = 1000 * 10)
    public void print() {
        log.info("測試打印");
    }
}

這種設置當方法的執行時間超過任務調度頻率時,調度器會在當前方法執行完成後立即執行下次任務。比如print()方法在第0秒開始執行,方法執行了12秒,那麼下一次執行work()方法的時間是第12秒。

啓動運行後,日誌如下

2020-10-14 06:19:37.137  INFO 683 --- [TaskScheduler-1] c.g.r.scheduler.TestScheduler            : 測試打印
2020-10-14 06:19:47.141  INFO 683 --- [TaskScheduler-2] c.g.r.scheduler.TestScheduler            : 測試打印
2020-10-14 06:19:57.144  INFO 683 --- [TaskScheduler-1] c.g.r.scheduler.TestScheduler            : 測試打印
2020-10-14 06:20:07.146  INFO 683 --- [TaskScheduler-3] c.g.r.scheduler.TestScheduler            : 測試打印

我們可以看到它是由不同的線程來執行的。

當然也可以使用Cron表達式來設置

常用表達式

@Component
@Slf4j
public class TestScheduler {
    @Scheduled(cron = "0/10 * * * * *")
    public void print() {
        log.info("測試打印");
    }
}

這麼寫也是每隔10秒打印一次。

現在我們來寫一個最簡單的分佈式調度,使用nacos

pom

<dependency>
   <groupId>com.alibaba.cloud</groupId>
   <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependencyManagement>
   <dependencies>
      <dependency>
         <groupId>org.springframework.cloud</groupId>
         <artifactId>spring-cloud-dependencies</artifactId>
         <version>Greenwich.SR2</version>
         <type>pom</type>
         <scope>import</scope>
      </dependency>
      <dependency>
         <groupId>com.alibaba.cloud</groupId>
         <artifactId>spring-cloud-alibaba-dependencies</artifactId>
         <version>2.1.1.RELEASE</version>
         <type>pom</type>
         <scope>import</scope>
      </dependency>
   </dependencies>
</dependencyManagement>

配置文件

spring:
  cloud:
    nacos:
      discovery:
        server-addr: 127.0.0.1:8848
  application:
      name: redis-caching
server:
  port: 8080

寫一個標籤

@Target({ ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
public @interface Scheduler {
}

一個AOP類

@Aspect
@Component
public class SchedulerAop {
    @Autowired
    private DiscoveryClient discoveryClient;
    @Value("${server.port}")
    private int port;
    @Value("${spring.application.name}")
    private String appName;

    @Around(value = "@annotation(com.guanjian.rediscaching.annotation.Scheduler)")
    public Object scheduler(ProceedingJoinPoint joinPoint) throws Throwable {
        List<ServiceInstance> nacos = discoveryClient.getInstances(appName);
        if (nacos != null && nacos.size() > 0) {
            String ip = IpUtils.getHostIp();
            if ((nacos.get(0).getHost() + nacos.get(0).getPort()).equals(ip + port)) {
                Object res = joinPoint.proceed();
                return res;
            }
        }
        return null;
    }
}

其中IpUtils的代碼如下

@Slf4j
public class IpUtils {

    public static String getHostIp() {
        String ip = null;
        try {
            //枚舉本機所有的網絡接口
            Enumeration<NetworkInterface> en = NetworkInterface
                    .getNetworkInterfaces();
            while (en.hasMoreElements()) {
                NetworkInterface intf = (NetworkInterface) en.nextElement();
                //遍歷所有Ip
                Enumeration<InetAddress> enumIpAddr = intf.getInetAddresses();
                while (enumIpAddr.hasMoreElements()) {
                    InetAddress inetAddress = (InetAddress) enumIpAddr
                            .nextElement();
                    //獲取類似192.168的內網IP
                    if (!inetAddress.isLoopbackAddress()  //isLoopbackAddress()是否是本機的IP地址(127開頭的,一般指127.0.0.1)
                            && !inetAddress.isLinkLocalAddress()  //isLinkLocalAddress()是否是本地連接地址(任意開頭)
                            && inetAddress.isSiteLocalAddress()) {  //isSiteLocalAddress()是否是地區本地地址(192.168段或其他內網IP)
                        ip = inetAddress.getHostAddress();
                    }
                }
            }
        } catch (SocketException e) {
            log.error("Fail to get IP address.", e);
        }
        return ip;
    }

    public static String getHostName() {
        String hostName = null;
        try {
            Enumeration<NetworkInterface> en = NetworkInterface
                    .getNetworkInterfaces();
            while (en.hasMoreElements()) {
                NetworkInterface intf = (NetworkInterface) en.nextElement();
                Enumeration<InetAddress> enumIpAddr = intf.getInetAddresses();
                while (enumIpAddr.hasMoreElements()) {
                    InetAddress inetAddress = (InetAddress) enumIpAddr
                            .nextElement();
                    if (!inetAddress.isLoopbackAddress()
                            && !inetAddress.isLinkLocalAddress()
                            && inetAddress.isSiteLocalAddress()) {
                        hostName = inetAddress.getHostName();
                    }
                }
            }
        } catch (SocketException e) {
            log.error("Fail to get host name.", e);
        }
        return hostName;
    }
}

最後依然是這個測試類,打上標籤

@Component
@Slf4j
public class TestScheduler {
    @Scheduler
    @Scheduled(fixedRate = 1000 * 10)
    public void print() {
        log.info("測試打印");
    }
}

現在我們來啓動第一個進程

nacos註冊中心註冊了該實例

日誌中也開始進行打印

2020-10-14 11:10:46.941  INFO 648 --- [TaskScheduler-1] c.g.r.scheduler.TestScheduler            : 測試打印
2020-10-14 11:10:51.812  INFO 648 --- [TaskScheduler-1] c.g.r.scheduler.TestScheduler            : 測試打印
2020-10-14 11:11:01.811  INFO 648 --- [TaskScheduler-2] c.g.r.scheduler.TestScheduler            : 測試打印
2020-10-14 11:11:11.813  INFO 648 --- [TaskScheduler-1] c.g.r.scheduler.TestScheduler            : 測試打印
2020-10-14 11:11:21.811  INFO 648 --- [TaskScheduler-3] c.g.r.scheduler.TestScheduler            : 測試打印
2020-10-14 11:11:31.811  INFO 648 --- [TaskScheduler-2] c.g.r.scheduler.TestScheduler            : 測試打印
2020-10-14 11:11:41.812  INFO 648 --- [TaskScheduler-4] c.g.r.scheduler.TestScheduler            : 測試打印
2020-10-14 11:11:51.812  INFO 648 --- [TaskScheduler-1] c.g.r.scheduler.TestScheduler            : 測試打印

現在我們修改端口,啓動第二個進程

server:
  port: 8081

啓動成功後

我們可以看到實例數變成了2

後臺打印

我們可以看到第一個進程的後臺日誌停止了打印,而第二個進程的後臺日誌開始打印

2020-10-14 11:15:23.925  INFO 693 --- [TaskScheduler-6] c.g.r.scheduler.TestScheduler            : 測試打印
2020-10-14 11:15:33.922  INFO 693 --- [TaskScheduler-2] c.g.r.scheduler.TestScheduler            : 測試打印
2020-10-14 11:15:43.925  INFO 693 --- [TaskScheduler-7] c.g.r.scheduler.TestScheduler            : 測試打印
2020-10-14 11:15:53.926  INFO 693 --- [TaskScheduler-4] c.g.r.scheduler.TestScheduler            : 測試打印
2020-10-14 11:16:03.925  INFO 693 --- [TaskScheduler-8] c.g.r.scheduler.TestScheduler            : 測試打印
2020-10-14 11:16:13.927  INFO 693 --- [TaskScheduler-1] c.g.r.scheduler.TestScheduler            : 測試打印

當然這是不一定的,兩個進程誰打印誰不打印都是隨機的,但可以肯定的是,只有一個進程可以打印日誌,另外一個進程則不會做出打印操作。

如果我們結束打印日誌的這個進程,則另外一個進程就會開始打印日誌。

如果我們在配置文件中的配置端口爲隨機端口的時候該怎麼處理呢?

server:
  port: 0

則只需要修改SchedulerAop的代碼如下

@Aspect
@Component
public class SchedulerAop {
    @Autowired
    private DiscoveryClient discoveryClient;
    @Value("${spring.application.name}")
    private String appName;
    @Autowired
    private WebServerApplicationContext context;

    @Around(value = "@annotation(com.guanjian.rediscaching.annotation.Scheduler)")
    public Object scheduler(ProceedingJoinPoint joinPoint) throws Throwable {
        List<ServiceInstance> nacos = discoveryClient.getInstances(appName);
        if (nacos != null && nacos.size() > 0) {
            String ip = IpUtils.getHostIp();
            int port = context.getWebServer().getPort();
            if ((nacos.get(0).getHost() + nacos.get(0).getPort()).equals(ip + port)) {
                Object res = joinPoint.proceed();
                return res;
            }
        }
        return null;
    }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章