使用shedlock实现分布式定时任务锁

定时器Scheduler在平时使用比较频繁(Spring定时任务:https://www.jianshu.com/p/570d6820b054 )在springboot中,配置好@Scheduled和@EnableScheduling之后,定时器就能正常执行,实现定时任务的功能。

但是在这样的情况下:如果开发的服务需要水平部署实现负载均衡,那么定时任务就会同时在多个服务实例上运行,那么一方面,可能由于定时任务的逻辑处理需要访问公共资源从而造成并发问题;另一方面,就算没有并发问题,那么一个同样的任务多个服务实例同时执行,也会造成资源的浪费。因此需要一种机制来保证多个服务实例之间的定时任务正常、合理地执行。

ShedLock的出现就是为了解决上述问题,它可以保证多个一个定时任务在多个服务实例之间最多只执行一次,是一个在分布式环境中保证定时任务合理执行的框架,我们可以叫它分布式定时任务锁。

ShedLock的实现原理是采用公共存储实现的锁机制,使得同一时间点只有第一个执行定时任务的服务实例能执行成功,并在公共存储中存储"我正在执行任务,从什么时候(预计)执行到什么时候",其他服务实例执行时如果发现任务正在执行,则直接跳过本次执行,从而保证同一时间一个任务只被执行一次。

上面提到的公共存储目前支持的有:

Monogo
DynamoDB
JdbcTemplate
ZooKeeper (using Curator)
Redis (using Spring RedisConnectionFactory)
Redis (using Jedis)
Hazelcast

值得注意的是,ShedLock不是一个分布式的定时任务框架,只是一个锁,用于保证分布式环境中的定时任务合理执行。

这里使用JdbcTemplate,新建springboot项目,pom文件如下:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>poc</artifactId>
        <groupId>com.hkj</groupId>
        <version>0.0.1-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>shedlock</artifactId>

    <properties>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>

        <!-- mysql驱动 -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
            <version>2.2.2.RELEASE</version>
        </dependency>

        <!-- shedlock start -->
        <dependency>
            <groupId>net.javacrumbs.shedlock</groupId>
            <artifactId>shedlock-spring</artifactId>
            <version>4.11.1</version>
        </dependency>

        <dependency>
            <groupId>net.javacrumbs.shedlock</groupId>
            <artifactId>shedlock-provider-jdbc-template</artifactId>
            <version>4.11.1</version>
        </dependency>
        <!-- shedlock end -->

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

shedlock配置类如下:

package com.hkj.config;

import net.javacrumbs.shedlock.core.LockProvider;
import net.javacrumbs.shedlock.provider.jdbctemplate.JdbcTemplateLockProvider;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.core.JdbcTemplate;

import javax.annotation.Resource;
import javax.sql.DataSource;
import java.util.TimeZone;

/**
 * @descrition shedlock配置类
 * @since 2020-06-01 15:19
 */
@Configuration
public class ShedlockConfig {
    @Resource
    private DataSource dataSource;

    /**
     * @description 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 )
     * );
     * @date 2020/6/1 15:19
     */
    @Bean
    public LockProvider lockProvider() {
        return new JdbcTemplateLockProvider(
                JdbcTemplateLockProvider.Configuration.builder()
                        .withJdbcTemplate(new JdbcTemplate(dataSource))
                        .withTimeZone(TimeZone.getTimeZone("GMT+8"))
                        .build()
        );
    }
}

在MySQL中添加一张名为shedlock的表

shedlock表

通过修改数据库表中的数据实现锁。

表字段说明:

主键name:每个定时任务的一个名字
locked_at:锁的开始时间
lock_until:锁的结束时间

再定时开始时,会更新这两个时间,在时间之内的定时是不会被执行的。

yml配置

server:
  port: 8080

spring:
  datasource:
    name: test
    url: jdbc:mysql://127.0.0.1:3306/test?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
    username: root
    password: lovehkj
    driver-class-name: com.mysql.cj.jdbc.Driver

启动类上开启shedlock和定时任务

package com.hkj;

import net.javacrumbs.shedlock.spring.annotation.EnableSchedulerLock;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;

@SpringBootApplication
@EnableScheduling
// 指定锁默认持有时间
@EnableSchedulerLock(defaultLockAtMostFor = "10m")
public class ShedLockStarter {

    public static void main(String[] args) {
        SpringApplication.run(ShedLockStarter.class, args);
    }

}

新建定时任务

package com.hkj.job;

import net.javacrumbs.shedlock.spring.annotation.SchedulerLock;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

import java.text.SimpleDateFormat;
import java.util.Date;

/**
 * @descrition
 * @since 2020-06-01 15:09
 */
@Component
public class TestJob {
    /**
     * @description 每隔1min打印一次
     * @date 2020/6/1 15:10
     */
    @Scheduled(cron = "0 0/1 * * * ?")
    // lockAtMostFor为锁默认持有时间,会覆盖启动类中的默认持有时间
    @SchedulerLock(name = "print", lockAtMostFor = "3m")
    public void print() {
        SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");//设置日期格式
        System.err.println(df.format(new Date()));// new Date()为获取当前系统时间
    }
}

@Scheduled注解有五个参数

name:定时任务的名字,就是数据库中的内个主键
lockAtMostFor:锁的最大时间单位为毫秒
lockAtMostForString:最大时间的字符串形式,例如:PT30S 代表30秒
lockAtLeastFor:锁的最小时间单位为毫秒
lockAtLeastForString:最小时间的字符串形式

分别启动两个实例8080和8081
观察console输出结果:


8080实例

8081实例

此时已经不会发生同一时间段多个实例执行同一调度任务的情况了。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。