智能提醒助理系列-分布式调度选型

    本系列文章记录“智能提醒助理”wx公众号 建设历程,记录实践经验、巩固知识点、锻炼总结能力。

目录

一、需求出发点

二、实现路径分析

三、最终方案

四、使用方式


本篇文章介绍 智能提醒助理的核心模块,调度器的技术选型和使用。

一、需求出发点

当用户在前端设置提醒的时候,会选择一个时间,比如:单次、每日、每月、每年、每周。

那么如何在用户设置的时间点给予提醒呢?

这个时候 就需要一个调度。

二、实现路径分析

1、Quartz 

2、xxl-job

对比分析:

1、quartz 是 调度逻辑和执行逻辑通常并存于同一个项目中,会相互影响。默认不支持分布式需要自己扩展。

2、支持分布式、调度独立部署、有独立的界面维护管理、支持分片、负载均衡策略和故障转移。

三、最终方案

选择xxl-job。

四、使用方式

1、如何集成

 使用源码包,自己增加API,调整下。官方地址

GitHub - xuxueli/xxl-job: A distributed task scheduling framework.(分布式任务调度平台XXL-JOB)

主要修改逻辑:将 addJob、remove 增加通过token认证即可API访问,我自己又增加了removes方法。

简单粗暴:增加、删除,批量删除,修改也是先删再加,已解决批量更新 还要触发任务的问题。

package com.xxl.job.admin.controller;

import com.xxl.job.admin.controller.annotation.TokenPermission;
import com.xxl.job.admin.core.exception.XxlJobException;
import com.xxl.job.admin.core.model.XxlJobGroup;
import com.xxl.job.admin.core.model.XxlJobInfo;
import com.xxl.job.admin.core.model.XxlJobUser;
import com.xxl.job.admin.core.route.ExecutorRouteStrategyEnum;
import com.xxl.job.admin.core.scheduler.MisfireStrategyEnum;
import com.xxl.job.admin.core.scheduler.ScheduleTypeEnum;
import com.xxl.job.admin.core.thread.JobScheduleHelper;
import com.xxl.job.admin.core.util.I18nUtil;
import com.xxl.job.admin.dao.XxlJobGroupDao;
import com.xxl.job.admin.service.LoginService;
import com.xxl.job.admin.service.XxlJobService;
import com.xxl.job.core.biz.model.ReturnT;
import com.xxl.job.core.enums.ExecutorBlockStrategyEnum;
import com.xxl.job.core.glue.GlueTypeEnum;
import com.xxl.job.core.util.DateUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.util.StopWatch;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.*;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.util.*;

/**
 * index controller
 * @author xuxueli 2015-12-19 16:13:16
 */
@Controller
@RequestMapping("/jobinfo")
public class JobInfoController {
	private static Logger logger = LoggerFactory.getLogger(JobInfoController.class);

	@Resource
	private XxlJobGroupDao xxlJobGroupDao;
	@Resource
	private XxlJobService xxlJobService;
	
	@RequestMapping
	public String index(HttpServletRequest request, Model model, @RequestParam(required = false, defaultValue = "-1") int jobGroup) {

		// 枚举-字典
		model.addAttribute("ExecutorRouteStrategyEnum", ExecutorRouteStrategyEnum.values());	    // 路由策略-列表
		model.addAttribute("GlueTypeEnum", GlueTypeEnum.values());								// Glue类型-字典
		model.addAttribute("ExecutorBlockStrategyEnum", ExecutorBlockStrategyEnum.values());	    // 阻塞处理策略-字典
		model.addAttribute("ScheduleTypeEnum", ScheduleTypeEnum.values());	    				// 调度类型
		model.addAttribute("MisfireStrategyEnum", MisfireStrategyEnum.values());	    			// 调度过期策略

		// 执行器列表
		List<XxlJobGroup> jobGroupList_all =  xxlJobGroupDao.findAll();

		// filter group
		List<XxlJobGroup> jobGroupList = filterJobGroupByRole(request, jobGroupList_all);
		if (jobGroupList==null || jobGroupList.size()==0) {
			throw new XxlJobException(I18nUtil.getString("jobgroup_empty"));
		}

		model.addAttribute("JobGroupList", jobGroupList);
		model.addAttribute("jobGroup", jobGroup);

		return "jobinfo/jobinfo.index";
	}

	public static List<XxlJobGroup> filterJobGroupByRole(HttpServletRequest request, List<XxlJobGroup> jobGroupList_all){
		List<XxlJobGroup> jobGroupList = new ArrayList<>();
		if (jobGroupList_all!=null && jobGroupList_all.size()>0) {
			XxlJobUser loginUser = (XxlJobUser) request.getAttribute(LoginService.LOGIN_IDENTITY_KEY);
			if (loginUser.getRole() == 1) {
				jobGroupList = jobGroupList_all;
			} else {
				List<String> groupIdStrs = new ArrayList<>();
				if (loginUser.getPermission()!=null && loginUser.getPermission().trim().length()>0) {
					groupIdStrs = Arrays.asList(loginUser.getPermission().trim().split(","));
				}
				for (XxlJobGroup groupItem:jobGroupList_all) {
					if (groupIdStrs.contains(String.valueOf(groupItem.getId()))) {
						jobGroupList.add(groupItem);
					}
				}
			}
		}
		return jobGroupList;
	}
	public static void validPermission(HttpServletRequest request, int jobGroup) {
		XxlJobUser loginUser = (XxlJobUser) request.getAttribute(LoginService.LOGIN_IDENTITY_KEY);
		if (!loginUser.validPermission(jobGroup)) {
			throw new RuntimeException(I18nUtil.getString("system_permission_limit") + "[username="+ loginUser.getUsername() +"]");
		}
	}
	
	@RequestMapping("/pageList")
	@ResponseBody
	@TokenPermission
	public Map<String, Object> pageList(@RequestParam(required = false, defaultValue = "0") int start,  
			@RequestParam(required = false, defaultValue = "10") int length,
			int jobGroup, int triggerStatus, String jobDesc, String executorHandler, String author) {
		
		return xxlJobService.pageList(start, length, jobGroup, triggerStatus, jobDesc, executorHandler, author);
	}
	
	@RequestMapping("/add")
	@ResponseBody
	public ReturnT<String> add(XxlJobInfo jobInfo) {
		return xxlJobService.add(jobInfo);
	}

	@PostMapping("/addJob")
	@ResponseBody
	@TokenPermission
	public ReturnT<String> addJob(@RequestBody XxlJobInfo jobInfo) {
		return xxlJobService.add(jobInfo);
	}

	@RequestMapping("/update")
	@ResponseBody
	public ReturnT<String> update(XxlJobInfo jobInfo) {
		return xxlJobService.update(jobInfo);
	}

	@PostMapping("/updateJob")
	@ResponseBody
	@TokenPermission
	public ReturnT<String> updateJob(@RequestBody XxlJobInfo jobInfo) {
		return xxlJobService.update(jobInfo);
	}

	@RequestMapping("/remove")
	@ResponseBody
	@TokenPermission
	public ReturnT<String> remove(int id) {
		return xxlJobService.remove(id);
	}
	@RequestMapping("/removes")
	@ResponseBody
	@TokenPermission
	public ReturnT<String> removes(String jobIds) {
		String[] split = jobIds.split(",");
		for (String s : split) {
			if ((StringUtils.isEmpty(s) || "null".equals(s))){
				continue;
			}
			xxlJobService.remove(Integer.valueOf(s));
		}
		return ReturnT.SUCCESS;
	}

	@RequestMapping("/stop")
	@ResponseBody
	@TokenPermission
	public ReturnT<String> pause(int id) {
		return xxlJobService.stop(id);
	}
	
	@RequestMapping("/start")
	@ResponseBody
	@TokenPermission
	public ReturnT<String> start(int id) {
		return xxlJobService.start(id);
	}
	
	@RequestMapping("/trigger")
	@ResponseBody
	@TokenPermission
	public ReturnT<String> triggerJob(HttpServletRequest request, int id, String executorParam, String addressList) {
		// login user
		XxlJobUser loginUser = (XxlJobUser) request.getAttribute(LoginService.LOGIN_IDENTITY_KEY);
		// trigger
		return xxlJobService.trigger(loginUser, id, executorParam, addressList);
	}

	@RequestMapping("/nextTriggerTime")
	@ResponseBody
	@TokenPermission
	public ReturnT<List<String>> nextTriggerTime(String scheduleType, String scheduleConf) {

		XxlJobInfo paramXxlJobInfo = new XxlJobInfo();
		paramXxlJobInfo.setScheduleType(scheduleType);
		paramXxlJobInfo.setScheduleConf(scheduleConf);

		List<String> result = new ArrayList<>();
		try {
			Date lastTime = new Date();
			for (int i = 0; i < 5; i++) {
				lastTime = JobScheduleHelper.generateNextValidTime(paramXxlJobInfo, lastTime);
				if (lastTime != null) {
					result.add(DateUtil.formatDateTime(lastTime));
				} else {
					break;
				}
			}
		} catch (Exception e) {
			logger.error(e.getMessage(), e);
			return new ReturnT<List<String>>(ReturnT.FAIL_CODE, (I18nUtil.getString("schedule_type")+I18nUtil.getString("system_unvalid")) + e.getMessage());
		}
		return new ReturnT<List<String>>(result);

	}
	
}

2、如何通过API动态创建任务

/**
   * 添加任务到xxljob
   * @param jobTime
   * @return
   */
  private Integer addJob(JobTime jobTime) {
    JobInfoVo jobInfoVo = new JobInfoVo();
    jobInfoVo.setJobDesc(jobTime.getTitle()+" "+jobTime.getRemark());java
    jobInfoVo.setScheduleConf(jobTime.getCron());

    jobInfoVo.setExecutorParam(JSONObject.toJSONString(jobTime));//参数
    jobInfoVo.setExecutorHandler("baseTipsJob");//执行器任务handler

    jobInfoVo.setAuthor("user-" + jobTime.getUserId());
    jobInfoVo.setJobGroup(2);
    jobInfoVo.setScheduleType("CRON");
    jobInfoVo.setExecutorRouteStrategy("FIRST");// 执行器路由策略 FIRST 第一个,ROUND 轮询,RANDOM 随机,SHARDING_BROADCAST 分片广播
    jobInfoVo.setExecutorBlockStrategy("DISCARD_LATER");//阻塞处理策略 DISCARD_LATER,丢弃后续调度
    jobInfoVo.setTriggerStatus(1);//调度状态:0-停止,1-运行

    jobInfoVo.setGlueType("BEAN");
    jobInfoVo.setMisfireStrategy("DO_NOTHING");

    jobInfoVo.setAddTime(new Date());

    Integer jobId = xxlJobApi.addJob(jobInfoVo);
    log.info("添加jobId成功:{}", jobId);
    return jobId;
  }

3、通过API更新和删除任务

/**
   * 修改提醒任务
   *
   * @param aiTips
   * @return
   */
  @Transactional(rollbackFor = Exception.class)
  public boolean updateTips(AiTips aiTips) {
    AiTips byId = super.getById(aiTips.getId());
    //先删除任务
    delJobs(byId.getJobId());

    //创建xxjob
    List<JobTime> jobTimes = aiTips.getJobTimes();
    List<String> jobIds = new ArrayList<>();
    for (JobTime jobTime : jobTimes) {
      jobTime.setAiTipsId(aiTips.getId());
      Integer jobId = addJob(jobTime);
      if (jobId!=null){
        jobIds.add(String.valueOf(jobId));
      }
    }

    // 3、更新jobId
    String join = String.join(",", jobIds);
    aiTips.setJobId(join);
    this.updateById(aiTips);
    return true;

  }

4、问题:时间如何转换为cron表达式

package com.jdy.common.util;

import com.jdy.common.enums.TipsCycleEnum;
import com.jdy.common.utils.DateUtils;
import com.nlf.calendar.Lunar;
import lombok.extern.slf4j.Slf4j;

import java.text.ParseException;

/**
 * cron 工具类
 */
@Slf4j
public class CronUtil {

  /**
   * 输入的日期转为cron
   */
  public static void date2Cron() throws ParseException {
 /*   log.info("单次提醒corn:{}",createCronByType("2024-07-30 10:07","单次"));
    // 阴历转阳历
    String[] str = "2024-07-30 10:07".split(" ");
    String yangli = yinli2yangli(str[0]);
    log.info("单次提醒corn:{}",createCronByType(yangli+" "+str[1],"阴历单次"));
    log.info("每天提醒corn:{}",createCronByType("09:03","每天"));
    log.info("每工作日corn:{}",createCronByType("10:07","每工作日"));
    log.info("每周corn:{}",createCronByType("10:08","每周",1));
    log.info("每周六日corn:{}",createCronByType("10:07","每周六日"));
    log.info("每月corn:{}",createCronByType("30 10:08","每月"));
    log.info("每年corn:{}",createCronByType("07-30 10:10","每年"));
    log.info("每年corn:{}",createCronByType("07-30 10:10","阴历每年"));
*/
  }

  /**
   * 根据类型创建cron表达式
   * @param str
   * @param tipsCycle
   * @return
   */
  public static String createCronByType(String str,TipsCycleEnum tipsCycle){
    return createCronByType(str, tipsCycle,0);
  }

  public static String createCronByType(String str,TipsCycleEnum tipsCycle,int week){
    String[] ary = str.split("-| |:");
    StringBuffer sb = new StringBuffer("0 ");
    int flag = -1;
    //单次 阴历单次
    if(TipsCycleEnum.ONE.equals(tipsCycle) || TipsCycleEnum.LUNAR_ONE.equals(tipsCycle)){
      flag = 0;
    }
    for (int i = ary.length -1; i >flag ; i--) {
      sb.append(ary[i]+" ");
    }
    switch (tipsCycle){
      case ONE: sb.append("? ").append(ary[0]); break;
      case LUNAR_ONE: sb.append("? ").append(ary[0]); break;
      case DAY: sb.append("* * ?");break;
      // cron的周 对应关系
      // 日 一 二 三 四 五 六
      // 1  2 3  4  5 6  7
      case WORKDAY: sb.append("? * 2-6");break;
      case WEEKEND: sb.append("? * 1,7");break;
      case WEEK:  sb.append("? * "+(week+1>7?0:week+1));break; // week cron的 1 是周日, 7 是周六 需要修正下
      case MONTH: sb.append("* ?");break;
      case YEAR:  sb.append("? *");break;
    }
    return sb.toString();
  }

  /**
   * 阴历转阳历
   */
  public static String yinli2yangli(String date) {
    String[] ary = date.split("-");
    Lunar lunar ;
    // 场景1:单次 "2024-12-23"
    if(ary.length == 3){
      lunar = Lunar.fromYmd(Integer.parseInt(ary[0]),Integer.parseInt(ary[1]),Integer.parseInt(ary[2]));
    }else{
      // 场景2:每年 "12-23"的情况,当没有年的时候 获取当前年份
      lunar = Lunar.fromYmd(DateUtils.getYear(),Integer.parseInt(ary[0]),Integer.parseInt(ary[1]));
    }
    return lunar.getSolar().toString();
  }


}

5、问题:阴历如何转阳历

使用 Lunar 每年阳历的时候 需要注意,需要 在每年正月初一的时候 把第二年的 阳历再修正一遍。

想要了解更多关于lunar的信息,请访问其官方文档:lunar

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值