有这样一个需求,需要跑大数据下的定时任务,主要是查表和写表操作,量很大,要支持续跑,并且每个库的数据不一致,所以需要轮询库,假如生产有4台实例,一共有8个库,为了减小服务器的压力,并且能够同时跑8个库的任务。
需要解决几个问题:
1.确保每个库,有且只能有1个线程在跑,如果其他线程也跑了这个库,就会导致数据插入重复,浪费资源。
2.确保每个实例都能够有线程在运行定时任务,让资源充分利用。
3.如果某个任务出现问题,希望可以重跑或者有所记录。
这个就直接在配置文件配置8个库连接,然后根据你传入的库名,来实现切库操作,这个我就不贴代码了。
/**
* @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()方法主动释放锁,让其他实例可以去竞争这个锁。
配置类:
/**
* @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分钟,绰绰有余。
由于redis集群下,一般项目会使用RedisTemplate来操作redis,但是redistemplate并不能支持lua脚本,必须要获取到其原生的redis连接才可以使用lua脚本。
如果觉得给到帮助了,不吝个赞啊!!!
谢谢