轻量级分布式定时器ShedLock使用记录

前言

单实例的定时器有很多,spring 自带的scheduled或者使用框架quartz等。当存在多个实例的时候,如果代码不做修改,那么同一时间,将会执行多个同样逻辑的任务,如果当中存在增删改等操作,这必定是是会影响我们的业务的。所以我们要把普通定时器扩展成分布式的定时器。
我个人觉得分布式定时器需要具备的重要的两点:

  1. 同一时间,项目里只有一个定时任务在执行,其他的需要等待下个回合进行执行任务机会的抢夺。
  2. 某个执行任务的线程断掉了,下一回合的其他线程可以获得执行机会。

解决方案:使用轻量级的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);
            }
        }
}

这里的锁有一个逻辑,加锁默认使用最长加锁时间,
解锁的时候有两种:

  1. 还未到最短的加锁时间时,修改redis key的过期时间是最少加锁时间
  2. 已经超过了最短的加锁时间时,直接删除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 &lt;= 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 参考链接

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值