集群模式下定时任务重复执行解决方案

2023-02-28

集群模式下定时任务重复执行解决方案

1、Redisson分布式锁

  1. 基于Redis方式实现存在一些问题,包括原子性、不可重入等问题,而这些问题使用Redisson都可以解决

    可重入锁:可重入就是说某个线程已经获得某个锁,可以再次获取锁而不会出现死锁。

    为什么要用重入锁?一个方法里,外层方法占用了锁,但是里面还有方法要获得锁,如果不是重入锁,程序无法继续运行,陷入死锁,是重入锁就继续执行。参考

    常见的可重入锁有:Synchronzed、ReentrantLock

  2. 什么是Redisson?

    Redisson是架设在Redis基础上的一个Java驻内存数据网格(In-Memory Data Grid)。充分的利用了Redis键值数据库提供的一系列优势,基于Java实用工具包中常用接口,为使用者提供了一系列具有分布式特性的常用工具类。使得原本作为协调单机多线程并发程序的工具包获得了协调分布式多机多线程并发系统的能力,大大降低了设计和研发大规模分布式系统的难度。同时结合各富特色的分布式服务,更进一步简化了分布式环境中程序相互之间的协作。

  3. 加入Redisson

    1、引入Jar包

    <dependency>
        <groupId>org.redisson</groupId>
        <artifactId>redisson</artifactId>
        <version>3.10.1</version>
    </dependency>
    

    2、创建配置文件

    @Component
    @Slf4j
    public class RedissonConfig {
       
        private Redisson redisson = null;
        private Config config = new Config();
       
        @Value("${redis.hostAndPort}")
        private String hostAndPort;
       
        @PostConstruct
        private void init() {
            try {
                config.useSingleServer().setAddress(new StringBuilder().append("redis://").append(hostAndPort).toString());
                redisson = (Redisson) Redisson.create(config);
                log.info("Redisson 初始化完成");
            } catch (Exception e) {
                log.error("init Redisson error ",e);
            }
        }
       
        public Redisson getRedisson() {
            return redisson;
        }
    }
    

    3、使用

    核心使用方法

    // 注入
    @Autowired
    private RedissonConfig redissonConfig;
       
    // 获取redission锁对象
    RLock lock = redissonConfig.getRedisson().getLock(defKey);
       
    // 尝试获取锁 true表示获取成功,其他服务器没有执行定时任务,反则正在执行
    // tryLock(long waitTime, long leaseTime, TimeUnit unit)
    boolean getLock =  lock.tryLock(0, expireTime, TimeUnit.SECONDS);
       
    // 解锁
    lock.unlock();
    

    tryLock()方法参数解释:在获取该锁时如果被其他线程先拿到锁就会进入等待,等待waitTime时间,如果还没用机会获取到锁就放弃,返回false;若获得了锁,除非是调用unlock释放,那么会一直持有锁,直到超过leaseTime指定的时间

    完整代码:

    • 在finally中释放锁,保证任务执行成功或失败,都能及时释放锁
    // 注入
    @Autowired
    private RedissonConfig redissonConfig;
       
    // ...
       
    // 获取redission,true表示获取成功,其他服务器没有执行定时任务,反则正在执行
    RLock lock = redissonConfig.getRedisson().getLock(defKey);
    boolean getLock = false;
    try {
        // 尝试获取锁
        getLock =  lock.tryLock(0, expireTime, TimeUnit.SECONDS);
        if (getLock) {
            log.info("获得分布式锁成功! key:{}", defKey);
          	// 执行业务方法
            pjp.proceed();
        } else {
            log.info("{}已在机器上占用分布式锁,聚类任务正在执行", defKey);
        }
    } catch (InterruptedException e) {
        log.error("定时任务执行失败:" + e);
    } finally {
        if (Boolean.TRUE.equals(getLock)) {
            Thread.sleep(5000);
            lock.unlock();
            log.info("Redisson分布式锁释放锁:{}", defKey)
        }
    }
    

参考:

https://blog.csdn.net/sinat_25295611/article/details/80420086

https://segmentfault.com/a/1190000023038777

https://www.cnblogs.com/liuqingzheng/p/11080501.html

2、@ConditionalOnProperty仅单台服务执行定时任务

  • 使用@ConditionalOnProperty(prefix = "schedule", name = "enabled", havingValue = "true")注解控制Bean是否生效。

    当配置文件配置如下时,该定时任务正常执行:

    schedule:
    	enabled: true
    

    当配置文件配置如下时,该定时任务不执行,且后台查询不到该Bean,从而实现指定服务去执行定时任务。

    schedule:
    	enabled: false