本文大綱📖
- 1、背景🍂
- 2、@SchedulerLock注解
- 3、實現原理
1、背景🍂
Spring生態下,日常開發定時任務,使用Spring Task框架還是很常見的選擇,但Spring Task并不是為分布式環境設計的,分布式環境下,服務被部署到多個節點,一個節點上運行著一個獨立的Jvm,各個節點之間并不會協調通訊📞,因此,同一個定時任務會在每一個節點上都執行一次,導致任務重復執行,此時,可以考慮使用redis、zookeeper等中間件來實現分布式鎖,保證一次只有一個節點執行任務,當然,也可以考慮支持分布式調度等框架,如Quartz、xxl-job
2、@SchedulerLock注解
在分布式場景下,可以使用@SchedulerLock注解來彌補Spring Task的缺點,注意??,這個不是Spring的注解,是shedlock庫的
package net.javacrumbs.shedlock.spring.annotation;import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface SchedulerLock {String name() default "";String lockAtMostFor() default "";String lockAtLeastFor() default "";
}@SchedulerLock
分布式鎖的實現方式很多,官方也提供了不同中間件的實現示例:🎒https://github.com/lukas-krecan/ShedLock/blob/master/README.md,這里演示用mysql實現的過程
- 引入相關依賴,注意依賴版本的兼容性
<dependency><groupId>net.javacrumbs.shedlock</groupId><artifactId>shedlock-spring</artifactId><version>4.42.0</version>
</dependency>
<dependency><groupId>net.javacrumbs.shedlock</groupId><artifactId>shedlock-provider-jdbc-template</artifactId><version>4.42.0</version>
</dependency>
- 配置鎖🔒提供者,這里是mysql
import net.javacrumbs.shedlock.core.LockProvider;
import net.javacrumbs.shedlock.provider.jdbctemplate.JdbcTemplateLockProvider;
import net.javacrumbs.shedlock.spring.annotation.EnableSchedulerLock;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.scheduling.annotation.EnableScheduling;import javax.sql.DataSource;@Configuration
@EnableScheduling //這個是Spring注解,開啟Spring task功能的
@EnableSchedulerLock(defaultLockAtMostFor = "30m", defaultLockAtLeastFor = "1m")
public class LockConfig {@Beanpublic LockProvider lockProvider(@Qualifier("primaryDataSource") DataSource dataSource) {return new JdbcTemplateLockProvider(JdbcTemplateLockProvider.Configuration.builder()// 使用primaryDataSource這個自定義的數據源,和業務接口用一個數據源就行 .withJdbcTemplate(new JdbcTemplate(dataSource)).usingDbTime().build());}
}
- 對應的庫里建表
CREATE TABLE shedlock(name VARCHAR(64) NOT NULL, lock_until TIMESTAMP(3) NOT NULL,locked_at TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), locked_by VARCHAR(255) NOT NULL, PRIMARY KEY (name));
- 使用
import net.javacrumbs.shedlock.spring.annotation.SchedulerLock;...@Scheduled(cron = "0 0 * * * *")
@SchedulerLock(name = "your.task.schedule.lockName") //鎖名稱自定義,可以駝峰,可以點點點
public void scheduledTask() {// do something
}
- 補充下,上面配置類里寫了默認的鎖持有的最長時間和最短時間,對于特定任務,可以自定義鎖持有時間
@SchedulerLock(name = "TaskScheduler_CommonWhiteRisksReport", lockAtMostFor = "${task.schedulerLock.lockAtMost}", lockAtLeastFor = "${task.schedulerLock.lockAtLeast}")
task:schedulerLock:lockAtMost: "PT8M"lockAtLeast: "PT8M"# Duration的格式是ISO-8601,例如:
# "PT8M" 表示8分鐘
# "PT30S" 表示30秒
# "PT1H" 表示1小時
3、實現原理
加@EnableSchedulerLock注解后,會引入SchedulerLockConfigurationSelector類
SchedulerLockConfigurationSelector類通過實現ImportSelector類,導入了兩個Bean,LockConfigurationExtractorConfiguration 和 MethodProxyLockConfiguration, Sting數組里是要注冊成Bean的類的全類名,這兩步,就是ImportSelector接口搭配@Import注解聲明Bean的方式一種使用
再往下,LockConfigurationExtractorConfiguration配置類聲明了ExtendedLockConfigurationExtractor這個Bean,里面包含了鎖的一些配置信息,如默認最大持有時間,這些配置是從注解的屬性里拿到的,這回配置提取的Bean,會帶著這些配置信息,給下面要提到的另一個Bean使用
另一個配置類,MethodProxyLockConfiguration,則是聲明了MethodProxyScheduledLockAdvisor這個Bean,里面通過上面的lockConfigurationExtractor獲取鎖的一些配置
跟進MethodProxyLockConfiguration類,發現其獲取了一個切面,切面就是包含@SchedulerLock注解的方法,切面攔截住以后,增強的部分是LockingInterceptor對象
而方法增強部分,就是根據我們提供的LockProvider來做加鎖和釋放鎖的操作:比如mysql向庫里寫數據,Redis的setnx