在我們業務開發過程中,經常會有需求做一些定時任務,但是由于定時任務的特殊性,以及一些方法的冪等性要求,在分布式多節點部署的情況下,某個定時任務只需要執行一次。
ShedLock(https://github.com/lukas-krecan/ShedLock) 是一個輕量級的分布式定時任務鎖組件,使用其可以滿足我們上面的技術需求,ShedLock 官方簡單自我介紹:
“
ShedLock makes sure that 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.
”
Shedlock 從嚴格意義上來說不是一個分布式任務調度框架,而是一個分布式鎖。所謂的分布式鎖,解決的核心問題就是各個節點中無法通信的痛點。各個節點并不知道這個定時任務有沒有被其他節點的定時器執行,所以理論上只需要有一個各個節點都能夠訪問到的資源,用這個資源去標記這個定時任務有沒有執行就可以了。
Shedlock 實現分布式鎖,可以依賴如下組件:
JdbcTemplate
Mongo
DynamoDB
DynamoDB 2
ZooKeeper (using Curator)
Redis (using Spring
RedisConnectionFactory)
Redis (using Jedis)
Hazelcast
Couchbase
ElasticSearch
CosmosDB
Cassandra
Multi-tenancy
本文主要以來 Redis 為公共存儲,實現定時任務的分布式鎖。首先,我們假設你的 Spring Boot 項目已經引入了 Redis,在項目的 pom 文件中加入依賴:
<dependency>
?<groupId>net.javacrumbs.shedlock</groupId>
?<artifactId>shedlock-spring</artifactId>
4.14.0
</dependency>
<dependency>
?<groupId>net.javacrumbs.shedlock</groupId>
?<artifactId>shedlock-provider-redis-spring</artifactId>
4.14.0
</dependency>
開啟定時任務鎖:
@Configuration
@EnableScheduling
@EnableSchedulerLock(defaultLockAtMostFor?="PT30S")
public?class?ShedlockConfig?{
????@Bean
????public?LockProvider?lockProvider(RedisTemplate?redisTemplate)?{
returnnewRedisLockProvider(redisTemplate.getConnectionFactory());
????}
}
“
defaultLockAtMostFor = “PT30S” 表示默認鎖的最大占用時間是 30s;
”
其次,在定時任務方法上,加上注解 @SchedulerLock:
/**
?*?通過設置lockAtMostFor,我們可以確保即使節點死亡,鎖也會被釋放;
?*?通過設置lockAtLeastFor,我們可以確保它在30s內不會執行超過一次;
?*/
@Scheduled(cron?="00?12?15?22?*??")
@SchedulerLock(name?="testTask-1",?lockAtMostFor?="30s",?lockAtLeastFor?="10s")
public?void?testTask1()?{
????LockAssert.assertLocked();
log.info("exec?testTask1......");
}
@Scheduled(fixedRate?=10000L)
@SchedulerLock(name?="testTask-2",?lockAtMostFor?="10s",?lockAtLeastFor?="2s")
public?void?testTask2()?{
????LockAssert.assertLocked();
log.info("exec?testTask2......");
}
啟動多個節點,會發現,每次定時任務只有一個節點執行,定時任務執行后,在 Redis 里會看到兩個 key:job-lock:default:testTask-1 和 job-lock:default:testTask-2。
Shedlock 通過 AOP,拿到 TaskScheduler 的行為做代理,并加入分布式鎖實現所需要的功能。
上鎖入口在 RedisLockProvider.java:
@NonNull
public?Optional<SimpleLock>?lock(@NonNull?LockConfiguration?lockConfiguration)?{
????String?key?=?this.buildKey(lockConfiguration.getName());
????Expiration?expiration?=?getExpiration(lockConfiguration.getLockAtMostUntil());
returnBoolean.TRUE.equals(tryToSetExpiration(this.redisTemplate,?key,?expiration,?SetOption.SET_IF_ABSENT))???Optional.of(newRedisLockProvider.RedisLock(key,?this.redisTemplate,?lockConfiguration))?:?Optional.empty();
}
private?static?Boolean?tryToSetExpiration(StringRedisTemplate?template,?String?key,?Expiration?expiration,?SetOption?option)?{
return(Boolean)template.execute((connection)?->?{
byte[]?serializedKey?=?template.getKeySerializer().serialize(key);
byte[]?serializedValue?=?template.getValueSerializer().serialize(String.format("ADDED:%s@%s",?Utils.toIsoString(ClockProvider.now()),?Utils.getHostname()));
returnconnection.set(serializedKey,?serializedValue,?expiration,?option);
},false);
}
可以看出上鎖,其實就是 Redis 的 set 操作的過程。
任務執行的入口,可以參考 net.javacrumbs.shedlock.core.DefaultLockingTaskExecutor:
@Override
@NonNull
public?<T>?TaskResult<T>?executeWithLock(@NonNull?TaskWithResult<T>?task,?@NonNull?LockConfiguration?lockConfig)?throws?Throwable?{
????Optional<SimpleLock>?lock?=?lockProvider.lock(lockConfig);
????String?lockName?=?lockConfig.getName();
if(alreadyLockedBy(lockName))?{
logger.debug("Already?locked?'{}'",?lockName);
returnTaskResult.result(task.call());
}elseif(lock.isPresent())?{
????????try?{
????????????LockAssert.startLock(lockName);
logger.debug("Locked?'{}',?lock?will?be?held?at?most?until?{}",?lockName,?lockConfig.getLockAtMostUntil());
returnTaskResult.result(task.call());
????????}?finally?{
????????????LockAssert.endLock();
????????????lock.get().unlock();
if(logger.isDebugEnabled())?{
????????????????Instant?lockAtLeastUntil?=?lockConfig.getLockAtLeastUntil();
????????????????Instant?now?=?ClockProvider.now();
if(lockAtLeastUntil.isAfter(now))?{
logger.debug("Task?finished,?lock?'{}'?will?be?released?at?{}",?lockName,?lockAtLeastUntil);
}else{
logger.debug("Task?finished,?lock?'{}'?released",?lockName);
????????????????}
????????????}
????????}
}else{
logger.debug("Not?executing?'{}'.?It's?locked.",?lockName);
returnTaskResult.notExecuted();
????}
}
首先判斷 lock 是否可用,然后再執行任務 task.call()。
作者:zhaoyh
來源鏈接:
http://zhaoyh.com.cn/2020/09/22/Spring%20Boot(%E5%85%AB)%E4%B9%8B%E5%AE%9A%E6%97%B6%E4%BB%BB%E5%8A%A1%E9%94%81Shedlock/#more