ShedLock

Spring 多个微服务做定时任务,如何做负载

基本思路

多个微服务,其业务的逻辑是一样的,自然包括定时任务。负载均衡在执行的时候,到达某个节点以后,定时任务都会执行,可以控制的思路就是使用队列的方式去操作。
如下有两种思路:

  1. 将负载均衡的定时任务,从原先的直接执行业务逻辑修改为先将业务逻辑请求到队列中,然后让空闲的微服务去队列中自动领取;
  2. 将负载均衡的定时任务,加锁进行操作。

ShedLock

ShedLock就是一种巧妙使用锁的方式的,在GitHub中的地址:GitHub - lukas-krecan/ShedLock: Distributed lock for your scheduled tasks

ShedLock does one and only one thing. It makes sure your scheduled tasks are executed at most once at the same time. If a task is being executed on one node, it acquires a lock which prevents execution of the same task from another node (or thread). Please note, that if one task is already being executed on one node, execution on other nodes does not wait, it is simply skipped.

Currently, Spring scheduled tasks coordinated through Mongo, JDBC database, Redis, Hazelcast or ZooKeeper are supported. More scheduling and coordination mechanisms and expected in the future.

ShedLock is not a distributed scheduler
Please note that ShedLock is not and will never be full-fledged scheduler, it's just a lock. If you need a distributed scheduler, please use another project. ShedLock is designed to be used in situations where you have scheduled tasks that are not ready to be executed in parallel, but can be safely executed repeatedly. For example if the task is fetching records from a database, processing them and marking them as processed at the end without using any transaction. In such case ShedLock may be right for you.

By setting lockAtMostFor we make sure that the lock is released even if the node dies and by setting lockAtLeastFor we make sure it's not executed more than once in fifteen minutes. Please note that if the task takes longer than 15 minutes, it will be executed again.

个人理解,拿JDBCTemplate进行说明,就是当第一个微服务执行定时任务的时候,会将此定时任务进行锁操作,然后其他的定时任务就不会再执行,锁操作有一定的时长,超过这个时长以后,再一次,所有的定时任务进行争抢下一个定时任务的执行权利,如此循环。其中两个配置lockAtMostFor和lockAtLeastFor,保证了在一个定时任务的区间内只有一个定时任务在执行,同时也保证了即便是其中的一个定时任务挂掉了,到一定的时间以后,锁也会释放,其他的定时任务依旧会进行执行权的争夺,执行定时任务。

使用步骤

  1. 环境支持
    Java 8 、slf4j-api、Spring Framework(可选)

  2. 依赖引入

<dependency>
  <groupId>net.javacrumbs.shedlock</groupId>
  <artifactId>shedlock-spring</artifactId>
  <version>0.18.2</version>
</dependency>
  1. 定时任务的写法
import net.javacrumbs.shedlock.core.SchedulerLock;

...

@Scheduled(...)
@SchedulerLock(name = "scheduledTaskName")
public void scheduledTask() {
   // do something
}

The @SchedulerLock annotation has several purposes. First of all, only annotated methods are locked, the library ignores all other scheduled tasks. You also have to specify the name for the lock. Only one tasks with the same name can be executed at the same time.

You can also set lockAtMostFor attribute which specifies how long the lock should be kept in case the executing node dies. This is just a fallback, under normal circumstances the lock is released as soon the tasks finishes.

Lastly, you can set lockAtLeastFor attribute which specifies minimum amount of time for which the lock should be kept. Its main purpose is to prevent execution from multiple nodes in case of really short tasks and clock difference between the nodes.

  • 示例
    Let's say you have a task which you execute every 15 minutes and which usually takes few minutes to run. Moreover, you want to execute it at most once per 15 minutes. In such case, you can configure it like this
import net.javacrumbs.shedlock.core.SchedulerLock;

...
private static final int FOURTEEN_MIN = 14 * 60 * 1000;
...

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

By setting lockAtMostFor we make sure that the lock is released even if the node dies and by setting lockAtLeastFor we make sure it's not executed more than once in fifteen minutes. Please note that if the task takes longer than 15 minutes, it will be executed again.

  1. 配置task
    Now we need to integrate the library into Spring. It's done by wrapping standard Spring task scheduler.
import net.javacrumbs.shedlock.spring.SpringLockableTaskSchedulerFactory;

...
@Bean
public ScheduledLockConfiguration taskScheduler(LockProvider lockProvider) {
    return ScheduledLockConfigurationBuilder
        .withLockProvider(lockProvider)
        .withPoolSize(10)
        .withDefaultLockAtMostFor(Duration.ofMinutes(10))
        .build();
}

Or if you already have an instance of ScheduledExecutorService

@Bean
public TaskScheduler taskScheduler(ScheduledExecutorService executorService, LockProvider lockProvider) {
    return SpringLockableTaskSchedulerFactory.newLockableTaskScheduler(executorService, lockProvider, Duration.of(10, MINUTES));
}
  1. 配置锁
    锁的配置有多种方式的,如下的。
  • Mongo

    • Import the project
    <dependency>
        <groupId>net.javacrumbs.shedlock</groupId>
        <artifactId>shedlock-provider-mongo</artifactId>
        <version>0.18.2</version>
    </dependency>
    
    • Configure:
    import net.javacrumbs.shedlock.provider.mongo.MongoLockProvider;
    
    ...
    
    @Bean
    public LockProvider lockProvider(MongoClient mongo) {
        return new MongoLockProvider(mongo, "databaseName");
    }
    

    Please note that MongoDB integration requires Mongo >= 2.4 and mongo-java-driver >= 3.4.0

  • JdbcTemplate

    • Create the table
    CREATE TABLE shedlock(
      name VARCHAR(64), 
      lock_until TIMESTAMP(3) NULL, 
      locked_at TIMESTAMP(3) NULL, 
      locked_by  VARCHAR(255), 
      PRIMARY KEY (name)
    ) 
    
    • script for MS SQL is here
    CREATE TABLE [dbo].[shedlock](
      [name] [varchar](64) NOT NULL,
      [lock_until] [datetime] NULL,
      [locked_at] [datetime] NULL,
      [locked_by] [varchar](255) NOT NULL,
     CONSTRAINT [PK_shedlock] PRIMARY KEY CLUSTERED 
    (
      [name] ASC
    )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
    ) ON [PRIMARY]
    
    • Add dependency
    <dependency>
        <groupId>net.javacrumbs.shedlock</groupId>
        <artifactId>shedlock-provider-jdbc-template</artifactId>
        <version>0.18.2</version>
    </dependency>
    
    • Configure:
    import net.javacrumbs.shedlock.provider.jdbctemplate.JdbcTemplateLockProvider;
    
    ...
    
    @Bean
    public LockProvider lockProvider(DataSource dataSource) {
        return new JdbcTemplateLockProvider(dataSource);
    }
    

    Tested with MySql, Postgres and HSQLDB

  • Plain JDBC

    • For those who do not want to use jdbc-template, there is plain JDBC lock provider. Just import
    <dependency>
        <groupId>net.javacrumbs.shedlock</groupId>
        <artifactId>shedlock-provider-jdbc</artifactId>
        <version>0.18.2</version>
    </dependency>
    
    • and configure
    import net.javacrumbs.shedlock.provider.jdbc.JdbcLockProvider;
    
    ...
    
    @Bean
    public LockProvider lockProvider(DataSource dataSource) {
        return new JdbcLockProvider(dataSource);
    }
    

    the rest is the same as with JdbcTemplate lock provider.

    • Warning
      Do not manually delete lock row or document from DB table or Mongo collection. ShedLock has an in-memory cache of existing locks so the row will NOT be automatically recreated until application restart. If you need to, you can edit the row/document, risking only that multiple locks will be held. Since 0.18.2 you can clean the cache by calling clearCache() on LockProvider.
  • ZooKeeper (using Curator)

    • Import
    <dependency>
        <groupId>net.javacrumbs.shedlock</groupId>
        <artifactId>shedlock-provider-zookeeper-curator</artifactId>
        <version>0.18.2</version>
    </dependency>
    
    • and configure
    import net.javacrumbs.shedlock.provider.zookeeper.curator.ZookeeperCuratorLockProvider;
    
    ...
    
    @Bean
    public LockProvider lockProvider(org.apache.curator.framework.CuratorFramework client) {
        return new ZookeeperCuratorLockProvider(client);
    }
    

    By default, ephemeral nodes for locks will be created under /shedlock node.

  • Redis (using Spring RedisConnectionFactory)

    • Import
    <dependency>
        <groupId>net.javacrumbs.shedlock</groupId>
        <artifactId>shedlock-provider-redis-spring</artifactId>
        <version>0.18.2</version>
    </dependency>
    
    • and configure
    import net.javacrumbs.shedlock.provider.redis.spring.RedisLockProvider;
    
    ...
    
    @Bean
    public LockProvider lockProvider(JedisPool jedisPool) {
        return new RedisLockProvider(connectionFactory, ENV);
    }
    
  • Redis (using Jedis)

    • Import
    <dependency>
        <groupId>net.javacrumbs.shedlock</groupId>
        <artifactId>shedlock-provider-redis-jedis</artifactId>
        <version>0.18.0</version>
    </dependency>
    
    • and configure
    import net.javacrumbs.shedlock.provider.redis.jedis.JedisLockProvider;
    
    ...
    
    @Bean
    public LockProvider lockProvider(JedisPool jedisPool) {
        return new JedisLockProvider(jedisPool, ENV);
    }
    
  • Hazelcast

    • Import the project
    <dependency>
        <groupId>net.javacrumbs.shedlock</groupId>
        <artifactId>shedlock-provider-hazelcast</artifactId>
        <version>0.18.0</version>
    </dependency>
    
    • Configure:
    import net.javacrumbs.shedlock.provider.hazelcast.HazelcastLockProvider;
    
    ...
    
    @Bean
    public HazelcastLockProvider lockProvider(HazelcastInstance hazelcastInstance) {
        return new HazelcastLockProvider(hazelcastInstance);
    }
    
  • Spring XML configuration

    • If you are using Spring XML config, use this configuration
    <!-- lock provider of your choice (jdbc/zookeeper/mongo/whatever) -->
    <bean id="lockProvider" class="net.javacrumbs.shedlock.provider.jdbctemplate.JdbcTemplateLockProvider">
        <constructor-arg ref="dataSource"/>
    </bean>
    
    <bean id="scheduler" class="net.javacrumbs.shedlock.spring.SpringLockableTaskSchedulerFactoryBean">
        <constructor-arg>
            <task:scheduler id="sch" pool-size="10"/>
        </constructor-arg>
        <constructor-arg ref="lockProvider"/>
        <constructor-arg name="defaultLockAtMostFor">
            <bean class="java.time.Duration" factory-method="ofMinutes">
                <constructor-arg value="10"/>
            </bean>
        </constructor-arg>
    </bean>
    
    
    <!-- Your task(s) without change (or annotated with @Scheduled)-->
    <task:scheduled-tasks scheduler="scheduler">
        <task:scheduled ref="task" method="run" fixed-delay="1" fixed-rate="1"/>
    </task:scheduled-tasks>
    
    • Annotate scheduler method(s)
    @SchedulerLock(name = "taskName")
    public void run() {
    
    }
    
  • Running without Spring

    • It is possible to use ShedLock without Spring
    LockingTaskExecutor executor = new DefaultLockingTaskExecutor(lockProvider);
    
    ...
    
    Instant lockAtMostUntil = Instant.now().plusSeconds(600);
    executor.executeWithLock(runnable, new LockConfiguration("lockName", lockAtMostUntil));
    
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,033评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,725评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,473评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,846评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,848评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,691评论 1 282
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,053评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,700评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,856评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,676评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,787评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,430评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,034评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,990评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,218评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,174评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,526评论 2 343

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,598评论 18 139
  • 配方:西红柿500克,番茄沙司100克,黄油30克,洋葱100克,盐6克,糖15克,黑胡椒粉1克,披萨草1克,罗勒...
    鹤舞霏扬阅读 117评论 0 0
  • 一般情况下有三种需求: 1、跳转到自己应用的设置页面 2、跳转到具体的设置页面(比如:Wife、蓝牙) 下面是常用...
    我是七月阅读 8,287评论 5 36
  • 今天又见到了小琴,颇感亲切,言语间忽然间明白一件事。 开始还要从约瑟夫·博伊斯(Joseph Beuys)的作品《...
    感性进化阅读 863评论 0 0