当前位置: 首页 > news >正文

Xxl-Job调度器原理解析

项目解析源码地址:https://gitee.com/lidishan/xxl-job-code-analysis
xxl-job版本:2.3.0
Xxl-Job分为执行器、调度器。而我们平时的客户端就属于一个执行器,执行器启动的时候会自动注册到调度器上,然后调度器进行远程调度。
调度器初始化过程步骤如下
1 国际化相关
配置参数: xxl.job.i18n=zh_CN, 这里设置为中文简体
2 初始化快线程fastTriggerPool、慢线程池slowTriggerPool
配置参数:xxl.job.triggerpool.fast.max=200, 这里设置为fastTriggerPool的最大线程数=200, 不能小于200
xxl.job.triggerpool.slow.max=100, 这里设置为slowTriggerPool的最大线程数=100, 不能小于100
3 启动注册监听线程
3.1 初始化registryOrRemoveThreadPool线程池:用于注册或者移除的线程池,客户端调用api/registry或api/registryRemove接口时,会用这个线程池进行注册或注销
3.2 启动监听注册的线程registryMonitorThread:清除心跳超过90s的注册信息,并且刷新分组注册信息
4 启动失败任务监听线程(重试、告警)
配置参数:spring.mail.from=xxx@qq.com, 告警邮箱
5 启动监控线程
5.1 初始化callbackThreadPool线程池:用于callback回调的线程池,客户端调用api/callback接口时会使用这个线程池
5.2 启动监控线monitorThread:调度记录停留在 "运行中" 状态超过10min,且对应执行器心跳注册失败不在线,则将本地调度主动标记失败
6 启动日志统计和清除线程logrThread
-- 日志记录刷新,刷新最近三天的日志Report(即统计每天的失败、成功、运行次数等)
-- 每天清除一次失效过期的日志数据
配置参数:xxl.job.logretentiondays=30, 清除xxl-job数据库日志的过期时间, 小于7天则不清除
7 启动任务调度(很重要!!主要靠这两个线程进行塞数据到时间轮,然后时间轮取数调度任务)
7.1 scheduleThread线程-取待执行任务数据入时间轮(塞数据)
-- 第一步:用select for update 数据库作为分布式锁加锁,避免多个xxl-job admin调度器节点同时执行
-- 第二步:预读数据从数据库中读取当前截止到五秒后内会执行的job信息,并且读取分页大小为preReadCount=6000条数据
----  preReadCount = (XxlJobAdminConfig.getAdminConfig().getTriggerPoolFastMax() + XxlJobAdminConfig.getAdminConfig().getTriggerPoolSlowMax()) * 20;
-- 第三步:将当前时间与下次调度时间对比,有如下三种情况
****  当前时间 大于 (任务的下一次触发时间 + PRE_READ_MS(5s)):可能是查询太久了,然后下面的代码刷新了任务下次执行时间,导致超过五秒,所以就需要特殊处理
--------  1、匹配过期失效的策略:DO_NOTHING=过期啥也不干,废弃;FIRE_ONCE_NOW=过期立即触发一次
--------  2、刷新上一次触发 和 下一次待触发时间
****  当前时间 大于 任务的下一次触发时间 并且是没有过期的:
--------  1、直接触发任务执行器
--------  2、刷新上一次触发 和 下一次待触发时间
--------  3、如果下一次触发在五秒内,直接放进时间轮里面待调度
----------------  1、求当前任务下一次触发时间所处一分钟的第N秒
----------------  2、将当前任务ID和ringSecond放进时间轮里面
----------------  3、刷新上一次触发 和 下一次待触发时间
****  当前时间 小于 下一次触发时间:
--------  1、求当前任务下一次触发时间所处一分钟的第N秒
--------  2、将当前任务ID和ringSecond放进时间轮里面
--------  3、刷新上一次触发 和 下一次待触发时间
-- 第四步:更新数据库执行器信息,如trigger_last_time、trigger_next_time
-- 第五步:提交数据库事务,释放数据库select for update排它锁
7.2 ringThread线程-根据时间轮执行job任务 (取数据执行)
首先时间轮数据格式为:Map<Integer, List<Integer>> ringData = new ConcurrentHashMap<>()
-- 第一步:获取当前所处的一分钟第几秒,然后for两次,第二次是为了重跑前面一个刻度没有被执行的的job list,避免前面的刻度遗漏了
-- 第二步:执行触发器
-- 第三步:清除当前刻度列表的数据
**** 执行的过程中还会选择对应的策略,如下:
-------- 阻塞策略:串行、废弃后面、覆盖前面
-------- 路由策略:取第一个、取最后一个、最小分发、一致性hash、快速失败、LFU最不常用、LRU最近最少使用、随机、轮询
初始化的入口代码为 XxlJobAdminConfig,代码如下:
@Component
public class XxlJobAdminConfig implements InitializingBean, DisposableBean {
    private static XxlJobAdminConfig adminConfig = null;
    public static XxlJobAdminConfig getAdminConfig() {
        return adminConfig;
    }
    // ---------------------- XxlJobScheduler ----------------------
    private XxlJobScheduler xxlJobScheduler;
    @Override
    public void afterPropertiesSet() throws Exception {// 生命周期中的属性注入来对xxlJobScheduler初始化
        adminConfig = this;
        // 初始化xxl-job定时任务
        xxlJobScheduler = new XxlJobScheduler();
        xxlJobScheduler.init();
    }
    @Override
    public void destroy() throws Exception { // 生命周期中的销毁来对xxlJobScheduler销毁
        xxlJobScheduler.destroy();
    }
    ..............省略..............
}
xxlJobScheduler.init()进行初始化会执行如下过程:
public class XxlJobScheduler {
    private static final Logger logger = LoggerFactory.getLogger(XxlJobScheduler.class);
    public void init() throws Exception {
        // 1 国际化相关 init i18n
        initI18n();
        // 2 初始化快线程池fastTriggerPool、慢线程池slowTriggerPool admin trigger pool start
        JobTriggerPoolHelper.toStart();
        // 3 启动注册监听线程 admin registry monitor run
        JobRegistryHelper.getInstance().start();
        // 4 启动失败任务监听线程(重试、告警) admin fail-monitor run
        JobFailMonitorHelper.getInstance().start();
        // 5 启动监控线程(调度记录停留在 "运行中" 状态超过10min,且对应执行器心跳注册失败不在线,则将本地调度主动标记失败)admin lose-monitor run ( depend on JobTriggerPoolHelper )
        JobCompleteHelper.getInstance().start();
        // 6 启动日志统计和清除线程(日志记录刷新,刷新最近三天的日志Report(即统计每天的失败、成功、运行次数等);每天清除一次失效过期的日志数据)admin log report start
        JobLogReportHelper.getInstance().start();
        // 7 启动任务调度(scheduleThread-取待执行任务数据入时间轮;ringThread-根据时间轮执行job任务) start-schedule ( depend on JobTriggerPoolHelper )
        JobScheduleHelper.getInstance().start();
        logger.info(">>>>>>>>> init xxl-job admin success.");
    }
    ................省略........................
}

上面初始化的7个步骤拆分如下==============
1 国际化相关
private void initI18n(){// 根据环境设置title为中文、英文等
    for (ExecutorBlockStrategyEnum item:ExecutorBlockStrategyEnum.values()) {
        item.setTitle(I18nUtil.getString("jobconf_block_".concat(item.name())));
    }
}
2 初始化快线程fastTriggerPool、慢线程池slowTriggerPool
这个步骤初始化了两个线程池fastTriggerPoolslowTriggerPool
在触发调度的时候会有一个选择快慢线程池的过程,如果job在一分钟内超过超过10次,就用slowTriggerPool来处理,如下:
ThreadPoolExecutor triggerPool_ = fastTriggerPool;
AtomicInteger jobTimeoutCount = jobTimeoutCountMap.get(jobId);
if (jobTimeoutCount!=null && jobTimeoutCount.get() > 10) { // job在一分钟内超过超过10次,就用slowTriggerPool来处理 job-timeout 10 times in 1 min
    triggerPool_ = slowTriggerPool;
}
triggerPool_.execute(new Runnable() {.........省略............}
3 启动注册监听线程
3.1 初始化registryOrRemoveThreadPool线程池:用于注册或者移除的线程池,客户端调用api/registry或api/registryRemove接口时,会用这个线程池进行注册或注销
3.2 启动监听注册的线程registryMonitorThread:清除心跳超过90s的注册信息,并且刷新分组注册信息
public void start(){
// 用于注册或者移除的线程池,客户端调用api/registry或api/registryRemove接口时,会用这个线程池进行注册或注销    for registry or remove
registryOrRemoveThreadPool = new ThreadPoolExecutor(
2,
10,
30L,
TimeUnit.SECONDS,
new LinkedBlockingQueue<Runnable>(2000),
new ThreadFactory() {
@Override
public Thread newThread(Runnable r) {
return new Thread(r, "xxl-job, admin JobRegistryMonitorHelper-registryOrRemoveThreadPool-" + r.hashCode());
}
},
new RejectedExecutionHandler() {
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
r.run();
logger.warn(">>>>>>>>>>> xxl-job, registry or remove too fast, match threadpool rejected handler(run now).");
}
});
// 启动监听注册的线程      for monitor
registryMonitorThread = new Thread(new Runnable() {
@Override
public void run() {
while (!toStop) {
try {
// 获取自动注册的执行器组(执行器地址类型:0=自动注册、1=手动录入) auto registry group
List<XxlJobGroup> groupList = XxlJobAdminConfig.getAdminConfig().getXxlJobGroupDao().findByAddressType(0);
if (groupList!=null && !groupList.isEmpty()) {// group组集合不为空
// 移除死掉的调用地址(心跳时间超过90秒,就当线程挂掉了。默认是30s做一次心跳)          remove dead address (admin/executor)
List<Integer> ids = XxlJobAdminConfig.getAdminConfig().getXxlJobRegistryDao().findDead(RegistryConfig.DEAD_TIMEOUT, new Date());
if (ids!=null && ids.size()>0) {// 移除挂掉的注册地址信息
XxlJobAdminConfig.getAdminConfig().getXxlJobRegistryDao().removeDead(ids);
}
// fresh online address (admin/executor)
HashMap<String, List<String>> appAddressMap = new HashMap<String, List<String>>();
// 找出所有正常没死掉的注册地址
List<XxlJobRegistry> list = XxlJobAdminConfig.getAdminConfig().getXxlJobRegistryDao().findAll(RegistryConfig.DEAD_TIMEOUT, new Date());
if (list != null) {
for (XxlJobRegistry item: list) {
// 确保是 EXECUTOR 执行器类型
if (RegistryConfig.RegistType.EXECUTOR.name().equals(item.getRegistryGroup())) {
String appname = item.getRegistryKey();
List<String> registryList = appAddressMap.get(appname);
if (registryList == null) {
registryList = new ArrayList<String>();
}
if (!registryList.contains(item.getRegistryValue())) {
registryList.add(item.getRegistryValue());
}
appAddressMap.put(appname, registryList);
}
}
}
// 刷新分组注册地址信息  fresh group address
for (XxlJobGroup group: groupList) {
List<String> registryList = appAddressMap.get(group.getAppname());
String addressListStr = null;
if (registryList!=null && !registryList.isEmpty()) {
Collections.sort(registryList);
StringBuilder addressListSB = new StringBuilder();
for (String item:registryList) {
addressListSB.append(item).append(",");
}
addressListStr = addressListSB.toString();
addressListStr = addressListStr.substring(0, addressListStr.length()-1);
}
group.setAddressList(addressListStr);
group.setUpdateTime(new Date());
XxlJobAdminConfig.getAdminConfig().getXxlJobGroupDao().update(group);
}
}
} catch (Exception e) {
if (!toStop) {
logger.error(">>>>>>>>>>> xxl-job, job registry monitor thread error:{}", e);
}
}
try {
TimeUnit.SECONDS.sleep(RegistryConfig.BEAT_TIMEOUT);
} catch (InterruptedException e) {
if (!toStop) {
logger.error(">>>>>>>>>>> xxl-job, job registry monitor thread error:{}", e);
}
}
}
logger.info(">>>>>>>>>>> xxl-job, job registry monitor thread stop");
}     
});
registryMonitorThread.setDaemon(true);
registryMonitorThread.setName("xxl-job, admin JobRegistryMonitorHelper-registryMonitorThread");
registryMonitorThread.start();
}
4 启动失败任务监听线程(重试、告警)
这部分逻辑比较简单,就是重试 + 告警,核心代码如下
// 获取执行失败的job信息
List<Long> failLogIds = XxlJobAdminConfig.getAdminConfig().getXxlJobLogDao().findFailJobLogIds(1000);
if (failLogIds!=null && !failLogIds.isEmpty()) {
for (long failLogId: failLogIds) {
// lock log
int lockRet = XxlJobAdminConfig.getAdminConfig().getXxlJobLogDao().updateAlarmStatus(failLogId, 0, -1);
if (lockRet < 1) {
continue;
}
XxlJobLog log = XxlJobAdminConfig.getAdminConfig().getXxlJobLogDao().load(failLogId);
XxlJobInfo info = XxlJobAdminConfig.getAdminConfig().getXxlJobInfoDao().loadById(log.getJobId());
 // 1、失败塞回重试       fail retry monitor
if (log.getExecutorFailRetryCount() > 0) {
JobTriggerPoolHelper.trigger(log.getJobId(), TriggerTypeEnum.RETRY, (log.getExecutorFailRetryCount()-1), log.getExecutorShardingParam(), log.getExecutorParam(), null);
String retryMsg = "<span style=\"color:#F39C12;\" > >>>>>>>>>>>"+ I18nUtil.getString("jobconf_trigger_type_retry") +"<<<<<<<<<<< </span>";
log.setTriggerMsg(log.getTriggerMsg() + retryMsg);
XxlJobAdminConfig.getAdminConfig().getXxlJobLogDao().updateTriggerInfo(log);
}
// 2、进行失败告警       fail alarm monitor
      int newAlarmStatus = 0;       // 告警状态:0-默认、-1=锁定状态、1-无需告警、2-告警成功、3-告警失败
if (info!=null && info.getAlarmEmail()!=null && info.getAlarmEmail().trim().length()>0) {
boolean alarmResult = XxlJobAdminConfig.getAdminConfig().getJobAlarmer().alarm(info, log);
newAlarmStatus = alarmResult?2:3;
} else {
newAlarmStatus = 1;
}
XxlJobAdminConfig.getAdminConfig().getXxlJobLogDao().updateAlarmStatus(failLogId, -1, newAlarmStatus);
}
}
5 启动监控线程
5.1 初始化callbackThreadPool线程池:用于callback回调的线程池,客户端调用api/callback接口时会使用这个线程池
5.2 启动监控线monitorThread:调度记录停留在 "运行中" 状态超过10min,且对应执行器心跳注册失败不在线,则将本地调度主动标记失败
逻辑较简单,如上两点
6 启动日志统计和清除线程logrThread
-- 日志记录刷新,刷新最近三天的日志Report(即统计每天的失败、成功、运行次数等)
-- 每天清除一次失效过期的日志数据
配置参数:xxl.job.logretentiondays=30, 清除xxl-job数据库日志的过期时间, 小于7天则不清除
逻辑较简单,如上两点
7 启动任务调度
7.1 scheduleThread-取待执行任务数据入时间轮
-- 第一步:用select for update 数据库作为分布式锁加锁,避免多个xxl-job admin调度器节点同时执行
-- 第二步:预读数据,从数据库中读取当前截止到五秒后内会执行的job信息,并且读取分页大小为preReadCount=6000条数据
----  preReadCount = (XxlJobAdminConfig.getAdminConfig().getTriggerPoolFastMax() + XxlJobAdminConfig.getAdminConfig().getTriggerPoolSlowMax()) * 20;
-- 第三步:将当前时间与下次调度时间对比,有如下三种情况
****  当前时间 大于 (任务的下一次触发时间 + PRE_READ_MS(5s)):可能是查询太久了,然后下面的代码刷新了任务下次执行时间,导致超过五秒,所以就需要特殊处理
--------  1、匹配过期失效的策略:DO_NOTHING=过期啥也不干,废弃;FIRE_ONCE_NOW=过期立即触发一次
--------  2、刷新上一次触发 和 下一次待触发时间
****  当前时间 大于 任务的下一次触发时间 并且是没有过期的:
--------  1、直接触发任务执行器
--------  2、刷新上一次触发 和 下一次待触发时间
--------  3、如果下一次触发在五秒内,直接放进时间轮里面待调度
----------------  1、求当前任务下一次触发时间所处一分钟的第N秒
----------------  2、将当前任务ID和ringSecond放进时间轮里面
----------------  3、刷新上一次触发 和 下一次待触发时间
****  当前时间 小于 下一次触发时间:
--------  1、求当前任务下一次触发时间所处一分钟的第N秒
--------  2、将当前任务ID和ringSecond放进时间轮里面
--------  3、刷新上一次触发 和 下一次待触发时间
-- 第四步:更新数据库执行器信息,如trigger_last_time、trigger_next_time
-- 第五步:提交数据库事务,释放数据库select for update排它锁
7.2 ringThread-根据时间轮执行job任务
首先时间轮数据格式为:Map<Integer, List<Integer>> ringData = new ConcurrentHashMap<>()
-- 第一步:获取当前所处的一分钟第几秒,然后for两次,第二次是为了重跑前面一个刻度没有被执行的的job list,避免前面的刻度遗漏了
-- 第二步:执行触发器
-- 第三步:清除当前刻度列表的数据
**** 执行的过程中还会选择对应的策略,如下:
-------- 阻塞策略:串行、废弃后面、覆盖前面
-------- 路由策略:取第一个、取最后一个、最小分发、一致性hash、快速失败、LFU最不常用、LRU最近最少使用、随机、轮询
启动两个线程解析的核心源码如下:
public void start(){
    // 启动调度线程,这些线程是用来取数据的 schedule thread
    scheduleThread = new Thread(new Runnable() {
    @Override
    public void run() {
    try {// 不知道为啥要休眠 4-5秒 时间,然后再启动
        TimeUnit.MILLISECONDS.sleep(5000 - System.currentTimeMillis()%1000 );
    } catch (InterruptedException e) {
        if (!scheduleThreadToStop) {
            logger.error(e.getMessage(), e);
        }
    }
    logger.info(">>>>>>>>> init xxl-job admin scheduler success.");
    // 这里是预读数量 pre-read count: treadpool-size * trigger-qps (each trigger cost 50ms, qps = 1000/50 = 20)
    int preReadCount = (XxlJobAdminConfig.getAdminConfig().getTriggerPoolFastMax() + XxlJobAdminConfig.getAdminConfig().getTriggerPoolSlowMax()) * 20;
    while (!scheduleThreadToStop) {
    // 扫描任务 Scan Job
    long start = System.currentTimeMillis();
    Connection conn = null;
    Boolean connAutoCommit = null;
    PreparedStatement preparedStatement = null
    boolean preReadSuc = true;
    try {
        conn = XxlJobAdminConfig.getAdminConfig().getDataSource().getConnection();
        connAutoCommit = conn.getAutoCommit();
        conn.setAutoCommit(false);
        // 采用select for update,是排它锁。说白了xxl-job用一张数据库表来当分布式锁了,确保多个xxl-job admin节点下,依旧只能同时执行一个调度线程任务
        preparedStatement = conn.prepareStatement( "select * from xxl_job_lock where lock_name = 'schedule_lock' for update" );
        preparedStatement.execute();
        // tx start
        // 1、预读数据 pre read
        long nowTime = System.currentTimeMillis();
        // -- 从数据库中读取截止到五秒后未执行的job,并且读取preReadCount=6000
        List<XxlJobInfo> scheduleList = XxlJobAdminConfig.getAdminConfig().getXxlJobInfoDao().scheduleJobQuery(nowTime + PRE_READ_MS, preReadCount);
        if (scheduleList!=null && scheduleList.size()>0) {
            // 2push压进 时间轮 push time-ring
            for (XxlJobInfo jobInfo: scheduleList) {
                // time-ring jump
                if (nowTime > jobInfo.getTriggerNextTime() + PRE_READ_MS) {
                    // 当前时间 大于 (任务的下一次触发时间 + PRE_READ_MS5s)),可能是查询太久了,然后下面的代码刷新了任务下次执行时间,导致超过五秒,所以就需要特殊处理
                    // 2.1trigger-expire > 5spass && make next-trigger-time
                    logger.warn(">>>>>>>>>>> xxl-job, schedule misfire, jobId = " + jobInfo.getId());
                    // 1、匹配过期失效的策略:DO_NOTHING=过期啥也不干,废弃;FIRE_ONCE_NOW=过期立即触发一次 misfire match
                    MisfireStrategyEnum misfireStrategyEnum = MisfireStrategyEnum.match(jobInfo.getMisfireStrategy(), MisfireStrategyEnum.DO_NOTHING);
                    if (MisfireStrategyEnum.FIRE_ONCE_NOW == misfireStrategyEnum) {
                        // FIRE_ONCE_NOW trigger
                        JobTriggerPoolHelper.trigger(jobInfo.getId(), TriggerTypeEnum.MISFIRE, -1, null, null, null);
                        logger.debug(">>>>>>>>>>> xxl-job, schedule push trigger : jobId = " + jobInfo.getId() );
                    }
                    // 2、刷新上一次触发 和 下一次待触发时间 fresh next
                    refreshNextValidTime(jobInfo, new Date());
                } else if (nowTime > jobInfo.getTriggerNextTime()) {
                    // 当前时间 大于 任务的下一次触发时间 并且是没有过期的
                    // 2.2trigger-expire < 5sdirect-trigger && make next-trigger-time
                    // 1、直接触发任务执行器 trigger
                    JobTriggerPoolHelper.trigger(jobInfo.getId(), TriggerTypeEnum.CRON, -1, null, null, null);
                    logger.debug(">>>>>>>>>>> xxl-job, schedule push trigger : jobId = " + jobInfo.getId() );
                    // 2、刷新上一次触发 和 下一次待触发时间 fresh next
                    refreshNextValidTime(jobInfo, new Date());
                    // 如果下一次触发在五秒内,直接放进时间轮里面待调度 next-trigger-time in 5s, pre-read again
                    if (jobInfo.getTriggerStatus()==1 && nowTime + PRE_READ_MS > jobInfo.getTriggerNextTime()) {
                        // 1、求当前任务下一次触发时间所处一分钟的第N make ring second
                        int ringSecond = (int)((jobInfo.getTriggerNextTime()/1000)%60);
                        // 2、将当前任务IDringSecond放进时间轮里面 push time ring
                        pushTimeRing(ringSecond, jobInfo.getId());
                        // 3、刷新上一次触发 和 下一次待触发时间 fresh next
                        refreshNextValidTime(jobInfo, new Date(jobInfo.getTriggerNextTime()));
                    }
                } else {
                    // 当前时间 小于 下一次触发时间
                    // 2.3trigger-pre-readtime-ring trigger && make next-trigger-time
                    // 1、求当前任务下一次触发时间所处一分钟的第N make ring second
                    int ringSecond = (int)((jobInfo.getTriggerNextTime()/1000)%60);
                    // 2、将当前任务IDringSecond放进时间轮里面 push time ring
                    pushTimeRing(ringSecond, jobInfo.getId());
                    // 3、刷新上一次触发 和 下一次待触发时间 fresh next
                    refreshNextValidTime(jobInfo, new Date(jobInfo.getTriggerNextTime()));
                }
            }
            // 3、更新数据库执行器信息,如trigger_last_timetrigger_next_time update trigger info
            for (XxlJobInfo jobInfo: scheduleList) {
                XxlJobAdminConfig.getAdminConfig().getXxlJobInfoDao().scheduleUpdate(jobInfo);
            }
        } else {
            preReadSuc = false;
        }
        // tx stop
    } catch (Exception e) {
        if (!scheduleThreadToStop) {
            logger.error(">>>>>>>>>>> xxl-job, JobScheduleHelper#scheduleThread error:{}", e);
        }
    } finally {
        // 提交事务,释放数据库select for update的锁 commit
        .......................省略.............   
    }
    long cost = System.currentTimeMillis()-start;
    // 如果执行太快了,就稍微sleep等待一下 Wait seconds, align second
    if (cost < 1000) { // scan-overtime, not wait
        try {
            // pre-read period: success > scan each second; fail > skip this period;
            TimeUnit.MILLISECONDS.sleep((preReadSuc?1000:PRE_READ_MS) - System.currentTimeMillis()%1000);
        } catch (InterruptedException e) {
            if (!scheduleThreadToStop) {
                logger.error(e.getMessage(), e);
            }
        }
    });
    scheduleThread.setDaemon(true);
    scheduleThread.setName("xxl-job, admin JobScheduleHelper#scheduleThread");
    scheduleThread.start();
    // 时间轮线程,用于取出每秒的数据,然后处理 ring thread
    ringThread = new Thread(new Runnable() {
        @Override
        public void run() {
            while (!ringThreadToStop) {
                // align second
                try {
                    TimeUnit.MILLISECONDS.sleep(1000 - System.currentTimeMillis() % 1000);
                } catch (InterruptedException e) {
                    if (!ringThreadToStop) {
                        logger.error(e.getMessage(), e);
                    }
                }
                try {
                    // second data
                    List<Integer> ringItemData = new ArrayList<>();
                    // 获取当前所处的一分钟第几秒,然后for两次,第二次是为了重跑前面一个刻度没有被执行的的job list,避免前面的刻度遗漏了
                    int nowSecond = Calendar.getInstance().get(Calendar.SECOND); // 避免处理耗时太长,跨过刻度,向前校验一个刻度;
                    for (int i = 0; i < 2; i++) {
                        List<Integer> tmpData = ringData.remove( (nowSecond+60-i)%60 );
                        if (tmpData != null) {
                            ringItemData.addAll(tmpData);
                        }
                    }
                    // ring trigger
                    logger.debug(">>>>>>>>>>> xxl-job, time-ring beat : " + nowSecond + " = " + Arrays.asList(ringItemData) );
                    if (ringItemData.size() > 0) {
                        // do trigger
                        for (int jobId: ringItemData) {
                            // 执行触发器 do trigger
                            JobTriggerPoolHelper.trigger(jobId, TriggerTypeEnum.CRON, -1, null, null, null);
                        }
                        // 清除当前刻度列表的数据 clear
                        ringItemData.clear();
                    }
                } catch (Exception e) {
                    if (!ringThreadToStop) {
                        logger.error(">>>>>>>>>>> xxl-job, JobScheduleHelper#ringThread error:{}", e);
                    }
                }
            }
            logger.info(">>>>>>>>>>> xxl-job, JobScheduleHelper#ringThread stop");
        }
    });
    ringThread.setDaemon(true);
    ringThread.setName("xxl-job, admin JobScheduleHelper#ringThread");
    ringThread.start();
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.exyb.cn/news/show-2862.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈,一经查实,立即删除!

webrtc之SVC实现(十)

一、概念 SVC&#xff08;可适性视频编码或可分级视频编码&#xff09;是传统H.264/MPEG-4 AVC编码的延伸&#xff0c;可提升更大的编码弹性&#xff0c;并具有时间可适性&#xff08;Temporal Scalability&#xff09;、空间可适性&#xff08;Spatial Scalability&#xff09…...

LeetCode 数值的整数次方

实现 pow(x, n) &#xff0c;即计算 x 的 n 次幂函数&#xff08;即&#xff0c;xn&#xff09;。不得使用库函数&#xff0c;同时不需要考虑大数问题。 示例 1&#xff1a; 输入&#xff1a;x 2.00000, n 10 输出&#xff1a;1024.00000 示例 2&#xff1a; 输入&#xf…...

python 继承和多态

在已有类的基础上创建新类&#xff0c;这其中的一种做法就是让一个类从另一个类那里将属性和方法直接继承下来&#xff0c;从而减少重复代码的编写。提供继承信息的我们称之为父类&#xff0c;也叫超类或基类&#xff1b;得到继承信息的我们称之为子类&#xff0c;也叫派生类或…...

JAVA 基础学习之 继承与方法覆写

1 继承引入​​​​​​​ 三个类都有重复的代码&#xff0c;可以把这共同的代码抽出去&#xff0c;抽出去放到另外一个类里面&#xff1b;下面的3个类和上面的类需要发生一点关系&#xff08;继承&#xff09;&#xff0c;上面的类叫做 父类&#xff08;超类&#xff0c;基类&…...

个性化个人主页html5模板

介绍&#xff1a; 个性化个人主页html5模板 网盘下载地址&#xff1a; http://kekewl.net/tARDuX5n02U0 图片&#xff1a;...

java IO教程《三》

缓冲区流讲解(Buffered) 什么是缓冲区&#xff1f; 缓冲流&#xff0c;也叫高效流&#xff0c;是对4个基本的File流的增强&#xff0c;所以也是4个流&#xff0c;按照数据类型分类&#xff1a; 字节缓冲流&#xff1a;BufferedInputStream&#xff0c;BufferedOutputStream字…...

elementUI-Tree 树形控件的使用

elementUI-Tree 树形控件的使用 实现效果&#xff1a; 控件的官方使用说明 控件要求返回的数据结构 {"success": true,"code": 20000,"message": "成功","data": {"items": [{"id": "1394579386…...

实训第一天以及第二天所学记录

实训第一天以及第二天所学记录 浏览器内核 IE&#xff1a;Trident Firefox&#xff1a;Gecko Chrome&#xff1a;Webkit / Blink Safari&#xff1a;Webkit Opera&#xff1a;Presto / Blink 在VScode中使用注释的快捷键 按住键盘的Ctrl/ 元素 &#xff08;标签 标记&…...

跨域请求

/* * Description: 配置文件 */ module.exports { publicPath: "./", devServer: { open: true, proxy: "http://106.15.179.105/api" //跨域路径 }, }; // proxy是代理的意思 // 代理跨域就是在欺骗浏览器 让浏览器认为你访问的还是 同…...

Mac os下通过Anaconda在远程服务器配置python虚拟环境

一、SSH管理软件 这里推荐一款本人正在使用的软件&#xff0c;Termius。Termius是一款非常好用而且漂亮的SSH客户端&#xff0c;能快速远程控制服务器&#xff0c;可以定制自己喜欢的主题.Termius不仅涵盖了Windows、Linux、OSX&#xff0c;还变态得支持Android和iOS&#xff…...

Labview需求(部分)

本人从事工控行业多年,有一些资源,目前labview的单子干不过来了,想找几个靠谱的labview工程师跟我合作,想做兼职,接单的labview工程师可以私聊我,另外我有一些项目还需要跟单片机工程师,plc工程师,fpga工程师合作,欢迎大家找我合作!以下是手里比较着急的项目,需要外包 需求1&am…...

2021java1年经验公司面试真题

1面我就省略了&#xff0c;主要就是看你的以前工作情况&#xff0c;以前工作内容能不能清晰表达&#xff0c;还有一些简单的java基础问题&#xff0c;大概20多分钟。二面就是40分钟基础&#xff0c;20分钟业务&#xff0c;10分钟个人情况。下面是二面问题 1.自我介绍 做一下自…...

Unity基础之C#核心篇笔记4:多态

Unity基础之C#核心篇笔记4&#xff1a;多态多态1.多态的概念2.解决的问题3.多态的实现4.总结抽象类和抽象方法1.抽象类2.抽象函数3.总结4.练习题接口1.接口的概念2. 接口的申明3.接口的使用4.接口可以继承接口5.显示实现接口6.总结7.练习题密封方法1.密封方法基本概念2.实例3.总…...

LeetCode每日一题 - 有多少小于当前数字的数字

题目&#xff1a; 给你一个数组 nums&#xff0c;对于其中每个元素 nums[i]&#xff0c;请你统计数组中比它小的所有数字的数目。 换而言之&#xff0c;对于每个 nums[i] 你必须计算出有效的 j 的数量&#xff0c;其中 j 满足 j ! i 且 nums[j] < nums[i] 。 以数组形式返回…...

用Python爬取彼岸图网图片

用Python爬取彼岸图网图片 *使用了 四个模块 import time import requests from lxml import etree import os 没有的话自行百度安装。 #encoding utf-8 import time import requests from lxml import etree import os# http://www.netbian.com/ 爬虫 if __name__ __mai…...

第k个数(快速选择)

算法思路 快速选择&#xff1a; 1.即任意选一个数&#xff0c;将数组划分为二。 2.最终根据该数所在的位置&#xff0c;即第&#xff1f;大&#xff0c;选择第k大的数字所在区间进行划分。 时间复杂度分析&#xff0c;第一次划分n,下一次划分期望n/2&#xff0c;n/4…累加和小…...

yolov5 detect.py报错

新手求助 yolov5训练了自己的数据集后&#xff0c;test没问题&#xff0c;但是运行detect.py就报错了&#xff0c;一直找不到问题所在&#xff0c; 求大佬指点指点...

DEX 争霸战火升级,BabySwap 会否成为下一代黑马?

在 AMM机制大力推动下&#xff0c;DEX的群雄争霸比预想中来得要快且凶猛。伴随着诸如高盛等主流资本逐步认可DeFi 的意义&#xff0c;在真正的去中心化金融爆发的前夜&#xff0c;可以说谁抢占了DEX 红海的先机&#xff0c;谁就赢得了未来。 从早期以太坊的Uniswap和DoDo&…...

flutter 常用的第三方组件

引用文章链接&#xff1a; https://www.jianshu.com/p/a523e5f131b2 1、格式化日期时间组件&#xff1a;https://pub.dev/packages/date_format 2、日期选择组件&#xff1a;https://pub.dev/packages/flutter_cupertino_date_picker 3、轮播图组件&#xff1a;https://pub.…...

Android面试回忆录:帮助程序员提高核心竞争力的30条建议,真香!

**新技术层出不穷&#xff0c;去年kotlin到如今Flutter&#xff0c;技术迭代&#xff0c;你是否会变得固步自封&#xff1f;**那么看本篇文章帮你解决问题&#xff0c;让你知道怎么样学习&#xff0c;学习那些技术点才能不被时代的迭代快速淘汰&#xff01; 首先&#xff0c;先…...

互联网发展简史(1)ARPANET的建立

原文为知乎&#xff0c;现转移到个人博客。 知乎原文 原文为微信公众号原文&#xff0c;现转移到个人博客。 微信公众号原文 互联网发展简史(1)ARPANET的建立 目录 [TOC] Internet发展简史&#xff08;ARPANET的建立&#xff09; 1955年 要了解互联网的起源&#xff0c;…...

XILINX公司的JESD204 IP核介绍(一) 概述

XILINX公司的JESD204 IP核能够实现复杂的JESD204B协议&#xff0c;支持的速度范围为1Gbps~12.5Gbps。该IP核可以被配置成发送器或者接收器&#xff0c;不能配置成同时收发。目前该IP核仅支持vivado软件&#xff0c;不支持ISE&#xff0c;且仅支持xilinx公司的7系列及其以上系列…...

【CoppeliaSim】Solidworks中模型导出模型到V-REP

一、下载安装插件 插件名称&#xff1a;sw2urdfSetup 下载地址&#xff1a;https://github.com/ros/solidworks_urdf_exporter/releases/tag/1.6.0 下载后直接安装即可。安装成功后&#xff0c;可在SolidWorks的菜单栏中看到&#xff1a;工具-Tools-Export as URDF 二、导出…...

适合项目经理使用的企业知识库一站式解决方案

搭建企业知识库迫在眉睫 在《2020年德勤全球人力资本趋势》报告显示&#xff0c;多达75&#xff05;的受访者声称&#xff0c;企业在不断发展的员工队伍中创造和保存知识对于他们在未来12-18个月的成功至关重要。因此建立科学的内部知识管理体系&#xff0c;对企业发展将会起到…...

mysql性能优化基础介绍

** MySQL性能优化基础知识 ** 1.mysql逻辑架构分层 最上层(连接/线程处理)&#xff1a;是基于网络的客户端/服务器的工具或者服务都具有类似架构。主要负责连接处理&#xff0c;权限控制和安全等。 中间层(解析&#xff0c;查询缓存&#xff0c;优化)&#xff1a;MySQL的核…...

鸿蒙系统中DirectionalLayout线性布局

鸿蒙系统中DirectionalLayout线性布局前言前期准备新建项目新建线性布局页面排列方式垂直排列水平排列对其方式权重前言 DirectionalLayout布局用于将一组组件(Component)按照水平或者垂直方向排布&#xff0c;能够方便地对齐布局内的组件。 DirectionalLayout的自有XML属性以…...

学生成绩管理系统+打包

文章目录文件管理&#xff1a;D:\hyt\python\pytorch_graduate\file_management成绩管理&#xff1a;D:\hyt\python\pytorch_graduate\stusystem打包&#xff1a;D:\hyt\python\pytorch_graduate\package_test学生成绩管理系统0.基础原理1.文件管理2 学生管理系统Tk打包1.基础原…...

为什么说HTTPS比HTTP安全呢

HTTP 协议 HTTP&#xff08;Hyper Text Transfer Protocol&#xff09;协议是超文本传输协议的缩写&#xff0c;它是从WEB服务器传输超文本标记语言(HTML)到本地浏览器的传送协议&#xff0c;位于 OSI 网络模型中的应用层 HTTP 是一个基于TCP/IP通信协议来传递数据的协议&…...

VMware下Ubuntu系统扩展硬盘方法

&#xff08;1&#xff09;首先在虚拟机里面关闭Ubuntu系统&#xff0c;如下图所示。 &#xff08;2&#xff09;在VMware里点击菜单虚拟机——>设置...&#xff0c;如下图所示。 &#xff08;3&#xff09;然后弹出“虚拟机设置”对话框&#xff0c;如下图所示。 &#xff…...

模拟实现堆栈将中缀算术表达式转换成后缀表达式

1、随机生成100个0到200的整数 用折半查找法&#xff08;二分法&#xff09;查找50是初始数据的第几个数&#xff0c; 并输出查找过程&#xff08;即和什么数进行了比较&#xff09;。 public class Random0To200 {public int random;public int num;public void display(){Sy…...

C++数据结构——字符串

C数据结构——字符串 参考博客或网址&#xff1a; &#xff08;1&#xff09;菜鸟教程——C字符串 &#xff08;2&#xff09;https://blog.csdn.net/ylh1234/article/details/64929992 &#xff08;3&#xff09;https://blog.csdn.net/fenxinzi557/article/details/514578…...

inline函数和宏定义区别 整理

本文转自http://blog.chinaunix.net/xmlrpc.php?rblog/article&uid29235952&id4206608 内联函数和普通函数相比可以加快程序运行的速度&#xff0c;因为不需要中断调用&#xff0c;在编译的时候内联函数可以直接呗镶嵌到目标代码中。内联函数要做参数类型检查&#xf…...

define和inline的区别

1&#xff0c;define&#xff1a;定义预编译时处理的宏&#xff0c;只是简单的字符串替换&#xff0c;无类型检查。 2&#xff0c;inline&#xff1a;关键字用来定义一个类的内联函数&#xff0c;引入它的主要原因是用它替代C中表达式形式的宏定义&#xff0c;编译阶段完成。 …...

matlab的输入字符串接收,matlab字符串操作总结

字符串操作总结char(S1,S2,…)利用给定的字符串或单元数组创建字符数组double(S)将字符串转化成ASC码形式cellstr(S)利用的给定的字符数组创建字符串单元数组blanks(n)生成一个由n个空格组成的字符串deblank(S)删除尾部的空格eval(S) evalc(S)使用MATLAB解释器求字符串表达式的…...

C++17之 Inline变量

c的一个优点是它支持只使用头文件库的开发。然而&#xff0c;c 17之前&#xff0c;头文件中不需要或不提供全局变量或对象时才有可能成为一个库。c 17可以在头文件中定义一个内联的变量/对象&#xff0c;如果这个定义被多个编译单元使用&#xff0c;它们都指向同一个惟一的对象…...

Inline Hook

内存攻击中,最简单的就是IAT Hook.但是在高版本的Windows系统里,IAT表是不允许修改的,所以,IAT Hook的方法便不奏效了.于是便出现了Inline Hook技术.这里,结合前面对PE文件的了解,从我写的内存注入代码中剪切出了Inline Hook的部分进行讲解,因为是部分代码,所以在测试的时候,只…...

c语言inline有什么作用,C语言inline关键字

一、inline关键字的概念inline关键字是C99标准的型关键字&#xff0c;其作用是将函数展开&#xff0c;吧函数的代码复制到每一个调用处。这样调用函数的过程就可以直接执行函数代码&#xff0c;而不发生跳转、压栈等一般性函数操作。可以节省时间&#xff0c;也会提高程序的执行…...

NVIDIA 7th SkyHackathon(六)Tao 目标检测模型训练与评估

1.模型准备 1.1 安装 NGC CLI 使用 NGC CLI 来获得预训练模型&#xff0c;关于 NGC 的详细信息可访问 官网 中的 SETUP 了解 # 创建 NGC CLI 目录 %env CLIngccli_cat_linux.zip mkdir -p $LOCAL_PROJECT_DIR/ngccli# 移除之前安装的 NGC CLI rm -rf $LOCAL_PROJECT_DIR/ngc…...

python将数据变成float32,numpy将float32转换为float64

I want to perform some standard operations on numpy float32 arrays in python 3, however Im seeing some strange behavior when working with numpy sum(). Heres an example session:Python 3.6.1 |Anaconda 4.4.0 (x86_64)| (default, May 11 2017, 13:04:09)[GCC 4.2...

mysql float 1,MySql中float类型含义及参数详解

float表示浮点数&#xff0c;通俗点来说的话&#xff0c;我们可以简单理解为小数参数有两个&#xff1a;M表示精度&#xff0c;表示浮点数的位数D表示标度&#xff0c;表示小数位数M位数不包括小数点位数举例&#xff1a;float(6,2)则最大范围表示&#xff1a;-9999.99 ------ …...

float c语言存储格式,float a=1.0f 这里的1.0f中的“f”代表什么 ?float的储存格式?...

float a1.0f 这里的1.0f中的“f”代表什么&#xff0c;有什么意思&#xff0c;在C语言里面&#xff0c;解答详细点啊&#xff01;&#xff01;&#xff01;f 代表这个数据是float类型的常量&#xff0c;如果你直接输入1.0就是double类型 &#xff0c;当你赋给float类型的时候就…...

float数组 java_如何在Java中将Float数组列表转换为float数组?

让我们首先创建一个浮点数组列表-ArrayList arrList new ArrayList ();arrList.add(5.2 f);arrList.add(10.3 f);arrList.add(15.3 f);arrList.add(20.4 f);现在&#xff0c;将float数组列表转换为float数组。首先&#xff0c;我们为浮点数组设置了相同的大小&#xff0c;即…...

float数据格式

通常浮点数&#xff08;Floating Point Number&#xff09;格式采用IEEE-754标准&#xff08;32&#xff09;&#xff0c;用四个字节共32位表示。浮点数格式如下&#xff1a; D31D30~D23D22~D0浮点数符号位阶码尾数符号位 s&#xff08;Sign&#xff09;决定数是正数&#xff…...

float 乘法的坑

BigDecimal bPrice new BigDecimal(0); int choice_device_count 0; float mTotal_money 0;String price "0.01"; bPrice new BigDecimal(price); choice_device_count 5;BigDecimal bCount new BigDecimal(choice_device_count); mTotal_money bPrice.multi...

mysql float 多少位_mysql float 默认长度

内容整理如下&#xff1a;1&#xff0c;整型&#xff1a;TINYINT 1 字节SMALLINT 2 个字节MEDIUMINT 3 个字节INT 4 个字节INTEGER 4 个字节BIGINT 8 个字节2&#xff0c;浮点型&#xff1a;FLOAT(X) 4 如果 X < 24 或 8 如果 25 < X < 53FLOAT 4 个字节DOUBLE 8 个…...

js中float运算

/浮点数加法运算 function FloatAdd(arg1,arg2){ var r1,r2,m; try{r1arg1.toString().split(".")[1].length}catch(e){r10} try{r2arg2.toString().split(".")[1].length}catch(e){r20} mMath.pow(10,Math.max(r1,r2)); return (arg1marg2m)/m; } //浮点…...

float类型

float类型遵循IEEE754标准&#xff0c;该标准为32位浮点数规定了二进制表示形式。IEEE754采用二进制的科学计数法来表示浮点数。对于float浮点数&#xff0c;用1位表示数字的符号&#xff08;浮点数正负性&#xff0c;0正1负&#xff09;&#xff0c;8位来表示指数&#xff08;…...

[七]基础数据类型之Float详解

Float 基本数据类型float 的包装类 Float 类型的对象包含一个 float 类型的字段 属性简介 用来以二进制补码形式表示 float 值的比特位数public static final int SIZE 32;二进制补码形式表示 float 值的字节数public static final int BYTES SIZE / Byte.SIZE;表示基本类…...

Float的取值范围及存储结构

Float的取值范围 写文章-CSDN博客https://mp.csdn.net/mp_blog/creation/editor/120832448 Float的存储结构 浮点数在机内用指数型式表示&#xff0c;分解为&#xff1a; 数符&#xff0c;尾数&#xff0c;指数符&#xff0c;指数四部分。 数符占1位二进制&#xff0c;表示…...

float的基本用法

浮动 float:left;float:right; 和display的区别&#xff1a; 1、没有空格解析&#xff1b; 2、display的行内是以基线对齐的&#xff0c;使用margin-top会出现行内所有元素全部下移&#xff1b; 3、浮动没有基线问题&#xff1b; 4、display不能控制方向&#xff1b; 文档…...