背景
应用中常会需要一些定时执行的任务,在spring中通过@Scheduled注解可以轻松实现。
然鹅现在正儿八经的项目一般不会只部署一个实例,至少也得搞两台支持不中断服务的发布,壕一点的部署个十几台、几十台的问题不大。
这样一来我们写定时任务时就需要考虑到这个任务到了执行的时候会不会所有实例都在执行,这样对业务会不会造成影响。
不造成影响的情况,如:
造成影响的情况。。。反之。
通过一个独占锁控制每个任务的执行权,必须获得了锁的实例才能执行任务,执行完再释放锁。这个锁的资源需要是所有实例都能访问的同一份资源,可以通过MySQL、Redis等实现。
因为所有实例都需要请求这个共享资源,所以需要提供一个服务接收这些请求。
用一个自定义注解@SyncJob代替@Scheduled即可拥有分布式下同步执行的能力(同一时刻只有一台执行),且定时的规则同@Scheudled。
基于这个目标,进行下面的设计。
DB作为“资源中心”,需要如下结构:
ID: 任务唯一标识,可以确定到具体执行的方法
状态: 任务执行状态,待执行,执行中
本次开始执行时间: 每次开始执行时更新,和状态一起作为CAS操作的判断条件
本次结束执行时间: 每次执行结束时更新,如果需要支持按结束时间间隔则需要
springboot提供的能力,spring全家桶中各种starter就是基于这个能力实现的。
只需要添加一个maven依赖,应用启动时就会自动扫描该包下的指定类,创建指定bean,让我们不用在自己项目里写一堆重复代码去创建bean。
添加文件:resources/META-INF/spring.factories
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.xxx.SyncJobConfig
引用了此依赖的项目在启动时会把SyncJobConfig类里面定义的bean创建并管理。
有两个重要的bean需要在SyncJobConfig中创建:
Q:任务执行中如果有人重发怎么办?任务执行到一半应用关闭,锁也没释放,重启后永远查询不到那个作业的记录。
A:给定时任务执行类定义一个bean的销毁方法(@PreDestroy),应用关闭时框架会自动调用,在里面完成善后。
// 举个栗子
public class ScheduleService {
// ...省略无关代码
@PreDestroy // 应用关闭会销毁bean,销毁bean会执行此注解修饰的方法
public void shutdown() {
running = false; // 用一个标记让轮询直接跳过简单粗暴
jobExecutor.shutdown();// 不再接收新任务
try {
// 等待所有作业执行完,或者超时
boolean ok = jobExecutor.awaitTermination(120, TimeUnit.SECONDS);
logger.warn("ScheduleService {}", ok ? "完美停止" : "等待超时");
} catch (InterruptedException e) {
logger.warn("ScheduleService 等待线程被中断 {}", e.getMessage());
}
}
}
Q:有人杀进程,服务器宕机等极端情况应用死的比较干脆,没有那么多时间优雅狗带,这时没释放锁怎么办?
A:没得办,只能人工干预。
可以做一个控制台页面,做更多事情,懒得做可以写个后门,或者直接改数据库。
重启中、挂掉、网络故障、数据库异常等意外出现时,众多业务系统无法和中心交流,也就无法判断能否执行任务,最好也就不要执行了,耐心等待或者告警。
注册任务失败:应用启动失败/无法执行任务,需要等待服务恢复
请求资源失败:无法执行任务,需要等待服务恢复
释放资源失败:由于锁没释放所以服务恢复后也不能执行,需要人工干预
针对释放资源失败必须人工干预,可以用一些措施偷个懒。
※ 为之奈何?
- END -