【SpringBoot实战】分布式定时任务锁Shedlock

本文介绍了如何使用Shedlock实现Spring Boot应用中的分布式定时任务锁,通过依赖Redis作为公共存储,确保定时任务在分布式环境中只执行一次。文章详细阐述了Shedlock的配置、使用方法以及核心原理,包括锁的获取和释放过程,并提供了代码示例。
摘要由CSDN通过智能技术生成

在我们业务开发过程中,经常会有需求做一些定时任务,但是由于定时任务的特殊性,以及一些方法的幂等性要求,在分布式多节点部署的情况下,某个定时任务只需要执行一次。

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 文件中加入依赖:

 
  1. <dependency>

  2.  <groupId>net.javacrumbs.shedlock</groupId>

  3.  <artifactId>shedlock-spring</artifactId>

  4.  <version>4.14.0</version>

  5. </dependency>

  6. <dependency>

  7.  <groupId>net.javacrumbs.shedlock</groupId>

  8.  <artifactId>shedlock-provider-redis-spring</artifactId>

  9.  <version>4.14.0</version>

  10. </dependency>

开启定时任务锁:

 
  1. @Configuration

  2. @EnableScheduling

  3. @EnableSchedulerLock(defaultLockAtMostFor = "PT30S")

  4. public class ShedlockConfig {

  5.     @Bean

  6.     public LockProvider lockProvider(RedisTemplate redisTemplate) {

  7.         return new RedisLockProvider(redisTemplate.getConnectionFactory());

  8.     }

  9. }

defaultLockAtMostFor = “PT30S” 表示默认锁的最大占用时间是 30s;

其次,在定时任务方法上,加上注解 @SchedulerLock:

 
  1. /**

  2.  * 通过设置lockAtMostFor,我们可以确保即使节点死亡,锁也会被释放;

  3.  * 通过设置lockAtLeastFor,我们可以确保它在30s内不会执行超过一次;

  4.  */

  5. @Scheduled(cron = "00 12 15 22 * ?")

  6. @SchedulerLock(name = "testTask-1", lockAtMostFor = "30s", lockAtLeastFor = "10s")

  7. public void testTask1() {

  8.     LockAssert.assertLocked();

  9.     log.info("exec testTask1......");

  10. }

  11. @Scheduled(fixedRate = 10000L)

  12. @SchedulerLock(name = "testTask-2", lockAtMostFor = "10s", lockAtLeastFor = "2s")

  13. public void testTask2() {

  14.     LockAssert.assertLocked();

  15.     log.info("exec testTask2......");

  16. }

启动多个节点,会发现,每次定时任务只有一个节点执行,定时任务执行后,在 Redis 里会看到两个 key:job-lock:default:testTask-1 和 job-lock:default:testTask-2。

[2]3. Shedlock 原理分析

Shedlock 通过 AOP,拿到 TaskScheduler 的行为做代理,并加入分布式锁实现所需要的功能。

上锁入口在 RedisLockProvider.java:

 
  1. @NonNull

  2. public Optional<SimpleLock> lock(@NonNull LockConfiguration lockConfiguration) {

  3.     String key = this.buildKey(lockConfiguration.getName());

  4.     Expiration expiration = getExpiration(lockConfiguration.getLockAtMostUntil());

  5.     return Boolean.TRUE.equals(tryToSetExpiration(this.redisTemplate, key, expiration, SetOption.SET_IF_ABSENT)) ? Optional.of(new RedisLockProvider.RedisLock(key, this.redisTemplate, lockConfiguration)) : Optional.empty();

  6. }

  7. private static Boolean tryToSetExpiration(StringRedisTemplate template, String key, Expiration expiration, SetOption option) {

  8.     return (Boolean)template.execute((connection) -> {

  9.         byte[] serializedKey = template.getKeySerializer().serialize(key);

  10.         byte[] serializedValue = template.getValueSerializer().serialize(String.format("ADDED:%s@%s", Utils.toIsoString(ClockProvider.now()), Utils.getHostname()));

  11.         return connection.set(serializedKey, serializedValue, expiration, option);

  12.     }, false);

  13. }

可以看出上锁,其实就是 Redis 的 set 操作的过程。

任务执行的入口,可以参考 net.javacrumbs.shedlock.core.DefaultLockingTaskExecutor:

 
  1. @Override

  2. @NonNull

  3. public <T> TaskResult<T> executeWithLock(@NonNull TaskWithResult<T> task, @NonNull LockConfiguration lockConfig) throws Throwable {

  4.     Optional<SimpleLock> lock = lockProvider.lock(lockConfig);

  5.     String lockName = lockConfig.getName();

  6.     if (alreadyLockedBy(lockName)) {

  7.         logger.debug("Already locked '{}'", lockName);

  8.         return TaskResult.result(task.call());

  9.     } else if (lock.isPresent()) {

  10.         try {

  11.             LockAssert.startLock(lockName);

  12.             logger.debug("Locked '{}', lock will be held at most until {}", lockName, lockConfig.getLockAtMostUntil());

  13.             return TaskResult.result(task.call());

  14.         } finally {

  15.             LockAssert.endLock();

  16.             lock.get().unlock();

  17.             if (logger.isDebugEnabled()) {

  18.                 Instant lockAtLeastUntil = lockConfig.getLockAtLeastUntil();

  19.                 Instant now = ClockProvider.now();

  20.                 if (lockAtLeastUntil.isAfter(now)) {

  21.                     logger.debug("Task finished, lock '{}' will be released at {}", lockName, lockAtLeastUntil);

  22.                 } else {

  23.                     logger.debug("Task finished, lock '{}' released", lockName);

  24.                 }

  25.             }

  26.         }

  27.     } else {

  28.         logger.debug("Not executing '{}'. It's locked.", lockName);

  29.         return TaskResult.notExecuted();

  30.     }

  31. }

首先判断 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

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

万事俱备,就差一个程序员了

谢谢您,赏俺根辣条,尝尝鲜.谢

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值