天天看點

【SpringBoot實戰】分布式定時任務鎖Shedlock1. 背景介紹[1]2. Shedlock 實作[2]3. Shedlock 原理分析

在我們業務開發過程中,經常會有需求做一些定時任務,但是由于定時任務的特殊性,以及一些方法的幂等性要求,在分布式多節點部署的情況下,某個定時任務隻需要執行一次。

1. 背景介紹

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 從嚴格意義上來說不是一個分布式任務排程架構,而是一個分布式鎖。所謂的分布式鎖,解決的核心問題就是各個節點中無法通信的痛點。各個節點并不知道這個定時任務有沒有被其他節點的定時器執行,是以理論上隻需要有一個各個節點都能夠通路到的資源,用這個資源去标記這個定時任務有沒有執行就可以了。

[1]2. 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>
 <version>4.14.0</version>
</dependency>
<dependency>
 <groupId>net.javacrumbs.shedlock</groupId>
 <artifactId>shedlock-provider-redis-spring</artifactId>
 <version>4.14.0</version>
</dependency>
           

開啟定時任務鎖:

@Configuration
@EnableScheduling
@EnableSchedulerLock(defaultLockAtMostFor = "PT30S")
public class ShedlockConfig {
    @Bean
    public LockProvider lockProvider(RedisTemplate redisTemplate) {
        return new RedisLockProvider(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。

[2]3. Shedlock 原理分析

Shedlock 通過 AOP,拿到 TaskScheduler 的行為做代理,并加入分布式鎖實作所需要的功能。

【SpringBoot實戰】分布式定時任務鎖Shedlock1. 背景介紹[1]2. Shedlock 實作[2]3. Shedlock 原理分析

上鎖入口在 RedisLockProvider.java:

@NonNull
public Optional<SimpleLock> lock(@NonNull LockConfiguration lockConfiguration) {
    String key = this.buildKey(lockConfiguration.getName());
    Expiration expiration = getExpiration(lockConfiguration.getLockAtMostUntil());
    return Boolean.TRUE.equals(tryToSetExpiration(this.redisTemplate, key, expiration, SetOption.SET_IF_ABSENT)) ? Optional.of(new RedisLockProvider.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()));
        return connection.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);
        return TaskResult.result(task.call());
    } else if (lock.isPresent()) {
        try {
            LockAssert.startLock(lockName);
            logger.debug("Locked '{}', lock will be held at most until {}", lockName, lockConfig.getLockAtMostUntil());
            return TaskResult.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);
        return TaskResult.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

【SpringBoot實戰】分布式定時任務鎖Shedlock1. 背景介紹[1]2. Shedlock 實作[2]3. Shedlock 原理分析

繼續閱讀