集群下的定时任务解决方案——Redis分布式锁

1. 问题描述

描述:同时运行多个相同的服务A,A中有一个定时任务,我们希望即使多个A服务同时运行时在同一个时间段也只有一个定时任务执行。

2. 解决方案

2.1解决方案一

单独开一个服务来运行定时任务,该服务也不做集群部署。

2.2 解决方案二——Redis分布式锁

首先引入相应的依赖

 <dependencies>
        <!--导入jedis-->
        <dependency>
            <groupId>redis.clients</groupId>
            <artifactId>jedis</artifactId>
            <version>3.3.0</version>
        </dependency>
        <!--redisson分布式锁方案-->
        <dependency>
            <groupId>org.redisson</groupId>
            <artifactId>redisson</artifactId>
            <version>3.15.5</version>
        </dependency>
    </dependencies>

先来看一下Jedis中的SETNX方法,reids分布式锁的也是用的SETNX方法

package com.alpha.redis;

import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;

public class JedisUtil {

    public static Jedis getJedis(){
        JedisPool jedisPool = new JedisPool("127.0.0.1",6379);
        Jedis resource = jedisPool.getResource();
        return resource;
    }
    public static void close(Jedis jedis){
        if( jedis != null){
            jedis.close();
        }

    }
}

package com.alpha.redis;

import redis.clients.jedis.Jedis;

public class RedisTest {

    private static int num = 0;
    
    public static void main(String[] args) throws InterruptedException {
    	task();
        //等待上面任务执行完毕
        Thread.sleep(5000);
        System.out.println("num的值是:" + num);
    }
    public static void add(){
    //这里我们只做了上锁动作,没有解锁动作,保证只有一个线程能够对num做操作
        if(lock()){
            num++;
        }
    }
    public static boolean lock(){
        Jedis jedis = JedisUtil.getJedis();
        // 1-创建成功,0-已存在,创建失败,这里没有去模拟一直尝试获取锁,超出某个时间没有获取到再退出
        Long lock = jedis.setnx("lock", "1");
        JedisUtil.close(jedis);
        return lock == 1L;
    }
    /**
     * 模仿集群下的实际业务代码,假设部署了100个相同的服务,同时对一个num进行加法操作,这个num可能是数据库中的值
     */
    public static void task(){
        for (int i = 0; i < 100; i++) {
            new Thread(RedisTest::add).start();
        }
    }
}

运行结果如图,发现即使是1000个线程去执行任务,最终num的值也只是加了1

在这里插入图片描述
但是我们发现如果再执行一次main方法,num的值就不会再变化了。这是因为我们没有删除redis中的lock键,所以我们再执行main方法,num的值就再也不会是1了。但是我们不可能在实际中也只让定时任务只跑一次,就再也没法执行了吧。所以我们要在任务执行完毕之后将lock键释放掉(删除)。那么这里又产生一个问题。

假设有两个相同的服务第一个线程A获取到了锁对num进行了操作之后释放了锁,这个时候另一个服务的线程B刚好在尝试获取锁,那么此时肯定能获取到锁,那么B也就对num执行操作了。这个时候的锁就将两个相同的定时任务变成了串行执行,就不符合我们的要求了。我们的要求是在这段时间内,只能对num执行一次操作。

解决方案:我们在A线程获取到锁之后,不释放锁,而是给锁设定过期时间Time1(这里的锁就是redis中的一个键值对),给线程设置一个时间 Time2,Time2 时间内获取不到锁就不由该线程执行任务。显然 Time1 > Time2,定时任务间隔时间Time3,则有 Time3 > Time1 > Time2,这是针对每次定时任务获取的锁名称都是一致的情况下,防止下一次定时任务运行的时候,锁还被上一个定时任务占用未过期,导致获取不到锁造成这次定时任务全部未执行。每次定时任务获取的锁名称变化,则可以只考虑 Time1 > Time2

这里就可以使用Redission中的锁了,代码如下:

//上面代码中加入如下代码
    private static Redisson redisson = null;
    static {
        Config config = new Config();
        config.useSingleServer().setAddress("redis://127.0.0.1:6379");
        redisson = (Redisson)Redisson.create(config);
    }
    
    public static void add02(){
        RLock rLock = getRLock();
        // 尝试获取锁 ,花费5s去获取锁,获取到返回True,锁10s自动释放。获取不到,返回false
        if(!rLock.tryLock(5,10,TimeUnit.SECONDS)){
            return;
        }
        num++;
    }
    public static RLock getRLock(){
        // 生成一个锁对象,此时并不实际在redis中生成锁,但是如果redis中存在该锁,就返回该锁
        final RLock lock = redisson.getLock("locklock");
        return lock;
    }
        /**
     * 模仿实际业务代码
     */
    public static void task02(){
        for (int i = 0; i < 1000; i++) {
            new Thread(RedisTest::add02).start();
        }
    }

运行结果如图
在这里插入图片描述
参考文档:

Redis分布式锁-这一篇全了解(Redission实现分布式锁完美方案)

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐