源码分析:@SchedulerLock分布式锁

24e3093380daa1eb2f3be5a525f2cfca.jpeg

背景

@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() {}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值