type
status
date
slug
summary
tags
category
icon
password

1、ShedLock是什么

ShedLock 是解决分布式系统中定时任务并发执行问题的有效工具。它通过分布式锁机制,确保任务的互斥性,避免任务重复执行,提高系统的可靠性和稳定性。对于需要在分布式环境中执行定时任务的应用,ShedLock 提供了一个简单、灵活的解决方案。github 地址是 https://github.com/lukas-krecan/ShedLock/tree/version4
Spring Quartz 和 ShedLock 是用于调度任务的两种不同解决方案,虽然 Spring Quartz 已经提供了分布式调度的功能,但在某些情况下使用 ShedLock 仍然是必要和有益的。以下是一些需要使用 ShedLock 的原因和场景:

1.1 Quartz 的复杂性

Quartz 是一个强大且复杂的调度框架,具有丰富的功能,例如分布式调度、持久化和集群支持。然而,Quartz 的配置和使用相对复杂,需要详细的配置文件和额外的依赖库。如果你的需求相对简单,只需要保证任务的互斥执行,使用 ShedLock 会更加简单和直接。

1.2 简单性和易用性

ShedLock 的设计初衷是提供一种简单、易用的分布式锁解决方案,特别适合用于分布式系统中的定时任务。它的配置和集成比 Quartz 更加简单,开发者可以更快地上手并实现所需功能。

1.3 独立性

ShedLock 可以与 Spring 的 @Scheduled 注解一起使用,不需要引入和配置复杂的调度框架。这对于不需要 Quartz 全部功能的应用来说,是一个很好的选择。使用 ShedLock 可以保持系统的独立性和简单性,而不必引入一个复杂的调度框架。

1.4 灵活性

ShedLock 支持多种存储后端,例如数据库、Redis、ZooKeeper 等。开发者可以根据具体的应用场景选择合适的存储后端,而不必受限于 Quartz 的实现方式。ShedLock 还可以与任何任务执行框架集成,而不仅仅是 Quartz。

1.5 案例场景

以下是一些适合使用 ShedLock 而不是 Quartz 的场景:
  • 轻量级应用:如果你的应用不需要复杂的调度功能,只需要确保任务在分布式环境中不重复执行,使用 ShedLock 更加轻量和高效。
  • 现有系统集成:如果你的现有系统已经在使用 Spring 的 @Scheduled 注解进行任务调度,但需要增加分布式锁的功能,ShedLock 可以无缝集成,而不需要重构为 Quartz。
  • 数据库集成:如果你的应用使用数据库进行锁管理,并希望利用现有的数据库连接池和事务管理,ShedLock 提供了灵活的数据库锁实现。

2、使用ShedLock

第一步:添加 shedlock 以来,如果是使用 Java 8,需要引入 4.44.0 以下的版本。pom 文件添加如下配置:
第二步:例如使用 Mysql 作为数据源,需要添加 shedlock-provider-jdbc-template 依赖。pom文件添加如下配置:
第三步:向数据库中插入表shedlock(必须)。建表语句如下:
第四步:添加配置类,这里设置锁的最大持有时间为 10 分钟。
第五步:添加 @SchedulerLock 到定时器业务方法入口。@SchedulerLock 解释如下:
  • name:锁名称,必须指定,每次只能执行一个具有相同名字的任务,锁名称应该是全局唯一的。
  • lockAtMostFor:设置锁的最大持有时间,为了解决如果持有锁的节点挂了,无法释放锁,其他节点无法进行下一次任务。
  • lockAtMostForString:成功执行任务的节点所能拥有的独占锁的最长时间的字符串表达,例如“PT14M”表示为14分钟
  • lockAtLeastFor:指定保留锁的最短时间。主要目的是在任务非常短的且节点之间存在时钟差异的情况下防止多个节点执行。这个属性是锁的持有时间。设置了多少就一定会持有多长时间,再此期间,下一次任务执行时,其他节点包括它本身是不会执行任务的
  • lockAtLeastForString:成功执行任务的节点所能拥有的独占锁的最短时间的字符串表达,例如“PT14M”表示为14分钟
例如使用 Scheduled 定时发送邮件,在分布式系统中会根据节点数使每个节点发送重复的邮件,这样明显不符合业务要求。本次测试的定时任务代码如下:

3、Shedlock的原理

3.1 切面

通过上面的入门我们可以发现只需要一个注解就可以帮我们解决问题,那么很容易想到注解+AOP的组合。
于是翻找源码发现了SchedulerLockConfigurationSelector
在默认的mode下面我们可以看到MethodProxyLockConfiguration,在这个配置中定义了MethodProxyScheduledLockAdvisor。
MethodProxyScheduledLockAdvisor中定义了切面的逻辑:
这里我们基本就知道SchedulerLock注解的作用了,其实就是切入点。而切面的逻辑都在LockingInterceptor的invoke方法中。
到此我们知道,引入了shedlock后,我们加了注解SchedulerLock的定时任务每次执行都会先进入LockingInterceptor的invoke方法中。

3.2 加锁以及锁原理

利用AOP其实已经解决了代码改动大的问题,这也是为什么说shedlock使用简单的一个原因。
那么它是如何解决重复执行任务的问题呢?
我们直接看invoke方法中的executeWithLock方法,第一步就是加锁,获取到锁后才能执行任务。
最后在finally中释放锁
lockProvider.lock锁逻辑的具体实现都在JdbcTemplateLockProvider extends StorageBasedLockProvider 中逻辑如下:
insertRecord:
第一次执行任务也就是缓存中没有当前任务名称的记录时候会执行insertRecord,主要是像shedlock表中插入一条数据内容如下:
updateRecord:
当前任务如果已经存在记录,则更新锁。更新成功则获取锁成功。条件是current lockUntil <= now。根据任务的名称来查看lock_until是否已经比当前时间小,也就说明上一个任务的锁时间已经到了。
最后来看看释放锁的逻辑:
doUnlock:
释放锁,基于IF条件来 取lockAtLeastForMicros或者当前时间的最大值。
以上所有的SQL都可以在源码中找到,我们使用的是Mysql,因此可以找到具体实现类MySqlServerTimeStatementsSource。

3.3 数据库的变化

最后来看一下数据库中数据的变化:
我们这里配置了 lockAtLeast 是 3s,默认是 0。配置了defaultLockAtMostFor 是30s。
假如两个节点执行在第一次数据插入之后,lock_util和lock_at直接会差30s。
如果另一个节点也在这个时间点内去获取锁,是获取不到的,那么就会执行
如果任务瞬间执行完毕,进行 unlock 后,因为我们配置了 lockAtLeast,那么 lock_util 则会被更新为
Spring嵌套事务导致获取数据库连接死锁问题Docker系列:Docker存储
mcbilla
mcbilla
一个普通的干饭人🍚
Announcement
type
status
date
slug
summary
tags
category
icon
password
🎉欢迎来到飙戈的博客🎉
-- 感谢您的支持 ---
👏欢迎学习交流👏