基于JFinal Cron4j做分布式定时任务

思路:建一个记录任务状态的表,通过去抢占表中的状态来获取执行定时任务的权限

直接上代码:

1,建一个表如下

create table XXJOB_STATUS
(
  id       VARCHAR2(32) default sys_guid() not null,
  jobname  VARCHAR2(40),
  status   NUMBER(11) default '0',
  locktime VARCHAR2(80),
  bz       VARCHAR2(40)
)

2,所有定时器格式如下

public class XXJob extends Task  {
	private static final String jobname = "XXJOBNAME";
	public void execute(TaskExecutionContext content) throws RuntimeException {
		if(!isRun()){
			if(lock()){
				//执行定时任务
				*************************
				*************************
				unlock();	
			}else{
				//job被占用 
			}
		}else{
			//======job被占用
		}
		//end job
	}
	private boolean isRun(){
		return Db.queryInt("select status from xx_job_status where jobname = ? ",jobname).intValue()==1;
	}
	private boolean lock(){
		return Db.update("update xx_job_status set status = 1,locktime = ? where status = 0 and jobname= ?",DateFormatUtil.format(new Date(), "yyyy-MM-dd HH:mm:ss"),jobname)==1;
	}
	private boolean unlock(){
		return Db.update("update xx_job_status set status = 0,locktime = '' where status = 1 and jobname= ?",jobname)==1;
	}
}



评论区

JFinal

2019-07-17 11:28

代码十分简洁,为了进一步提升可靠性与稳固性,有几个建议:
1:isRun()、lock()、unlock() 都用 Db.tx(...) 开启事务

2:lock() 方法的抢占需要引入一个 locker 字段,用于标识是否是当前线程抢到了,具体办法如下:
a:添加一个字段名叫 locker,默认值为 null

b:locker 字段的值通过 StrKit.getRandomUUID() 生成一个 UUID 值,由于 UUID 是全球唯一的,所以可以确保不可能重复。而当前你的 lock() 方法在并发高的时候,多个线程很可能都返回 true

c:改进 lock() 内部代码逻辑,大致如下:
String UUID = StrKit.getRandomUUID();
// 该 sql 只更新 locker 字段,也就是先只去抢占锁,而不能做其它任何事情
String sql = "update task locker = ? where locker is NULL";
int n = Db.update(sql, UUID);

// n 大于 0 表示 update 成功,但不能保证是当前线程抢到的该 task
if (n > 0) {
// 通过前面生成的 UUID 去查询,查到了才能证明真的是当前线程抢占到了该 task
Record task = Db.findFirst("select * from task where locker = ?", UUID);
if (task != null) {
这里再对 task 进行处理,例如更新 task 表的其它字段
return true; // lock() 方法不建议返回 boolean 值,而是返回 task 对象,便于上层调用者使用,用完以后还可以方便置回状态
}
}

上面的改进思路主要是用一个 locker 字段让多线程先抢占锁,抢到以后再进行后续的操作

注意,上述示例代码用到了 task 表以及 locker 字段,与你给出来的代码中用的 xx_job_status 并不相同

JFinal

2019-07-17 11:36

我给的方案,再补充完善一点点:
1:所有任务存放在一个中心的数据库中
2:任务的表名为 task,主要字段有: task(id, locker)
3:创建一个 TaskService 业务类
4:TaskService 中创建一个抢占 Task 的方法:

public class TaskService {

private static final Task dao = new Task().dao();

public Task getTask() {
String UUID = StrKit.getRandomUUID();
// 该 sql 只更新 locker 字段,也就是先只去抢占锁,而不能做其它任何事情
String sql = "update task set locker = ? where locker is NULL";
int n = Db.update(sql, UUID);
if ( n <= 0) {
return null;
};

// n 大于 0 表示 update 成功,但不能保证是当前线程抢到的该 task
// 通过前面生成的 UUID 去查询,查到了才能证明真的是当前线程抢占到了该 task
Task task = dao.findFirst("select * from task where locker = ?", UUID);
if (task != null) {
return task;
} else {
// 如果没有抢占到 task ,可以放弃则 return null, 也可以重试几次
return null;
}

}
}

10000

2019-07-17 11:42

@JFinal
XXJOB_STATUS这个表确实必须放在中心库
另外你说的locker字段其实就是跟status一样的,这个下面这个sql执行返回1,一定是抢到了任务
update xx_job_status set status = 1,locktime = ? where status = 0 and jobname= ?

JFinal

2019-07-17 11:45

@10000 status 的取值是 0 和 1,高并发的时候,不知道是谁将这个状态置为 1 的

虽然在事务中也可以保障这个值只能被某个线程抢到,但多一个 UUID 作为 locker 更加安全,因为事务依赖不少的条件,例如必须是 InnoDB 引擎,必须要开启事务

上面的方案用到 UUID 的 locker 相当于是双保险

10000

2019-07-17 11:53

l745230

2019-07-17 16:48

@JFinal 感觉locker有点像商品秒杀

happyboy

2019-07-19 09:27

在保证所有子系统都能正常运行的情况下,只让其中一个子系统运行调度,其他子系统不开启。也算是一种简单的方案。^_^

SM.Time

2019-07-23 16:15

把任务时间段加到数据库就好了

热门分享

扫码入社