定時任務一般需要分佈式鎖進行任務同步,否則容易出現多個節點處理同一任務的情況。
本系列講解使用並強化shedlock來實現分佈式redis鎖,主要需要完成的內容包括:
- 實現程序意外退出時,使用鉤子函數刪除redis分佈式鎖;
- 實現可重入鎖;
- 實現鎖的細分,例如,要批量處理商品,可以根據商品ID來設置鎖,多個節點可以同時執行商品的處理任務,只是每個節點處理的商品不同;
- 實現鎖續約,如果定時任務耗時較長,且時間動態變動,變動的範圍又無法預估,在這種情況下,鎖的超時時間是無法有效設置的,過早釋放鎖,會導致同一批任務又被其他節點再次處理,如果處理接口無法保證冪等性,則會導致數據的不一致,即使批處理接口能保證冪等性,也有可能導致兩個節點同時處理一批任務而造成死鎖等問題。
本次主要講解環境的搭建,並解決第一個問題,即實現程序意外退出時,使用鉤子函數刪除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