Spring分布式锁SchedulerLock

3,396 阅读4分钟

简介

  ShedLock的作用,确保任务在同一时刻最多执行一次。如果一个任务正在一个节点上执行,则它将获得一个锁,该锁将阻止从另一个节点(或线程)执行同一任务。如果一个任务已经在一个节点上执行,则在其他节点上的执行不会等待,只需跳过它即可

用法

  • 启用和配置计划
  • 配置锁提供者
  • 定时任务
  • 测试结果

启用和配置计划

maven方式

<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
   <groupId>net.javacrumbs.shedlock</groupId>
   <artifactId>shedlock-spring</artifactId>
   <version>4.23.0</version>
</dependency>
<dependency>
   <groupId>net.javacrumbs.shedlock</groupId>
   <artifactId>shedlock-provider-redis-spring</artifactId>
   <version>2.5.0</version>
</dependency>

spring配置redis

redis:
  #数据库索引
  database: 0
  host: 127.0.0.1
  port: 6379
  password:
  jedis:
    pool:
      #最大连接数
      max-active: 8
      #最大阻塞等待时间(负数表示没限制)
      max-wait: -1
      #最大空闲
      max-idle: 8
      #最小空闲
      min-idle: 0
      #连接超时时间
  timeout: 10000

Redis配置

import org.springframework.cache.annotation.CachingConfigurerSupport;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

/**
 * @ClassName RedisConfig
 * @Description
 * @Author Jiang
 * @Time 2023/7/7 11:12
 * @Version 1.0
 */
@Configuration
@EnableCaching
public class RedisConfig extends CachingConfigurerSupport {
    @Bean(name = "redisTemplate")
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory){

        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        //参照StringRedisTemplate内部实现指定序列化器
        redisTemplate.setConnectionFactory(redisConnectionFactory);
        redisTemplate.setKeySerializer(keySerializer());
        redisTemplate.setHashKeySerializer(keySerializer());
        redisTemplate.setValueSerializer(valueSerializer());
        redisTemplate.setHashValueSerializer(valueSerializer());
        return redisTemplate;
    }

    private RedisSerializer<String> keySerializer(){
        return new StringRedisSerializer();
    }

    //使用Jackson序列化器
    private RedisSerializer<Object> valueSerializer(){
        return new GenericJackson2JsonRedisSerializer();
    }

}

启用SchedulerLock

@EnableScheduling、@EnableSchedulerLock可以放在项目的启动类上,也可以放在项目配置类上,但是一定记得不能缺少,否则分布式锁会失效。(项目中遇到了这个问题排查了一天,最终发现是@EnableSchedulerLock没有配置,哭死


import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;

// 开启定时任务注解
@EnableScheduling
// 开启定时任务锁,默认设置锁最大占用时间为30s
@EnableSchedulerLock(defaultLockAtMostFor = "PT30S")
@SpringBootApplication
public class HelloSpringbootApplication {

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

}

其中 @EnableSchedulerLock(defaultLockAtMostFor = "PT30S") 源码中有这样一段介绍如下,指定在执行节点结束时应保留锁的默认时间使用ISO8601 Duration格式,作用就是在被加锁的节点挂了时,无法释放锁,造成其他节点无法进行下一任务,我们使用注解时候需要给定一个值,还有最后一句,可以在每个ScheduledLock注解中被重写,也就是说我们每个定时任务都可以重新定义时间,来控制每个定时任务。

/**
 * Default value how long the lock should be kept in case the machine which obtained the lock died before releasing it.
 * Can be either time with suffix like 10s or ISO8601 duration as described in {@link java.time.Duration#parse(CharSequence)}, for example PT30S.
 * This is just a fallback, under normal circumstances the lock is released as soon the tasks finishes.
 * Set this to some value much higher than normal task duration. Can be overridden in each ScheduledLock annotation.

 */
String defaultLockAtMostFor();

配置锁提供者

  ShedLock使用Mongo,JDBC数据库,Redis,Hazelcast,ZooKeeper或其他外部存储进行协调,即通过外部存储来实现锁机制。

  本文主要介绍通过Redis实现分布式锁的方式。

import net.javacrumbs.shedlock.core.LockProvider;
import net.javacrumbs.shedlock.spring.annotation.EnableSchedulerLock;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.RedisTemplate;
import net.javacrumbs.shedlock.provider.redis.spring.RedisLockProvider;

/**
 * @ClassName ScheduledLockConfig
 * @Description
 * @Author Jiang
 * @Time 2023/7/7 13:28
 * @Version 1.0
 */
@Configuration
public class ScheduledLockConfig {

    @Autowired
    RedisTemplate redisTemplate;

    @Bean
    public LockProvider lockProvider() {
        return new RedisLockProvider(redisTemplate.getConnectionFactory());
    }

}

定时任务

可根据自己的实际业务编写定时任务

import lombok.extern.slf4j.Slf4j;
import net.javacrumbs.shedlock.core.SchedulerLock;
import org.apache.commons.lang3.time.DateFormatUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

import java.util.Date;
import java.util.concurrent.TimeUnit;

/**
 * @ClassName TestScheduled
 * @Description
 * @Author Jiang
 * @Time 2023/7/7 11:26
 * @Version 1.0
 */
@Slf4j
@Component
public class TestScheduled {

    @Autowired
    RedisTemplate redisTemplate;

    @Scheduled(fixedDelay = 30 * 1000)
    @SchedulerLock(name = "evaluateUnsubmit",  lockAtLeastFor = 5*60*1000 ,lockAtMostFor = 20*60*1000 )
    public void testMethod(){
        log.info("开始执行 {}", DateFormatUtils.format(new Date(), "yyyy-MM-dd HH:mm:ss"));
        try {
            Thread.sleep(100);
            redisTemplate.opsForValue().set("test" + System.currentTimeMillis(),"goodJob",100, TimeUnit.SECONDS);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        log.info("执行完成 {}", DateFormatUtils.format(new Date(), "yyyy-MM-dd HH:mm:ss"));
    }

}

参数说明 @Scheduled

  • fixedDelay:非常好理解,它的间隔时间是根据上次的任务结束的时候开始计时的。方法上设置了fixedDelay=30 * 1000,那么当该方法某一次执行结束后,开始计算时间,当时间达到30秒,就开始再次执行该方法

@SchedulerLock注解主要参数:

  • name:锁的名称,必须保证唯一;
  • lockAtMostFor:成功执行任务的节点所能拥有的独占锁的最长时间,设置的值要保证比定时任务正常执行完成的时间大一些,此属性保证了如果task节点突然宕机,也能在超过设定值时释放任务锁;
  • lockAtLeastFor:成功执行任务的节点所能拥有的独占锁的最短时间,其主要目的是任务执行时间可能很短,执行后如果发上释放锁,可能会造成多个节点执行。

测试结果

项目启动后执行一次任务,在redis里新增了一条job-lock:default:evaluateUnsubmit用来解决多点部署重复执行任务的问题。 1688798447589.jpg

WX20230708-144116@2x.png

工作中遇到了就顺手总结一下,如有什么问题欢迎指正

Stay hungry,Stay foolish