SpringBoot定時任務之分佈式鎖(1)

    定時任務一般需要分佈式鎖進行任務同步,否則容易出現多個節點處理同一任務的情況。

    本系列講解使用並強化shedlock來實現分佈式redis鎖,主要需要完成的內容包括:

  1. 實現程序意外退出時,使用鉤子函數刪除redis分佈式鎖;
  2. 實現可重入鎖;
  3. 實現鎖的細分,例如,要批量處理商品,可以根據商品ID來設置鎖,多個節點可以同時執行商品的處理任務,只是每個節點處理的商品不同;
  4. 實現鎖續約,如果定時任務耗時較長,且時間動態變動,變動的範圍又無法預估,在這種情況下,鎖的超時時間是無法有效設置的,過早釋放鎖,會導致同一批任務又被其他節點再次處理,如果處理接口無法保證冪等性,則會導致數據的不一致,即使批處理接口能保證冪等性,也有可能導致兩個節點同時處理一批任務而造成死鎖等問題。

    本次主要講解環境的搭建,並解決第一個問題,即實現程序意外退出時,使用鉤子函數刪除redis分佈式鎖。

  •     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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.2.5.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.springboot</groupId>
    <artifactId>shedlock</artifactId>
    <version>1.0.0</version>
    <name>shedlock</name>
    <description>distribute lock</description>

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

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

        <!--shedlock依賴-->
        <dependency>
            <groupId>net.javacrumbs.shedlock</groupId>
            <artifactId>shedlock-spring</artifactId>
            <version>4.9.2</version>
        </dependency>
        <!--shedlock實現redis分佈式鎖-->
        <dependency>
            <groupId>net.javacrumbs.shedlock</groupId>
            <artifactId>shedlock-provider-redis-spring</artifactId>
            <version>4.9.2</version>
        </dependency>

        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
            <version>3.4</version>
            <type>pom</type>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <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>
  • properties配置文件
shedlock.redis.host=127.0.0.1
shedlock.redis.port=6379
shedlock.redis.mode=STANDALONE
shedlock.redis.password=
shedlock.redis.timeout=100s
  • redis配置:三種模式的配置
package com.springboot.shedlock.config.redis;

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
import io.lettuce.core.ClientOptions;
import io.lettuce.core.TimeoutOptions;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.*;
import org.springframework.data.redis.connection.lettuce.LettuceClientConfiguration;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import org.springframework.util.StringUtils;

import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;


@Configuration
@EnableConfigurationProperties(RedisProperties.class)
@ConditionalOnProperty("shedlock.redis.host")
@Slf4j
public class CommonRedisConfig {

    @Bean
    public RedisConnectionFactory redisConnectionFactory(RedisProperties properties) {
        ClientOptions build = ClientOptions.builder()
                .autoReconnect(true)
                .timeoutOptions(TimeoutOptions.enabled(properties.getTimeout()))
                .build();

        LettuceClientConfiguration clientConfiguration = LettuceClientConfiguration.builder()
                .clientName("my-redis")
                .clientOptions(build)
                .build();
        RedisProperties.RedisMode mode = properties.getMode();
        switch (mode) {
            case CLUSTER:
                return createCluster(properties, clientConfiguration);
            case SENTINEL:
                return createSentinel(properties, clientConfiguration);
            case STANDALONE:
                return createStandAlone(properties, clientConfiguration);
            default:
                throw new IllegalArgumentException("redisMode[STANDALONE|SENTINEL|CLUSTER]:" + mode);
        }
    }

    private LettuceConnectionFactory createCluster(RedisProperties properties,
                                                   LettuceClientConfiguration clientConfiguration) {
        RedisClusterConfiguration conf = new RedisClusterConfiguration();
        String[] hosts = StringUtils.split(properties.getHost(), ",");
        String[] ports = StringUtils.split(properties.getPort(), ",");
        List<RedisNode> nodes = new ArrayList<>(hosts.length);
        for (int i = 0; i < hosts.length; i++) {
            nodes.add(new RedisClusterNode(hosts[i], Integer.parseInt(ports[i])));
        }
        conf.setClusterNodes(nodes);
        conf.setMaxRedirects(8);
        if (!StringUtils.isEmpty(properties.getPassword())) {
            conf.setPassword(properties.getPassword());
        }

        return new LettuceConnectionFactory(conf, clientConfiguration);
    }


    /**
     * 注意配置第一個節點爲Master
     *
     * @return LettuceConnectionFactory
     */
    private LettuceConnectionFactory createSentinel(RedisProperties properties,
                                                    LettuceClientConfiguration clientConfiguration) {
        RedisSentinelConfiguration configuration = new RedisSentinelConfiguration();
        String[] hosts = StringUtils.split(properties.getHost(), ",");
        String[] ports = StringUtils.split(properties.getPort(), ",");


        for (int i = 0; i < hosts.length; i++) {
            String host = hosts[i];
            String port = ports[i];
            RedisNode node = new RedisNode(host, Integer.parseInt(port));
            configuration.addSentinel(node);
        }
        configuration.master(properties.getMasterName());

        if (!StringUtils.isEmpty(properties.getPassword())) {
            configuration.setPassword(properties.getPassword());
        }
        return new LettuceConnectionFactory(configuration, clientConfiguration);
    }

    private LettuceConnectionFactory createStandAlone(RedisProperties properties,
                                                      LettuceClientConfiguration clientConfiguration) {
        RedisStandaloneConfiguration configuration = new RedisStandaloneConfiguration();
        if (!StringUtils.isEmpty(properties.getPassword())) {
            configuration.setPassword(properties.getPassword());
        }
        configuration.setHostName(properties.getHost());
        configuration.setPort(Integer.parseInt(properties.getPort()));
        return new LettuceConnectionFactory(configuration, clientConfiguration);
    }

    @Bean(name = "redisTemplate")
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
        RedisTemplate<String, Object> template = new RedisTemplate();
        template.setConnectionFactory(factory);
        setSerializer(template);
        template.afterPropertiesSet();
        return template;
    }

    @Bean(name = "strRedisTemplate")
    public RedisTemplate<String, String> stringRedisTemplate(RedisConnectionFactory factory) {
        RedisTemplate<String, String> template = new RedisTemplate();
        template.setConnectionFactory(factory);
        setSerializer(template);
        template.afterPropertiesSet();
        return template;
    }

    private void setSerializer(RedisTemplate template) {
        Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = getJacksonSerializer();
        template.setValueSerializer(jackson2JsonRedisSerializer);
        template.setKeySerializer(new StringRedisSerializer());
    }

    private Jackson2JsonRedisSerializer<Object> getJacksonSerializer() {
        Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);
        ObjectMapper om = new ObjectMapper();
        om.setSerializationInclusion(JsonInclude.Include.NON_NULL);
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        om.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
        om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        JavaTimeModule javaTimeModule = new JavaTimeModule();
        javaTimeModule.addSerializer(LocalDate.class, new LocalDateSerializer(DateTimeFormatter.ofPattern("yyyy-MM-dd")));
        javaTimeModule.addDeserializer(LocalDate.class, new LocalDateDeserializer(DateTimeFormatter.ofPattern("yyyy-MM-dd")));
        javaTimeModule.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
        javaTimeModule.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
        om.registerModule(javaTimeModule);
        jackson2JsonRedisSerializer.setObjectMapper(om);
        return jackson2JsonRedisSerializer;
    }


}
package com.springboot.shedlock.config.redis;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;

import java.time.Duration;

@Data
@ConfigurationProperties(prefix = "shedlock.redis")
public class RedisProperties {
    public enum RedisMode {
        SENTINEL, STANDALONE, CLUSTER
    }

    private String host;
    private String port = "6379";
    private String password;
    private RedisMode mode = RedisMode.STANDALONE;
    private Duration timeout;
    private String masterName;
}
  • 定時任務配置:使多個定時任務分線程同時啓動,springboot默認的是隻有一個線程,也就是一次只執行一個定時任務
package com.springboot.shedlock.config.scheduler;

import com.springboot.shedlock.scheduler.Job;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.TaskScheduler;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
import org.springframework.util.CollectionUtils;

import java.util.List;

@Configuration
@EnableScheduling
public class ScheduleConfig {

    @Autowired
    private List<Job> jobs;

    @Bean
    public TaskScheduler taskScheduler() {
        ThreadPoolTaskScheduler taskScheduler = new ThreadPoolTaskScheduler();
        taskScheduler.setPoolSize(getCount());
        return taskScheduler;
    }

    public int getCount() {
        int count;

        if(CollectionUtils.isEmpty(jobs)){
            count = 1;
        } else if(jobs.size() > 10){
            count = 10;
        } else {
            count = jobs.size();
        }
        return count;
    }

}
  • redis分佈式鎖配置:提供增強類,提供鎖關閉功能:
package com.springboot.shedlock.config.lock;

import net.javacrumbs.shedlock.core.LockProvider;
import net.javacrumbs.shedlock.provider.redis.spring.RedisLockProvider;
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.connection.RedisConnectionFactory;

@Configuration
@EnableSchedulerLock(defaultLockAtMostFor = "120s")
public class LockConfig {

    @Autowired
    private RedisConnectionFactory redisConnectionFactory;

    @Bean
    public LockProvider lockProvider() {
//        RedisLockProvider redisLockProvider = new RedisLockProvider(redisConnectionFactory);
        RedisLockProviderEnhance redisLockProvider = new RedisLockProviderEnhance(redisConnectionFactory);
        return redisLockProvider;
    }

}
package com.springboot.shedlock.config.lock;

import lombok.extern.slf4j.Slf4j;
import net.javacrumbs.shedlock.provider.redis.spring.RedisLockProvider;
import net.javacrumbs.shedlock.support.LockException;
import net.javacrumbs.shedlock.support.annotation.NonNull;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.StringRedisTemplate;

import java.util.Set;

@Slf4j
public class RedisLockProviderEnhance extends RedisLockProvider {

    private final StringRedisTemplate redisTemplate;

    public RedisLockProviderEnhance(@NonNull RedisConnectionFactory redisConn) {
        super(redisConn);
        redisTemplate = new StringRedisTemplate(redisConn);
    }

    public void unlockByKey(String key) {
        try {

            redisTemplate.delete(key);
        } catch (Exception e) {
            throw new LockException("Can not remove node", e);
        }
    }

    public void unlockByPrefix(String prefix) {
        try {
            Set<String> keys = redisTemplate.keys(prefix + "*");
            log.error("刪除的分佈式鎖包括:" + keys);
            redisTemplate.delete(keys);
        } catch (Exception e) {
            throw new LockException("Can not remove node", e);
        }
    }

}
  • 啓動類:開啓定時任務功能,調用鉤子函數刪除分佈式鎖
package com.springboot.shedlock;

import com.springboot.shedlock.config.lock.RedisLockProviderEnhance;
import net.javacrumbs.shedlock.core.LockProvider;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.scheduling.annotation.EnableScheduling;

@SpringBootApplication
@EnableScheduling
public class ShedlockApplication {

    public static void main(String[] args) {
        ConfigurableApplicationContext context = SpringApplication.run(ShedlockApplication.class, args);
        RedisLockProviderEnhance lockProvider = context.getBean(RedisLockProviderEnhance.class);
        Runtime.getRuntime().addShutdownHook(new Thread(() -> {
            lockProvider.unlockByPrefix("job-lock:default:");
        }));
    }

}
  • 定時任務:所有定時任務繼承同一個接口
package com.springboot.shedlock.scheduler;

public interface Job {

    void exec();
}
package com.springboot.shedlock.scheduler;

import lombok.SneakyThrows;
import net.javacrumbs.shedlock.spring.annotation.SchedulerLock;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;

import java.util.concurrent.TimeUnit;

@Service
public class MyTestTaskScheduler implements Job {

    @SneakyThrows
    @Scheduled(cron = "0/5 * * * * ?")
    @SchedulerLock(name = "myTask", lockAtMostFor="200000", lockAtLeastFor="200000")
    @Override
    public void exec() {
        System.out.println("執行任務...");
        TimeUnit.SECONDS.sleep(10000);
    }
}

全量代碼參考github:https://github.com/JohnZhaowen/shedlock.git

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章