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; } }