前言
很多业务中,我们都会使用到定时任务,例如支付宝支付的定时检查、需要周期性执行的任务等,在同一个微服务中,可能会需要写很多的定时任务。在分布式环境下,每个任务都需要写分布式锁相关的代码,而且很多用户执行的时候,可以使用线程池加快任务执行的效率,因此,提出了基于策略模式和线程池重构分布式定时任务的想法。
分布式锁
分布式锁实现的框架较多,本文章使用的是Redission,这也是目前为止,个人认为分布式锁最好的框架,使用起来很简单,使用方法在次不再赘述,有兴趣的小伙伴可以自己学习。
策略模式
策略模式属于24种设计模式之一,是行为型模式的一种,用于描述类或对象之间怎样相互协同共同完成单个对象无法完成的任务,以及怎样分配职责。后面我会详细介绍24种设计模式,参考文章地址:
详细代码示例
重构前代码
代码结构:
- 定时任务类:ATask、BTask
@Component
public class ATask {
private static final String REDIS_KEY = "redis-key-a";
@Scheduled(cron = "0 0 1 * * ?")
public void aMethod() {
RLock lock = redisson.getLock(REDIS_KEY);
try {
if (lock.tryLock(5, 10, TimeUnit.SECONDS)) {
try {
// 获取到分布式锁,执行业务方法
} finally {
// 释放锁
lock.unlock();
}
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
@Component
public class BTask {
private static final String REDIS_KEY = "redis-key-b";
@Scheduled(cron = "0 0 1 * * ?")
public void bMethod() {
RLock lock = redisson.getLock(REDIS_KEY);
try {
if (lock.tryLock(5, 10, TimeUnit.SECONDS)) {
try {
// 获取到分布式锁,执行业务方法
} finally {
// 释放锁
lock.unlock();
}
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
很明显,以上每个定时任务都需要写获取锁,并且任务名称也不统一,且效率低下,下面加上分布式策略及线程池进行重构
重构后代码
代码结构:
3. 自定义注解:MyScheduled
4. 自定义接口:MyScheduleTask,执行定时任务
5. 定时任务类:ATask、BTask,主要是定时任务的具体代码
下面依次实现上面的类或接口,完成定任务的重构。
自定义注解,所有定时任务类都需要添加该注解,线程通过反射获取注解的定时任务表达式、锁释放时间、锁释放单位,从而确定定时任务的执行。
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MyScheduled {
/**
* 任务cron表达式,兼容quartz.
*/
String cron();
/**
* 锁释放时间值,默认3.
*/
int releaseTime() default 3;
/**
* 锁释放时间单位,默认秒.
*/
TimeUnit timeUnit() default TimeUnit.SECONDS;
}
定义接口:MyScheduleTask,所有业务定时类都需要实现该接口,重写run()方法,这样统一了所有定时任务的方法
public interface MyScheduleTask {
/**
* 所有定时任务类都需要重写该方法,执行定时任务的业务代码.
*/
void run();
/**
* 内部注入类,将定时任务和表达式注入到线程池的任务中.
*/
@Component
@Slf4j
class Initializer {
@Autowired
private ApplicationContext context;
@Autowired
private RedissonClient redisson;
// 线程池任务调度类,能够开启线程池进行任务调度
private ThreadPoolTaskScheduler exec = new ThreadPoolTaskScheduler();
// 将2秒换算成毫秒,minTaskProcessTime=2000毫秒
// 判断任务的用时时间,如果小于2秒,则睡眠2秒后再结束定时任务
private static final Long minTaskProcessTime = TimeUnit.SECONDS.toMillis(2);
@PostConstruct
public void init() {
// 初始化线程池
exec.setPoolSize(10);
exec.setThreadGroupName("my-task");
exec.initialize();
// 获取所有实现PayScheduleTask接口的定时任务
Map<String, MyScheduleTask> beansOfType = context.getBeansOfType(MyScheduleTask.class);
beansOfType.values().forEach(task -> {
// 获取定时任务类的注解@PayScheduled
MyScheduled myScheduled = task.getClass().getAnnotation(MyScheduled.class);
//启用事务后会生成代理类,再次查找代理类父类的注解.
if (myScheduled == null) {
myScheduled = task.getClass().getSuperclass().getAnnotation(MyScheduled.class);
}
if (myScheduled != null) {
// 变成final,用于lambda表达式中
final MyScheduled scheduled = myScheduled;
/*
schedule()方法会创建一个定时计划ScheduledFuture,在这个方法需要添加两个参数,
Runnable(线程接口类) 和CronTrigger(定时任务触发器)
Runnable线程用于执行定时任务,CronTrigger设定定时任务的cron表达式
*/
exec.schedule(() -> {
RLock lock = redisson.getLock(RedisKeys.SCHEDULE_TASK_LOCK.build(toOriginalName(task.getClass().getSimpleName())));
try {
// 编程规约:lock.tryLock()方法不要放在try代码块中,因为lock.tryLock()可能会抛出uncheck异常
if (lock.tryLock(0L, scheduled.releaseTime(), scheduled.timeUnit())) {
try {
long start = System.currentTimeMillis();
// task表示定时任务类,执行定时任务类重写的run()方法
task.run();
long timeCost = System.currentTimeMillis() - start;
log.info("lock success :{} hold count is :{} , task process cost: {}ms", lock.getName(), lock.getHoldCount(), timeCost);
//如果任务耗时不足最小处理时间,则sleep 2秒,降低多实例任务冲突.
if (timeCost < minTaskProcessTime) {
TimeUnit.SECONDS.sleep(2);
}
} catch (InterruptedException e) {
log.error("计划任务执行失败", e);
} finally {
lock.unlock();
log.debug("release lock :{}", lock.getName());
}
} else {
log.info("lock faild :{} hold count is :{}", lock.getName(), lock.getHoldCount());
}
} catch (InterruptedException e) {
log.error("lock error .", e);
}
}, new CronTrigger(myScheduled.cron()));
}
});
}
/**
* 关闭线程池之前,执行该方法.
*/
@PreDestroy
public void desgtroy() {
exec.shutdown();
}
/**
* 获取类名.
*/
private static String toOriginalName(String cglibName) {
return cglibName.contains("$") ? cglibName.substring(0, cglibName.indexOf("$")) : cglibName;
}
}
}
定时任务类:ATask、BTask,完成业务定时任务代码
@MyScheduled(cron = "30 0/2 * * * ?")
@Component
@Slf4j
public class ATask implements MyScheduleTask {
@Override
@Transactional(rollbackFor = Exception.class)
public void run() {
// 直接写具体的业务方法
}
}
@MyScheduled(cron = "30 0/2 * * * ?")
@Component
@Slf4j
public class BTask implements MyScheduleTask {
@Override
@Transactional(rollbackFor = Exception.class)
public void run() {
// 直接写具体的业务方法
}
}
以上就完成了定时任务的重构,具体的线程池参数设置,可以自行根据业务来确定。