背景
@SchedulerLock源码分析思路总结:
一个中心(围绕@EnableDiscoveryClient开展)、两个基本点(两个方法级别的注解:@SchedulerLock和@Scheduled)。
我们都知道@SchedulerLock有两种分布式锁的方案:一个是Mysql,一个是Redis,分布式锁的底层原理不难:
如果是采用Mysql,则通过磁盘记录的一张表,用于存储分布式锁信息
如果是采用Redis,则通过内存记录的KV值,用于存储分布式锁信息
源码分析:@EnableSchedulerLock
@EnableSchedulerLock:修饰启动类
@EnableFeignClients
@SpringBootApplication
@EnableDiscoveryClient
@EnableScheduling
@EnableSchedulerLock(defaultLockAtLeastFor = "10s", defaultLockAtMostFor = "60s")
public class AuditServerApplication {
public static void main(String[] args) {
CodeSpringApplication.run(AuditServerApplication.class, args);
}
}
@Import导入资源加载类
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Import(SchedulerLockConfigurationSelector.class)
public @interface EnableSchedulerLock {
InterceptMode interceptMode() default InterceptMode.PROXY_METHOD;
String defaultLockAtMostFor();
String defaultLockAtLeastFor() default "PT0S";
AdviceMode mode() default AdviceMode.PROXY;
boolean proxyTargetClass() default false;
int order() default Ordered.LOWEST_PRECEDENCE;
}
代码分析:
@Import注解(将某个类注入到IOC容器中,即将依赖到的资源导入到当前容器中)
SchedulerLockConfigurationSelector:资源加载类
public class SchedulerLockConfigurationSelector implements ImportSelector {
@Override
@NonNull
public String[] selectImports(@NonNull AnnotationMetadata metadata) {
AnnotationAttributes attributes = AnnotationAttributes.fromMap(metadata.getAnnotationAttributes(EnableSchedulerLock.class.getName(), false));
InterceptMode mode = attributes.getEnum("interceptMode");
if (mode == PROXY_METHOD) {
return new String[]{AutoProxyRegistrar.class.getName(),
LockConfigurationExtractorConfiguration.class.getName(),
MethodProxyLockConfiguration.class.getName()};
} else if (mode == PROXY_SCHEDULER) {
return new String[]{AutoProxyRegistrar.class.getName(),
LockConfigurationExtractorConfiguration.class.getName(),
SchedulerProxyLockConfiguration.class.getName(),
RegisterDefaultTaskSchedulerPostProcessor.class.getName()};
} else {
throw new UnsupportedOperationException("Unknown mode " + mode);
}
}
}
关注点:
ImportSelector接口(ImportSelector接口是至spring中导入外部配置的核心接口,在SpringBoot的自动化配置和@EnableXXX(功能性注解)都有它的存在),对于实现了selectImports方法,会返回需要被引入的类名集合(selectImports()方法的返回值就是我们向Spring容器中导入的类的全类名)。
SchedulerLockConfigurationSelector 会进行以下资源类的导入工作
AutoProxyRegistrar
LockConfigurationExtractorConfiguration
MethodProxyLockConfiguration
SchedulerProxyLockConfiguration
RegisterDefaultTaskSchedulerPostProcessor
3、分布式定时任务原理
默认是分布式锁是对方法级别进行加锁,最典型的就是分布式定时任务了。
因此关注 :LockConfigurationExtractorConfiguration定时配置类
和 MethodProxyLockConfiguration方法代理锁配置类。
3.1 LockConfigurationExtractorConfiguration:定时锁配置类
@Configuration
class LockConfigurationExtractorConfiguration extends AbstractLockConfiguration implements EmbeddedValueResolverAware {
private final StringToDurationConverter durationConverter = StringToDurationConverter.INSTANCE;
private StringValueResolver resolver;
@Bean
ExtendedLockConfigurationExtractor lockConfigurationExtractor() {
// 使用了
return new SpringLockConfigurationExtractor(
defaultLockAtMostForDuration(),
defaultLockAtLeastForDuration(),
resolver,
durationConverter);
}
// 。。。getter/setter
}
代码分析:
初始化了一个bean:ExtendedLockConfigurationExtractor。后面的方法代理锁配置类,会依赖这个bean。
具体是初始化了 SpringLockConfigurationExtractor 是锁配置提取器,传入了两个属性,是的,这就是我们修饰了启动类时,全局配置注解@EnableSchedulerLock写入的属性。
defaultLockAtLeastFor:最短锁时间
defaultLockAtMostFor:最长锁时间
3.2 MethodProxyLockConfiguration:方法代理锁配置类
MethodProxyLockConfiguration的源码如下:
@Configuration
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
class MethodProxyLockConfiguration extends AbstractLockConfiguration {
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
MethodProxyScheduledLockAdvisor proxyScheduledLockAopBeanPostProcessor(
@Lazy LockProvider lockProvider,
@Lazy ExtendedLockConfigurationExtractor lockConfigurationExtractor
) {
MethodProxyScheduledLockAdvisor advisor =
new MethodProxyScheduledLockAdvisor(
lockConfigurationExtractor,
new DefaultLockingTaskExecutor(lockProvider)
);
advisor.setOrder(getOrder());
return advisor;
}
}
关注点是 MethodProxyScheduledLockAdvisor 的装配初始化。
使用到LockProvider lockProvider:是懒加载的bean,但它不可以为空,因为 new DefaultLockingTaskExecutor(lockProvider) 代码片段里头,是会抛异常的。
使用到ExtendedLockConfigurationExtractor:这个就是上一步已经初始化好的SpringLockConfigurationExtractor实例。
那么问题来了,这里使用了@Lazy懒加载,那么LockProvider是什么时候,什么地点被加载到Spring容器的呢?答案是,要我们使用方自定义了。
3.3 RedisLockProvider:分布式锁实现类
这里我们使用了Redis方案,那么需要初始化一个RedisLockProvider bean,如下:
@Configuration
public class ApplicationConfig {
@Bean
public LockProvider lockProvider(
@Autowired LettuceConnectionFactory connectionFactory) {
return new RedisLockProvider(connectionFactory, applicationName);
}
}
代码分析:
RedisLockProvider的初始化:注入了LettuceConnectionFactory,它是连接工厂,它怎么来的呢?
LettuceConnectionFactory实现了InitializingBean接口,将在容器正常加载。
new了一个bean RedisLockProvider,并注入connectionFactory 和 名称(环境标识,自定义)
RedisLockProvider构造器
public RedisLockProvider(@NonNull RedisConnectionFactory redisConn, @NonNull String environment) {
this(redisConn, environment, "job-lock");
}
public RedisLockProvider(@NonNull RedisConnectionFactory redisConn, @NonNull String environment, @NonNull String keyPrefix) {
this(new StringRedisTemplate(redisConn), environment, keyPrefix);
}
public RedisLockProvider(@NonNull StringRedisTemplate redisTemplate, @NonNull String environment, @NonNull String keyPrefix) {
this.redisTemplate = redisTemplate;
this.environment = environment;
this.keyPrefix = keyPrefix;
}
代码分析:StringRedisTemplate的初始化。
new了一个bean StringRedisTemplate,并注入了名称 和keyPrefix
RedisLockProvider#buildKey(String lockName);
String buildKey(String lockName) {
return String.format("%s:%s:%s", this.keyPrefix, this.environment, lockName);
}
keyPrefix:默认是job-lock
environment:则是new RedisLockProvider时,传入的applicationName
lockName:则是由 @SchedulerLock 注解定义的name属性决定
那么 @SchedulerLock注解什么时候用到了呢?那便是我们期望分布式锁生效的地方,某个类的某个方法了。
@SchedulerLock和@Scheduled:修饰定时任务的方法
回想一下,还记得业务侧使用分布式锁时,是标注在方法上面,如下示例所示:
@Scheduled(cron = "0 0 10 * * ?", zone = "GMT+08") // 线上用,每天早上10点执行
@SchedulerLock(name = TASK_NAME, lockAtLeastFor = "120s", lockAtMostFor = "360s")
public void execute() {}