前言
单实例的定时器有很多,spring 自带的scheduled或者使用框架quartz等。当存在多个实例的时候,如果代码不做修改,那么同一时间,将会执行多个同样逻辑的任务,如果当中存在增删改等操作,这必定是是会影响我们的业务的。所以我们要把普通定时器扩展成分布式的定时器。
我个人觉得分布式定时器需要具备的重要的两点:
- 同一时间,项目里只有一个定时任务在执行,其他的需要等待下个回合进行执行任务机会的抢夺。
- 某个执行任务的线程断掉了,下一回合的其他线程可以获得执行机会。
解决方案:使用轻量级的ShedLock-Spring进行处理
原理:使用共享数据库实现分布式锁,每次进行执行机会的抢夺都对同一条数据进行插入/修改,操作成功的获取执行机会并对数据加锁,进行业务逻辑的执行,执行完毕对数据进行解锁。未抢夺成功得线程在下一次轮回继续争夺数据得操作机会。
依赖引入
<!--关键依赖-->
<dependency>
<groupId>net.javacrumbs.shedlock</groupId>
<artifactId>shedlock-spring</artifactId>
<version>4.34.0</version>
</dependency>
<!--使用redis实现,引入redis支持包-->
<dependency>
<groupId>net.javacrumbs.shedlock</groupId>
<artifactId>shedlock-provider-redis-spring</artifactId>
<version>4.34.0</version>
</dependency>
<!-- 使用jdbc数据库连接,引入支持包-->
<dependency>
<groupId>net.javacrumbs.shedlock</groupId>
<artifactId>shedlock-provider-jdbc-template</artifactId>
<version>4.34.0</version>
</dependency>
配置类
@Configuration
@Slf4j
public class SchedulerConfiguration {
//默认使用的是beanName=lockProvider的支持驱动,spring中只要有一个beanName=lockProvider,不管定义顺序先后都会使用它
//如果找不到指定名称的驱动,就根据类型匹配,此时如果数据库里面出现了两个类型一样的LockProvider的bean,执行任务会报错,如果只有一个就使用唯一类型的,也可用@Primary注解指定优先使用的驱动
@Bean
public LockProvider mysqlLockProvider(DataSource dataSource) {
return new JdbcTemplateLockProvider(dataSource);
}
@Bean
@Primary
//我不想创建表。所以我优先使用了redis的驱动支持类
public LockProvider redisLockProvider(RedisConnectionFactory redisConnectionFactory) {
return new RedisLockProvider(redisConnectionFactory);
}
}
使用
@Configuration
@Slf4j
public class ElasticSearchUtil {
/**
* 使用cron表达式 每十秒执行一次
* name必须唯一,不然会出现错误,推荐使用类名+方法名
* lockAtLeastFor 数据至少加锁时间,这里需要注意,假如你每十秒执行一次方法,但是至少锁定时间是30s,那么无法完成每十秒执行一次的定时
*/
@Scheduled(cron = "0/10 * * * * ?")
@SchedulerLock(name = "ElasticSearchUtil_scheduledCreateEsIndex", lockAtLeastFor = "PT5S")
public void scheduledCreateEsIndex() {
log.info("我来了");
}
}
启动类上需要添加注解
//开启定时任务
@EnableScheduling
//开启shedlock锁,defaultLockAtMostFor全局配置,数据最多锁多久
@EnableSchedulerLock(defaultLockAtMostFor = "PT30S")
public class TestScheduledApplication{
public static void main(String[] args) {
SpringApplication.run(XiotDeviceApplication.class, args);
}
}
打断点查看调用堆栈,查找net.javacrumbs.shedlock这个包名下的方法,可以查看执行逻辑
执行的源代码
public class DefaultLockingTaskExecutor implements LockingTaskExecutor {
@NonNull
public <T> TaskResult<T> executeWithLock(@NonNull TaskWithResult<T> task, @NonNull LockConfiguration lockConfig) throws Throwable {
//根据配置(redis/jdbc)尝试加锁,加锁成功返回锁,加锁失败返回null
Optional<SimpleLock> lock = lockProvider.lock(lockConfig);
String lockName = lockConfig.getName();
//判断是否早就获取了锁(加锁成功,当前线程会设置锁名,以此判断是否可以执行)
if (alreadyLockedBy(lockName)) {
logger.debug("Already locked '{}'", lockName);
return TaskResult.result(task.call());
//否则根据lock判断是否枷锁成功
} else if (lock.isPresent()) {
try {
//加锁1 当前线程设置锁名
LockAssert.startLock(lockName);
//加锁2 当前线程设置锁
LockExtender.startLock(lock.get());
logger.debug("Locked '{}', lock will be held at most until {}", lockName, lockConfig.getLockAtMostUntil());
//执行任务
return TaskResult.result(task.call());
} finally {
//线程解锁1
LockAssert.endLock();
//lock 解锁
SimpleLock activeLock = LockExtender.endLock();
if (activeLock != null) {
activeLock.unlock();
} else {
// This should never happen, but I do not know any better way to handle the null case.
logger.warn("No active lock, please report this as a bug.");
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();
}
}
}
加锁逻辑 redis实现
public class RedisLockProvider implements LockProvider {
@Override
@NonNull
public Optional<SimpleLock> lock(@NonNull LockConfiguration lockConfiguration) {
//生成redis key,我的redis key名 job-lock:default:ElasticSearchUtil_scheduledCreateEsIndex
String key = buildKey(lockConfiguration.getName());
//根据配置获取key的失效时间
Expiration expiration = getExpiration(lockConfiguration.getLockAtMostUntil());
//尝试添加key和过期时间,条件:key不存在,存在的时候说明已经被其他定时器加锁,且还未解锁。不存在说明还未被加锁
if (TRUE.equals(tryToSetExpiration(redisTemplate, key, expiration, SET_IF_ABSENT))) {
//成功返回锁
return Optional.of(new RedisLock(key, redisTemplate, lockConfiguration));
} else {
return Optional.empty();
}
}
}
//redisLock解锁方法
private static final class RedisLock extends AbstractSimpleLock {
@Override
public void doUnlock() {
//
Expiration keepLockFor = getExpiration(lockConfiguration.getLockAtLeastUntil());
// lock at least until is in the past
if (keepLockFor.getExpirationTimeInMilliseconds() <= 0) {
try {
redisTemplate.delete(key);
} catch (Exception e) {
throw new LockException("Can not remove node", e);
}
} else {
tryToSetExpiration(this.redisTemplate, key, keepLockFor, SetOption.SET_IF_PRESENT);
}
}
}
这里的锁有一个逻辑,加锁默认使用最长加锁时间,
解锁的时候有两种:
- 还未到最短的加锁时间时,修改redis key的过期时间是最少加锁时间
- 已经超过了最短的加锁时间时,直接删除redis key
加锁逻辑 jdbc实现
public class StorageBasedLockProvider implements ExtensibleLockProvider {
@Override
@NonNull
public Optional<SimpleLock> lock(@NonNull LockConfiguration lockConfiguration) {
boolean lockObtained = doLock(lockConfiguration);
if (lockObtained) {
return Optional.of(new StorageLock(lockConfiguration, storageAccessor));
} else {
return Optional.empty();
}
}
/**
* 加锁set lockUntil当lockUntil<=now
* Sets lockUntil according to LockConfiguration if current lockUntil <= now
*/
protected boolean doLock(LockConfiguration lockConfiguration) {
String name = lockConfiguration.getName();
if (!lockRecordRegistry.lockRecordRecentlyCreated(name)) {
// create record in case it does not exist yet
//数据不存在的话首先创建,创建成功,代表获得锁
if (storageAccessor.insertRecord(lockConfiguration)) {
lockRecordRegistry.addLockRecord(name);
// we were able to create the record, we have the lock
return true;
}
// we were not able to create the record, it already exists, let's put it to the cache so we do not try again
//有数据的时候,记录到缓存,这样下一次救不用尝试插入数据
lockRecordRegistry.addLockRecord(name);
}
// let's try to update the record, if successful, we have the lock
//然后尝试更新数据
return storageAccessor.updateRecord(lockConfiguration);
}
}
//插入语句和更新语句,解锁语句
class SqlStatementsSource {
String getInsertStatement() {
return "INSERT INTO " + this.tableName() + "(" + this.name() + ", " + this.lockUntil() + ", " + this.lockedAt() + ", " + this.lockedBy() + ") VALUES(:name, :lockUntil, :now, :lockedBy)";
}
public String getUpdateStatement() {
return "UPDATE " + this.tableName() + " SET " + this.lockUntil() + " = :lockUntil, " + this.lockedAt() + " = :now, " + this.lockedBy() + " = :lockedBy WHERE " + this.name() + " = :name AND " + this.lockUntil() + " <= :now";
}
public String getUnlockStatement() {
return "UPDATE " + this.tableName() + " SET " + this.lockUntil() + " = :unlockTime WHERE " + this.name() + " = :name";
}
}
以上就是ShedLock-Spring的简单使用和部分源码解读,感谢查看
附上参考链接
shedlock-spring 参考链接