分布式定时任务轻量级解决方案ShedLock

微信搜索“onebyte”发现更多技术文章。

01 什么是ShedLock

ShedLock利用锁(分布式锁)机制,确保处于多个节点的定时任务只执行一次。注意,ShedLock本身并不是一个分布式定时调度器。

Please note that ShedLock is not and will never be full-fledged scheduler, it's just a lock. 

此外,ShedLock是基于时间来实现的分布式锁,它假定每个节点上的时钟已同步。

Moreover, the locks are time-based and ShedLock assumes that clocks on the nodes are synchronized.

02 

ShedLock核心组件

  • Core - 锁机制的支持;

  • Integration - 使用Spring AOP,Micronaut AOP或自定义开发与应用程序集成;

  • Lock provider - 基于额外扩展程序(例如SQL数据库,Mongo,Redis等)实现锁机制。

  • 03 

    如何使用

    使用ShedLock非常简单,需遵循以下3个步骤:

  • 启用并配置ShedLock,@EnableSchedulerLock;

  • 使用注解标注调度任务,使用@SchedulerLock注解标注即可;

  • 配置锁实现。

  • 第一步:启用并配置ShedLock;Enable and configure Scheduled locking (Spring)

    添加依赖

    <dependency>
        <groupId>net.javacrumbs.shedlock</groupId>
        <artifactId>shedlock-spring</artifactId>
        <version>4.12.0</version>
    </dependency>

    启用,其中@EnableScheduling开启Spring对任务调度的支持

    @Configuration
    @EnableScheduling
    @EnableSchedulerLock(defaultLockAtMostFor = "30s")
    public class ShedLockConfig {
        ...}

    第二步:使用注解标注调度任务;Annotate your scheduled tasks

    import net.javacrumbs.shedlock.core.SchedulerLock;

    @Scheduled(cron = "0 */2 * * * *")
    @SchedulerLock(name = "scheduledTaskName", lockAtMostFor = "2s", lockAtLeastFor = "30s")
    public void scheduledTask() {
       // do something}

    @SchedulerLock注解说明:

  • 标识调度任务以被ShedLock识别,并执行代理(获取锁后再执行任务);

  • name属性:必须指定,ShedLock保证具有相同name的定时任务同一时刻仅执行一次;

  • lockAtMostFor属性:任务获得锁后的最长持有时间;在正常情况下,任务执行完毕后会立即释放锁,这里的时间设置防止程序无法正常释放锁导致死锁。此外,lockAtMostFor设置的时间务必大于任务的执行时间,否则可能存在多个线程持有该锁,不能保证任务执行结果的正确性。如果未在@SchedulerLock中指定lockAtMostFor,则将使用@EnableSchedulerLock中的默认值。

  • lockAtLeastFor属性:任务获取锁后最短持有时间;在任务执行时间很短且节点之间的时钟不同步的情况下,该属性阻止任务在多个节点执行。
  • 第三步:配置锁实现;Configure LockProvider

    官方支持如下组件的锁实现,这里我们选用Redis,Redis (using Spring RedisConnectionFactory);

    Lock Providers

  • JdbcTemplate

  • Mongo

  • DynamoDB

  • DynamoDB 2

  • ZooKeeper (using Curator)

  • Redis (using Spring RedisConnectionFactory)

  • Redis (using Jedis)

  • Hazelcast

  • Couchbase

  • ElasticSearch

  • CosmosDB

  • Cassandra

  • Consul

  • ArangoDB

  • Etcd

  • Multi-tenancy

  • 添加Redis依赖,这里我们以SpringBoot为例:

    <!-- redis -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
    <!-- commons-pool2,为redis提供连接池 -->
    <dependency>
        <groupId>org.apache.commons</groupId>
        <artifactId>commons-pool2</artifactId>
    </dependency>
        
    <dependency>
        <groupId>net.javacrumbs.shedlock</groupId>
    <artifactId>shedlock-provider-redis-spring</artifactId>
        <version>4.12.0</version>
    </dependency>

    配置lockProvider:

    import net.javacrumbs.shedlock.core.LockProvider;
    import net.javacrumbs.shedlock.provider.redis.spring.RedisLockProvider;
    import net.javacrumbs.shedlock.spring.annotation.EnableSchedulerLock;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.data.redis.connection.RedisConnectionFactory;

    /**
     * @ClassName ShedLockConfiguration
     * @Author yangqk */
    @Configuration
    @EnableSchedulerLock(defaultLockAtMostFor = "30s")
    public class ShedLockConfig {
        @Bean
        public LockProvider lockProvider(RedisConnectionFactory redisConnectionFactory) {
            return new RedisLockProvider(redisConnectionFactory);
        }}

    注意:Redis实现的锁在哨兵或者集群模式下,发生主从故障时候,将存在不可靠的问题。04

    ShedLock的原理简析

    ShedLock基于代理实现对任务调度的控制。在执行任务前,会先获取锁,拿到锁的任务才能执行。如下图所示:

    与Spring整合的2种代理模式

    一种基于aop对ScheduledMethod进行代理(PROXY_METHOD),另一种是基于aop对TaskScheduler进行代理(PROXY_SCHEDULER)。

    代理模式一:Scheduled Method proxy

    从4.0.0版本开始,默认的代理方式。

    这种模式的主要优点是,它与希望以某种方式更改默认Spring调度机制的其他框架很好地配合使用。缺点是即使直接调用该方法也需要先获取锁。如果该方法返回一个值并且该锁由另一个进程持有,则将返回null或空的Optional对象(不支持原始返回类型)。

    注意:Final和non-public的方法无法执行代理,必须将其改为public的,或者使用TaskScheduler代理。

    代理模式二:TaskScheduler proxy

    这种模式将Spring TaskScheduler包装在AOP代理中。其开启方式(PROXY_SCHEDULER是4.0.0之前的默认方法):

    @EnableSchedulerLock(interceptMode = PROXY_SCHEDULER)

    如果在spring容器内没有发现TaskScheduler的实例,则会为其创建一个默认的实例。如果你有特殊需要,只需创建一个实现TaskScheduler接口的bean,它将自动包装到AOP代理中。如下代码的MySpecialTaskScheduler既是自定义的实现:

    @Bean
    public TaskScheduler taskScheduler() {
        return new MySpecialTaskScheduler();}

    此种代理方式的原理示意图如下:

    总结:ShedLock的使用比较简单,但并不是分布式调度任务的完美解决方案;对于追求分布式调度任务高可用、强一致性的的系统来说,建议选型更为优秀的解决方案。

    05

    引用

    [1] https://github.com/lukas-krecan/ShedLock

    [2] https://www.baeldung.com/shedlock-spring

    [3] https://rieckpil.de/lock-scheduled-tasks-with-shedlock-and-spring-boot/

    点击上方“蓝字”,发现更多精彩。

    最后编辑于
    ©著作权归作者所有,转载或内容合作请联系作者
    【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
    平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

    相关阅读更多精彩内容

    友情链接更多精彩内容