多实例下获取Redis分布式锁完成多库并发的定时任务

本篇旨在提供SpringCloud下多实例多库跑定时任务的优化方案

业务场景:

有这样一个需求,需要跑大数据下的定时任务,主要是查表和写表操作,量很大,要支持续跑,并且每个库的数据不一致,所以需要轮询库,假如生产有4台实例,一共有8个库,为了减小服务器的压力,并且能够同时跑8个库的任务。

需要解决几个问题:

1.确保每个库,有且只能有1个线程在跑,如果其他线程也跑了这个库,就会导致数据插入重复,浪费资源。

2.确保每个实例都能够有线程在运行定时任务,让资源充分利用。

3.如果某个任务出现问题,希望可以重跑或者有所记录。

 

方案一(不支持Redis集群下的解决方案):

实现细节:

1.如何轮询生产数据库?

这个就直接在配置文件配置8个库连接,然后根据你传入的库名,来实现切库操作,这个我就不贴代码了。

 

2.如何用Redis实现分布式锁,在网上找到一个不错的方式:

/**
 * @Author: Markful
 * @Description:
 * 1.互斥性。在任意时刻,只有一个客户端能持有锁。
 * 2.不会发生死锁。即使有一个客户端在持有锁的期间崩溃而没有主动解锁,也能保证后续其他客户端能加锁。
 * 3.具有容错性。只要大部分的Redis节点正常运行,客户端就可以加锁和解锁。
 * 4.解铃还须系铃人。加锁和解锁必须是同一个客户端,客户端自己不能把别人加的锁给解了。
 *
 * @Version 1.0
 */
public class RedisTool {

    private static final Long RELEASE_SUCCESS = 1L;

    private static final String LOCK_SUCCESS = "OK";
    private static final String SET_IF_NOT_EXIST = "NX";
    private static final String SET_WITH_EXPIRE_TIME = "EX";


    /**
     * 尝试获取分布式锁
     * @param jedis Redis客户端
     * @param lockKey 锁
     * @param requestId 请求标识
     * @param expireTime 超期时间
     * @return 是否获取成功
     */
    public static boolean tryGetDistributedLock(Jedis jedis,String lockKey, String requestId, int expireTime){

        String result = jedis.set(lockKey,requestId,SET_IF_NOT_EXIST,SET_WITH_EXPIRE_TIME,expireTime);
        if(LOCK_SUCCESS.equals(result)){
            return true;
        }
        return false;
    }

    /**
     * 释放分布式锁
     * @param jedis Redis客户端
     * @param lockKey 锁
     * @param requestId 请求标识
     * @return 是否释放成功
     * @warn 不能直接del()删除锁,因为没有判断锁的拥有者,可能会删除其他客户端的锁;
     * Lua脚本语言确保执行是原子操作。
     */
    public static boolean releaseDistributedLock(Jedis jedis, String lockKey, String requestId) {

        String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
        //eval函数为执行Lua脚本
        Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId));

        if (RELEASE_SUCCESS.equals(result)) {
            return true;
        }
        return false;
    }

}

这个就是使用Redis实现的分布式锁比较好的解决方案,可以满足我们这个业务需求,因为有requestID 来区分现在执行的是哪个库和LockKey作为redis的key。

并且也确保了在分布式情况下,有且只有一个线程可以竞争到这个锁,并且如果被死锁,也可以通过过期时间来确保锁会被释放,如果想要提前释放,就可以调用里面的releaseDistributedLock()方法主动释放锁,让其他实例可以去竞争这个锁。

 

3.SpringBoot的多线程

配置类:

/**
 * @Description: 配置类实现AsyncConfigurer接口,并重写getAsyncExecutor方法,并返回一个ThreadPoolTaskExecutor,
 * 这样我们就获得一个基于线程池TaskExecutor
 * @Author: Markful
 */
@Configuration
@ComponentScan("com.athena.redismode")
@EnableAsync//利用@EnableAsync注解开启异步任务支持
public class CustomMultiThreadingConfig implements AsyncConfigurer {

    @Override
    public Executor getAsyncExecutor() {
        ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
        taskExecutor.setCorePoolSize(5);
        taskExecutor.setMaxPoolSize(10);
        taskExecutor.setQueueCapacity(25);
        taskExecutor.initialize();
        return taskExecutor;
    }

    @Override
    public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
        return AsyncConfigurer.super.getAsyncUncaughtExceptionHandler();
    }

}

为什么不能直接new 一个线程出来,或者直接使用线程池呢?

因为在SpringBoot中,Spring都帮你管理了所有的类的初始化,你单独new 一个线程出来,里面的东西Spring是不知道的,如果需要调用Spring管理的Bean,你就肯定是拿不到的,所以,SpringBoot也提供了多线程的解决方案:Async。

 

接下来就是看下怎么使用了:

/**
 * @Author: Markful
 * @Description:
 * @Version 1.0
 */
@Service
public class DistributeLockService {


    private static final String[] DBLIST = {"cs", "gz", "nj", "qd", "sh", "bj", "cd", "xa"};
    private static final String PREFIX_NAME = "uploadTax";

    private Jedis testJedis = new Jedis();


    public void distributeLock() {
        //1.方法一:串行
        System.out.println("方法一开始");
        for (String db : DBLIST) {
            String lockKey = PREFIX_NAME + "-" + db;
            String requestId = UUID.randomUUID().toString();

            this.MultiThreadService(lockKey, requestId);


        }
        System.out.println("方法一结束");

        //2.方法二,并行
        /*System.out.println("方法二开始");
        int count = 0;
        for (String db : DBLIST) {
            String lockKey = PREFIX_NAME + "-" + db;
            String requestId = UUID.randomUUID().toString();
            if (count >= 2) {
                break;
            }
            boolean getLock = RedisTool.tryGetDistributedLock(testJedis, lockKey, requestId, 20);
            if (getLock) {
                System.out.println("获取到锁" + lockKey);
                count++;
                //如果获取到了锁,则进入主要方法
                try {

                    long date = Calendar.getInstance().getTime().getTime();
                    mainService();
                    long endTime = Calendar.getInstance().getTime().getTime();
                    System.out.println("执行时间:" + (endTime - date));

                } catch (Exception e) {
                    //如果出现问题,是否要解锁?
                    RedisTool.releaseDistributedLock(testJedis, lockKey, requestId);
                }
            }
        }
        System.out.println("方法二结束");*/

    }

    @Async
    public void MultiThreadService(String lockKey, String requestId) {
        //如果串行,则会起8个线程,过期时间20秒
        boolean getLock = RedisTool.tryGetDistributedLock(testJedis, lockKey, requestId, 20);
        if (getLock) {
            System.out.println("获取到锁" + lockKey);
            //如果获取到了锁,则进入主要方法
            try {
                long date = Calendar.getInstance().getTime().getTime();
                mainService();
                long endTime = Calendar.getInstance().getTime().getTime();
                System.out.println("执行时间:" + (endTime - date));
            } catch (Exception e) {
                System.out.println("出了问题:" + e.getMessage());
                //如果出现问题,是否要解锁?
                RedisTool.releaseDistributedLock(testJedis, lockKey, requestId);
            }
        }
    }

    public void mainService() {
        try {
            SleepThreadService t1 = new SleepThreadService("t1");
            t1.run();
        } catch (Exception e) {
            System.out.println(e.getMessage());
        }

    }

mainService里面的方法,主要就是让线程休息2秒。


/**
 * @Author: Markful
 * @Description:
 * @Version 1.0
 */
@Service
public class SleepThreadService extends Thread {

    private static Object obj = new Object();

    public SleepThreadService(String name){
        super(name);
    }

    public SleepThreadService() {
    }

    public void run(){
        synchronized(obj){
            try {
                for(int i=0; i <5; i++){
                    System.out.printf("%s: %d\n", this.getName(), i);
                    // i能被4整除时,休眠2000毫秒
                    if (i%4 == 0)
                        Thread.sleep(2000);
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

 

这就是基本的解决思路,使用Redis的分布式锁,确保所有实例,只有一个线程,可以跑某个库的数据,并且不存在重复跑的情况,因为只要你的过期时间设置的比轮询库的时间长就行了,因为调用方法是异步的,所以轮询库可能就几秒钟搞定了,所以过期时间设置到10分钟,绰绰有余。

 

2.解决方案二(支持Redis集群):

由于redis集群下,一般项目会使用RedisTemplate来操作redis,但是redistemplate并不能支持lua脚本,必须要获取到其原生的redis连接才可以使用lua脚本。

 

 

 

 

如果觉得给到帮助了,不吝个赞啊!!!

谢谢

 

 

你可能感兴趣的:(JAVA,Spring,SpringCloud,Redis)