quartz (从原理到应用)详解篇
一、Quartz 基本介绍
1.1 Quartz 概述
1.2 Quartz特点
1.3 Quartz 集群配置
二、Quartz 原理及流程
2.1 quartz基本原理
2.2 quartz启动流程
三、Spring + Quartz 实现企业级调度的实现示例
3.1 环境信息
3.2 相关代码及配置
四、问题及解决方案
五、相关知识
六、参考资料
总结
一、Quartz 基本介绍
1.1 Quartz 概述
Quartz 是 OpenSymphony 开源组织在任务调度领域的一个开源项目,完全基于 Java 实现。该项目于 2009 年被 Terracotta 收购,目前是 Terracotta 旗下的一个项目。读者可以到 http://www.quartz-scheduler.org/站点下载 Quartz 的发布版本及其源代码。
1.2 Quartz特点
作为一个优秀的开源调度框架,Quartz 具有以下特点:
- 强大的调度功能,例如支持丰富多样的调度方法,可以满足各种常规及特殊需求;
- 灵活的应用方式,例如支持任务和调度的多种组合方式,支持调度数据的多种存储方式;
- 分布式和集群能力,Terracotta 收购后在原来功能基础上作了进一步提升。
另外,作为 Spring 默认的调度框架,Quartz 很容易与 Spring 集成实现灵活可配置的调度功能。
quartz调度核心元素:
- Scheduler:任务调度器,是实际执行任务调度的控制器。在spring中通过SchedulerFactoryBean封装起来。
- Trigger:触发器,用于定义任务调度的时间规则,有SimpleTrigger,CronTrigger,DateIntervalTrigger和NthIncludedDayTrigger,其中CronTrigger用的比较多,本文主要介绍这种方式。CronTrigger在spring中封装在CronTriggerFactoryBean中。
- Calendar:它是一些日历特定时间点的集合。一个trigger可以包含多个Calendar,以便排除或包含某些时间点。
- JobDetail:用来描述Job实现类及其它相关的静态信息,如Job名字、关联监听器等信息。在spring中有JobDetailFactoryBean和 MethodInvokingJobDetailFactoryBean两种实现,如果任务调度只需要执行某个类的某个方法,就可以通过MethodInvokingJobDetailFactoryBean来调用。
- Job:是一个接口,只有一个方法void execute(JobExecutionContext context),开发者实现该接口定义运行任务,JobExecutionContext类提供了调度上下文的各种信息。Job运行时的信息保存在JobDataMap实例中。实现Job接口的任务,默认是无状态的,若要将Job设置成有状态的,在quartz中是给实现的Job添加@DisallowConcurrentExecution注解(以前是实现StatefulJob接口,现在已被Deprecated),在与spring结合中可以在spring配置文件的job detail中配置concurrent参数。
1.3 Quartz 集群配置
quartz集群是通过数据库表来感知其他的应用的,各个节点之间并没有直接的通信。只有使用持久的JobStore才能完成Quartz集群。
数据库表:以前有12张表,现在只有11张表,现在没有存储listener相关的表,多了QRTZ_SIMPROP_TRIGGERS表:
Table name | Description |
---|---|
QRTZ_CALENDARS | 存储Quartz的Calendar信息 |
QRTZ_CRON_TRIGGERS | 存储CronTrigger,包括Cron表达式和时区信息 |
QRTZ_FIRED_TRIGGERS | 存储与已触发的Trigger相关的状态信息,以及相联Job的执行信息 |
QRTZ_PAUSED_TRIGGER_GRPS | 存储已暂停的Trigger组的信息 |
QRTZ_SCHEDULER_STATE | 存储少量的有关Scheduler的状态信息,和别的Scheduler实例 |
QRTZ_LOCKS | 存储程序的悲观锁的信息 |
QRTZ_JOB_DETAILS | 存储每一个已配置的Job的详细信息 |
QRTZ_SIMPLE_TRIGGERS | 存储简单的Trigger,包括重复次数、间隔、以及已触的次数 |
QRTZ_BLOG_TRIGGERS | Trigger作为Blob类型存储 |
QRTZ_TRIGGERS | 存储已配置的Trigger的信息 |
QRTZ_SIMPROP_TRIGGERS |
QRTZ_LOCKS就是Quartz集群实现同步机制的行锁表,包括以下几个锁:CALENDAR_ACCESS 、JOB_ACCESS、MISFIRE_ACCESS 、STATE_ACCESS 、TRIGGER_ACCESS。
二、Quartz 原理及流程
2.1 quartz基本原理
核心元素
Quartz 任务调度的核心元素是 scheduler, trigger 和 job,其中 trigger 和 job 是任务调度的元数据, scheduler 是实际执行调度的控制器。
在 Quartz 中,trigger 是用于定义调度时间的元素,即按照什么时间规则去执行任务。Quartz 中主要提供了四种类型的 trigger:SimpleTrigger,CronTirgger,DateIntervalTrigger,和 NthIncludedDayTrigger。这四种 trigger 可以满足企业应用中的绝大部分需求。我们将在企业应用一节中进一步讨论四种 trigger 的功能。
在 Quartz 中,job 用于表示被调度的任务。主要有两种类型的 job:无状态的(stateless)和有状态的(stateful)。对于同一个 trigger 来说,有状态的 job 不能被并行执行,只有上一次触发的任务被执行完之后,才能触发下一次执行。Job 主要有两种属性:volatility 和 durability,其中 volatility 表示任务是否被持久化到数据库存储,而 durability 表示在没有 trigger 关联的时候任务是否被保留。两者都是在值为 true 的时候任务被持久化或保留。一个 job 可以被多个 trigger 关联,但是一个 trigger 只能关联一个 job。
在 Quartz 中, scheduler 由 scheduler 工厂创建:DirectSchedulerFactory 或者 StdSchedulerFactory。 第二种工厂 StdSchedulerFactory 使用较多,因为 DirectSchedulerFactory 使用起来不够方便,需要作许多详细的手工编码设置。 Scheduler 主要有三种:RemoteMBeanScheduler, RemoteScheduler 和 StdScheduler。本文以最常用的 StdScheduler 为例讲解。这也是笔者在项目中所使用的 scheduler 类。
Quartz 核心元素之间的关系如下图所示:
图 1. Quartz 核心元素关系图
线程视图
在 Quartz 中,有两类线程,Scheduler 调度线程和任务执行线程,其中任务执行线程通常使用一个线程池维护一组线程。
图 2. Quartz 线程视图
Scheduler 调度线程主要有两个: 执行常规调度的线程,和执行 misfired trigger 的线程。常规调度线程轮询存储的所有 trigger,如果有需要触发的 trigger,即到达了下一次触发的时间,则从任务执行线程池获取一个空闲线程,执行与该 trigger 关联的任务。Misfire 线程是扫描所有的 trigger,查看是否有 misfired trigger,如果有的话根据 misfire 的策略分别处理。下图描述了这两个线程的基本流程:
图 3. Quartz 调度线程流程图
关于 misfired trigger,我们在企业应用一节中将进一步描述。
数据存储
Quartz 中的 trigger 和 job 需要存储下来才能被使用。Quartz 中有两种存储方式:RAMJobStore, JobStoreSupport,其中 RAMJobStore 是将 trigger 和 job 存储在内存中,而 JobStoreSupport 是基于 jdbc 将 trigger 和 job 存储到数据库中。RAMJobStore 的存取速度非常快,但是由于其在系统被停止后所有的数据都会丢失,所以在通常应用中,都是使用 JobStoreSupport。
在 Quartz 中,JobStoreSupport 使用一个驱动代理来操作 trigger 和 job 的数据存储:StdJDBCDelegate。StdJDBCDelegate 实现了大部分基于标准 JDBC 的功能接口,但是对于各种数据库来说,需要根据其具体实现的特点做某些特殊处理,因此各种数据库需要扩展 StdJDBCDelegate 以实现这些特殊处理。Quartz 已经自带了一些数据库的扩展实现,可以直接使用,如下图所示:
图 4. Quartz 数据库驱动代理
作为嵌入式数据库的代表,Derby 近来非常流行。如果使用 Derby 数据库,可以使用上图中的 CloudscapeDelegate 作为 trigger 和 job 数据存储的代理类。
2.2 quartz启动流程
若quartz是配置在spring中,当服务器启动时,就会装载相关的bean。SchedulerFactoryBean实现了InitializingBean接口,因此在初始化bean的时候,会执行afterPropertiesSet方法,该方法将会调用SchedulerFactory(DirectSchedulerFactory 或者 StdSchedulerFactory,通常用StdSchedulerFactory)创建Scheduler。SchedulerFactory在创建quartzScheduler的过程中,将会读取配置参数,初始化各个组件,关键组件如下:
-
ThreadPool:一般是使用SimpleThreadPool,SimpleThreadPool创建了一定数量的WorkerThread实例来使得Job能够在线程中进行处理。WorkerThread是定义在SimpleThreadPool类中的内部类,它实质上就是一个线程。在SimpleThreadPool中有三个list:workers-存放池中所有的线程引用,availWorkers-存放所有空闲的线程,busyWorkers-存放所有工作中的线程;
线程池的配置参数如下所示:1 2 3
org.quartz.threadPool.class=org.quartz.simpl.SimpleThreadPool org.quartz.threadPool.threadCount=3 org.quartz.threadPool.threadPriority=5
-
JobStore:分为存储在内存的RAMJobStore和存储在数据库的JobStoreSupport(包括JobStoreTX和JobStoreCMT两种实现,JobStoreCMT是依赖于容器来进行事务的管理,而JobStoreTX是自己管理事务),若要使用集群要使用JobStoreSupport的方式;
- QuartzSchedulerThread:用来进行任务调度的线程,在初始化的时候paused=true,halted=false,虽然线程开始运行了,但是paused=true,线程会一直等待,直到start方法将paused置为false;
另外,SchedulerFactoryBean还实现了SmartLifeCycle接口,因此初始化完成后,会执行start()方法,该方法将主要会执行以下的几个动作:
- 创建ClusterManager线程并启动线程:该线程用来进行集群故障检测和处理,将在下文详细讨论;
- 创建MisfireHandler线程并启动线程:该线程用来进行misfire任务的处理,将在下文详细讨论;
- 置QuartzSchedulerThread的paused=false,调度线程才真正开始调度;
整个启动流程如下图:
三、Spring + Quartz 实现企业级调度的实现示例
3.1 环境信息
此示例中的环境: Spring 4.1.6.RELEASE + quartz 2.2.1 + Mysql 5.6
3.2 相关代码及配置
3.2.1 Maven 引入
3.2.2 数据库脚本准备
SET FOREIGN_KEY_CHECKS=0;
– —————————-
– Table structure for task_schedule_job
– —————————-
DROP TABLE IF EXISTS `task_schedule_job`;
CREATE TABLE `task_schedule_job` (
`job_id` bigint(20) NOT NULL AUTO_INCREMENT,
`create_time` timestamp NULL DEFAULT NULL,
`update_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`job_name` varchar(255) DEFAULT NULL,
`job_group` varchar(255) DEFAULT NULL,
`job_status` varchar(255) DEFAULT NULL,
`cron_expression` varchar(255) NOT NULL,
`description` varchar(255) DEFAULT NULL,
`bean_class` varchar(255) DEFAULT NULL,
`is_concurrent` varchar(255) DEFAULT NULL COMMENT ‘1’,
`spring_id` varchar(255) DEFAULT NULL,
`method_name` varchar(255) NOT NULL
PRIMARY KEY (`job_id`),
UNIQUE KEY `name_group` (`job_name`,`job_group`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
在Quartz包下docs/dbTables,选择对应的数据库脚本,创建相应的数据库表即可,我用的是mysql5.6,这里有一个需要注意的地方,mysql5.5之前用的表存储引擎是MyISAM,使用的是表级锁,锁发生冲突的概率比较高,并发度低;5.6之后默认的存储引擎为InnoDB,InnoDB采用的锁机制是行级锁,并发度也较高。而quartz集群使用数据库锁的
机制来来实现同一个任务在同一个时刻只被实例执行,所以为了防止冲突,我们建表的时候要选取InnoDB作为表的存
储引擎。如下:
3.2.3 关键代码及配置
<1>spring-quartz.xml 配置 在application.xml 文件中引入
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd"><!-- 注册本地调度任务<bean id="localQuartzScheduler" class="org.springframework.scheduling.quartz.SchedulerFactoryBean"></bean>--><!-- 注册集群调度任务 --><bean id="schedulerFactoryBean" lazy-init="false" autowire="no"class="org.springframework.scheduling.quartz.SchedulerFactoryBean" destroy-method="destroy"><!--可选,QuartzScheduler 启动时更新己存在的Job,这样就不用每次修改targetObject后删除qrtz_job_details表对应记录了 --><property name="overwriteExistingJobs" value="true" /><!--必须的,QuartzScheduler 延时启动,应用启动完后 QuartzScheduler 再启动 --><property name="startupDelay" value="3" /><!-- 设置自动启动 --><property name="autoStartup" value="true" /><property name="applicationContextSchedulerContextKey" value="applicationContext" /><property name="configLocation" value="classpath:quartz.properties" /></bean></beans>
<2>quartz.properties 文件配置
#==============================================================
#Configure Main Scheduler Properties
#==============================================================
org.quartz.scheduler.instanceName = KuanrfGSQuartzScheduler
org.quartz.scheduler.instanceId = AUTO#==============================================================
#Configure JobStore
#==============================================================
org.quartz.jobStore.class = org.quartz.impl.jdbcjobstore.JobStoreTX
org.quartz.jobStore.driverDelegateClass = org.quartz.impl.jdbcjobstore.StdJDBCDelegate
org.quartz.jobStore.tablePrefix = QRTZ_
org.quartz.jobStore.isClustered = true
org.quartz.jobStore.clusterCheckinInterval = 20000
org.quartz.jobStore.dataSource = myDS
org.quartz.jobStore.maxMisfiresToHandleAtATime = 1
org.quartz.jobStore.misfireThreshold = 120000
org.quartz.jobStore.txIsolationLevelSerializable = false#==============================================================
#Configure DataSource
#==============================================================
org.quartz.dataSource.myDS.driver = com.mysql.jdbc.Driver
org.quartz.dataSource.myDS.URL = 你的数据链接
org.quartz.dataSource.myDS.user = 用户名
org.quartz.dataSource.myDS.password = 密码
org.quartz.dataSource.myDS.maxConnections = 30
org.quartz.jobStore.selectWithLockSQL = SELECT * FROM {0}LOCKS WHERE LOCK_NAME = ? FOR UPDATE#==============================================================
#Configure ThreadPool
#==============================================================
org.quartz.threadPool.class= org.quartz.simpl.SimpleThreadPool
org.quartz.threadPool.threadCount= 10
org.quartz.threadPool.threadPriority= 5
org.quartz.threadPool.threadsInheritContextClassLoaderOfInitializingThread= true#==============================================================
#Skip Check Update
#update:true
#not update:false
#==============================================================
org.quartz.scheduler.skipUpdateCheck = true#============================================================================
# Configure Plugins
#============================================================================
org.quartz.plugin.triggHistory.class = org.quartz.plugins.history.LoggingJobHistoryPlugin
org.quartz.plugin.shutdownhook.class = org.quartz.plugins.management.ShutdownHookPlugin
org.quartz.plugin.shutdownhook.cleanShutdown = true
<3>关键代码
package com.netease.ad.omp.service.sys;import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Set;import javax.annotation.PostConstruct;
import javax.annotation.Resource;import com.netease.ad.omp.common.utils.SpringUtils;
import com.netease.ad.omp.dao.sys.mapper.ScheduleJobMapper;
import com.netease.ad.omp.entity.sys.ScheduleJob;
import com.netease.ad.omp.quartz.job.JobUtils;
import com.netease.ad.omp.quartz.job.MyDetailQuartzJobBean;
import com.netease.ad.omp.quartz.job.QuartzJobFactory;
import com.netease.ad.omp.quartz.job.QuartzJobFactoryDisallowConcurrentExecution;
import org.apache.log4j.Logger;
import org.quartz.CronScheduleBuilder;
import org.quartz.CronTrigger;
import org.quartz.JobBuilder;
import org.quartz.JobDetail;
import org.quartz.JobExecutionContext;
import org.quartz.JobKey;
import org.quartz.Scheduler;
import org.quartz.SchedulerException;
import org.quartz.Trigger;
import org.quartz.TriggerBuilder;
import org.quartz.TriggerKey;
import org.quartz.impl.matchers.GroupMatcher;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.quartz.SchedulerFactoryBean;
import org.springframework.stereotype.Service;/*** 计划任务管理*/
@Service
public class JobTaskService {public final Logger log = Logger.getLogger(this.getClass());@Autowiredprivate SchedulerFactoryBean schedulerFactoryBean;@Autowiredprivate ScheduleJobMapper scheduleJobMapper;/*** 从数据库中取 区别于getAllJob* * @return*/public List<ScheduleJob> getAllTask() {return scheduleJobMapper.select(null);}/*** 添加到数据库中 区别于addJob*/public void addTask(ScheduleJob job) {job.setCreateTime(new Date());scheduleJobMapper.insertSelective(job);}/*** 从数据库中查询job*/public ScheduleJob getTaskById(Long jobId) {return scheduleJobMapper.selectByPrimaryKey(jobId);}/*** 更改任务状态* * @throws SchedulerException*/public void changeStatus(Long jobId, String cmd) throws SchedulerException {ScheduleJob job = getTaskById(jobId);if (job == null) {return;}if ("stop".equals(cmd)) {deleteJob(job);job.setJobStatus(JobUtils.STATUS_NOT_RUNNING);} else if ("start".equals(cmd)) {job.setJobStatus(JobUtils.STATUS_RUNNING);addJob(job);}scheduleJobMapper.updateByPrimaryKeySelective(job);}/*** 更改任务 cron表达式* * @throws SchedulerException*/public void updateCron(Long jobId, String cron) throws SchedulerException {ScheduleJob job = getTaskById(jobId);if (job == null) {return;}job.setCronExpression(cron);if (JobUtils.STATUS_RUNNING.equals(job.getJobStatus())) {updateJobCron(job);}scheduleJobMapper.updateByPrimaryKeySelective(job);}/*** 添加任务* * @throws SchedulerException*/public void addJob(ScheduleJob job) throws SchedulerException {if (job == null || !JobUtils.STATUS_RUNNING.equals(job.getJobStatus())) {return;}Scheduler scheduler = schedulerFactoryBean.getScheduler();log.debug(scheduler + ".......................................................................................add");TriggerKey triggerKey = TriggerKey.triggerKey(job.getJobName(), job.getJobGroup());CronTrigger trigger = (CronTrigger) scheduler.getTrigger(triggerKey);// 不存在,创建一个if (null == trigger) {Class clazz = JobUtils.CONCURRENT_IS.equals(job.getIsConcurrent()) ? QuartzJobFactory.class : QuartzJobFactoryDisallowConcurrentExecution.class;JobDetail jobDetail = JobBuilder.newJob(clazz).withIdentity(job.getJobName(), job.getJobGroup()).build();jobDetail.getJobDataMap().put("scheduleJob", job);CronScheduleBuilder scheduleBuilder = CronScheduleBuilder.cronSchedule(job.getCronExpression());trigger = TriggerBuilder.newTrigger().withIdentity(job.getJobName(), job.getJobGroup()).withSchedule(scheduleBuilder).build();scheduler.scheduleJob(jobDetail, trigger);} else {// Trigger已存在,那么更新相应的定时设置CronScheduleBuilder scheduleBuilder = CronScheduleBuilder.cronSchedule(job.getCronExpression());// 按新的cronExpression表达式重新构建triggertrigger = trigger.getTriggerBuilder().withIdentity(triggerKey).withSchedule(scheduleBuilder).build();// 按新的trigger重新设置job执行scheduler.rescheduleJob(triggerKey, trigger);}}@PostConstructpublic void init() throws Exception {// 这里获取任务信息数据List<ScheduleJob> jobList = scheduleJobMapper.select(null);for (ScheduleJob job : jobList) {addJob(job);}}/*** 获取所有计划中的任务列表* * @return* @throws SchedulerException*/public List<ScheduleJob> getAllJob() throws SchedulerException {Scheduler scheduler = schedulerFactoryBean.getScheduler();GroupMatcher<JobKey> matcher = GroupMatcher.anyJobGroup();Set<JobKey> jobKeys = scheduler.getJobKeys(matcher);List<ScheduleJob> jobList = new ArrayList<ScheduleJob>();for (JobKey jobKey : jobKeys) {List<? extends Trigger> triggers = scheduler.getTriggersOfJob(jobKey);for (Trigger trigger : triggers) {ScheduleJob job = new ScheduleJob();job.setJobName(jobKey.getName());job.setJobGroup(jobKey.getGroup());job.setDescription("触发器:" + trigger.getKey());Trigger.TriggerState triggerState = scheduler.getTriggerState(trigger.getKey());job.setJobStatus(triggerState.name());if (trigger instanceof CronTrigger) {CronTrigger cronTrigger = (CronTrigger) trigger;String cronExpression = cronTrigger.getCronExpression();job.setCronExpression(cronExpression);}jobList.add(job);}}return jobList;}/*** 所有正在运行的job* * @return* @throws SchedulerException*/public List<ScheduleJob> getRunningJob() throws SchedulerException {Scheduler scheduler = schedulerFactoryBean.getScheduler();List<JobExecutionContext> executingJobs = scheduler.getCurrentlyExecutingJobs();List<ScheduleJob> jobList = new ArrayList<ScheduleJob>(executingJobs.size());for (JobExecutionContext executingJob : executingJobs) {ScheduleJob job = new ScheduleJob();JobDetail jobDetail = executingJob.getJobDetail();JobKey jobKey = jobDetail.getKey();Trigger trigger = executingJob.getTrigger();job.setJobName(jobKey.getName());job.setJobGroup(jobKey.getGroup());job.setDescription("触发器:" + trigger.getKey());Trigger.TriggerState triggerState = scheduler.getTriggerState(trigger.getKey());job.setJobStatus(triggerState.name());if (trigger instanceof CronTrigger) {CronTrigger cronTrigger = (CronTrigger) trigger;String cronExpression = cronTrigger.getCronExpression();job.setCronExpression(cronExpression);}jobList.add(job);}return jobList;}/*** 暂停一个job* * @param scheduleJob* @throws SchedulerException*/public void pauseJob(ScheduleJob scheduleJob) throws SchedulerException {Scheduler scheduler = schedulerFactoryBean.getScheduler();JobKey jobKey = JobKey.jobKey(scheduleJob.getJobName(), scheduleJob.getJobGroup());scheduler.pauseJob(jobKey);}/*** 恢复一个job* * @param scheduleJob* @throws SchedulerException*/public void resumeJob(ScheduleJob scheduleJob) throws SchedulerException {Scheduler scheduler = schedulerFactoryBean.getScheduler();JobKey jobKey = JobKey.jobKey(scheduleJob.getJobName(), scheduleJob.getJobGroup());scheduler.resumeJob(jobKey);}/*** 删除一个job* * @param scheduleJob* @throws SchedulerException*/public void deleteJob(ScheduleJob scheduleJob) throws SchedulerException {Scheduler scheduler = schedulerFactoryBean.getScheduler();JobKey jobKey = JobKey.jobKey(scheduleJob.getJobName(), scheduleJob.getJobGroup());scheduler.deleteJob(jobKey);}/*** 立即执行job* * @param scheduleJob* @throws SchedulerException*/public void runAJobNow(ScheduleJob scheduleJob) throws SchedulerException {Scheduler scheduler = schedulerFactoryBean.getScheduler();JobKey jobKey = JobKey.jobKey(scheduleJob.getJobName(), scheduleJob.getJobGroup());scheduler.triggerJob(jobKey);}/*** 更新job时间表达式* * @param scheduleJob* @throws SchedulerException*/public void updateJobCron(ScheduleJob scheduleJob) throws SchedulerException {Scheduler scheduler = schedulerFactoryBean.getScheduler();TriggerKey triggerKey = TriggerKey.triggerKey(scheduleJob.getJobName(), scheduleJob.getJobGroup());CronTrigger trigger = (CronTrigger) scheduler.getTrigger(triggerKey);CronScheduleBuilder scheduleBuilder = CronScheduleBuilder.cronSchedule(scheduleJob.getCronExpression());trigger = trigger.getTriggerBuilder().withIdentity(triggerKey).withSchedule(scheduleBuilder).build();scheduler.rescheduleJob(triggerKey, trigger);}public static void main(String[] args) {CronScheduleBuilder scheduleBuilder = CronScheduleBuilder.cronSchedule("xxxxx");}
}
package com.netease.ad.omp.quartz.job;import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;import com.netease.ad.omp.common.utils.SpringUtils;
import com.netease.ad.omp.entity.sys.ScheduleJob;
import org.apache.commons.lang3.StringUtils;
import org.apache.log4j.Logger;
import org.quartz.JobExecutionContext;
import org.springframework.context.ApplicationContext;/*** Created with IntelliJ IDEA* ProjectName: omp* Author: bjsonghongxu* CreateTime : 15:58* Email: bjsonghongxu@crop.netease.com* Class Description:* 定时任务工具类*/
public class JobUtils {public final static Logger log = Logger.getLogger(JobUtils.class);public static final String STATUS_RUNNING = "1"; //启动状态public static final String STATUS_NOT_RUNNING = "0"; //未启动状态public static final String CONCURRENT_IS = "1";public static final String CONCURRENT_NOT = "0";private ApplicationContext ctx;/*** 通过反射调用scheduleJob中定义的方法** @param scheduleJob*/public static void invokMethod(ScheduleJob scheduleJob,JobExecutionContext context) {Object object = null;Class clazz = null;if (StringUtils.isNotBlank(scheduleJob.getSpringId())) {object = SpringUtils.getBean(scheduleJob.getSpringId());} else if (StringUtils.isNotBlank(scheduleJob.getBeanClass())) {try {clazz = Class.forName(scheduleJob.getBeanClass());object = clazz.newInstance();} catch (Exception e) {// TODO Auto-generated catch blocke.printStackTrace();}}if (object == null) {log.error("任务名称 = [" + scheduleJob.getJobName() + "]---------------未启动成功,请检查是否配置正确!!!");return;}clazz = object.getClass();Method method = null;try {method = clazz.getMethod(scheduleJob.getMethodName(), new Class[] {JobExecutionContext.class});} catch (NoSuchMethodException e) {log.error("任务名称 = [" + scheduleJob.getJobName() + "]---------------未启动成功,方法名设置错误!!!");} catch (SecurityException e) {// TODO Auto-generated catch blocke.printStackTrace();}if (method != null) {try {method.invoke(object, new Object[] {context});} catch (IllegalAccessException e) {// TODO Auto-generated catch blocke.printStackTrace();} catch (IllegalArgumentException e) {// TODO Auto-generated catch blocke.printStackTrace();} catch (InvocationTargetException e) {// TODO Auto-generated catch blocke.printStackTrace();}}log.info("任务名称 = [" + scheduleJob.getJobName() + "]----------启动成功");}
}
package com.netease.ad.omp.quartz.job;import com.netease.ad.omp.entity.sys.ScheduleJob;
import org.apache.log4j.Logger;
import org.quartz.Job;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;/*** * @Description: 计划任务执行处 无状态* Spring调度任务 (重写 quartz 的 QuartzJobBean 类原因是在使用 quartz+spring 把 quartz 的 task 实例化进入数据库时,会产生: serializable 的错误)*/
public class QuartzJobFactory implements Job {public final Logger log = Logger.getLogger(this.getClass());@Overridepublic void execute(JobExecutionContext context) throws JobExecutionException {ScheduleJob scheduleJob = (ScheduleJob) context.getMergedJobDataMap().get("scheduleJob");JobUtils.invokMethod(scheduleJob,context);}
}
package com.netease.ad.omp.quartz.job;import com.netease.ad.omp.entity.sys.ScheduleJob;
import org.apache.log4j.Logger;
import org.quartz.DisallowConcurrentExecution;
import org.quartz.Job;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;/*** * @Description: 若一个方法一次执行不完下次轮转时则等待该方法执行完后才执行下一次操作* Spring调度任务 (重写 quartz 的 QuartzJobBean 类原因是在使用 quartz+spring 把 quartz 的 task 实例化进入数据库时,会产生: serializable 的错误)*/
@DisallowConcurrentExecution
public class QuartzJobFactoryDisallowConcurrentExecution implements Job {public final Logger log = Logger.getLogger(this.getClass());@Overridepublic void execute(JobExecutionContext context) throws JobExecutionException {ScheduleJob scheduleJob = (ScheduleJob) context.getMergedJobDataMap().get("scheduleJob");JobUtils.invokMethod(scheduleJob,context);}
}
package com.netease.ad.omp.entity.sys;import javax.persistence.Id;
import javax.persistence.Table;
import java.io.Serializable;
import java.util.Date;/*** Created with IntelliJ IDEA* ProjectName: omp* Author: bjsonghongxu* CreateTime : 15:48* Email: bjsonghongxu@crop.netease.com* Class Description:* 计划任务信息*/
@Table(name = "task_schedule_job")
public class ScheduleJob implements Serializable {@Idprivate Long jobId;private Date createTime;private Date updateTime;/*** 任务名称*/private String jobName;/*** 任务分组*/private String jobGroup;/*** 任务状态 是否启动任务*/private String jobStatus;/*** cron表达式*/private String cronExpression;/*** 描述*/private String description;/*** 任务执行时调用哪个类的方法 包名+类名*/private String beanClass;/*** 任务是否有状态*/private String isConcurrent;/*** spring bean*/private String springId;/*** 任务调用的方法名*/private String methodName;public Long getJobId() {return jobId;}public void setJobId(Long jobId) {this.jobId = jobId;}public Date getCreateTime() {return createTime;}public void setCreateTime(Date createTime) {this.createTime = createTime;}public Date getUpdateTime() {return updateTime;}public void setUpdateTime(Date updateTime) {this.updateTime = updateTime;}public String getJobName() {return jobName;}public void setJobName(String jobName) {this.jobName = jobName;}public String getJobGroup() {return jobGroup;}public void setJobGroup(String jobGroup) {this.jobGroup = jobGroup;}public String getJobStatus() {return jobStatus;}public void setJobStatus(String jobStatus) {this.jobStatus = jobStatus;}public String getCronExpression() {return cronExpression;}public void setCronExpression(String cronExpression) {this.cronExpression = cronExpression;}public String getDescription() {return description;}public void setDescription(String description) {this.description = description;}public String getBeanClass() {return beanClass;}public void setBeanClass(String beanClass) {this.beanClass = beanClass;}public String getIsConcurrent() {return isConcurrent;}public void setIsConcurrent(String isConcurrent) {this.isConcurrent = isConcurrent;}public String getSpringId() {return springId;}public void setSpringId(String springId) {this.springId = springId;}public String getMethodName() {return methodName;}public void setMethodName(String methodName) {this.methodName = methodName;}
}
package com.netease.ad.omp.common.utils;import org.springframework.beans.BeansException;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;public final class SpringUtils implements BeanFactoryPostProcessor {private static ConfigurableListableBeanFactory beanFactory; // Spring应用上下文环境@Overridepublic void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {SpringUtils.beanFactory = beanFactory;}/*** 获取对象* * @param name* @return Object 一个以所给名字注册的bean的实例* @throws BeansException* */@SuppressWarnings("unchecked")public static <T> T getBean(String name) throws BeansException {return (T) beanFactory.getBean(name);}/*** 获取类型为requiredType的对象* * @param clz* @return* @throws BeansException* */public static <T> T getBean(Class<T> clz) throws BeansException {@SuppressWarnings("unchecked")T result = (T) beanFactory.getBean(clz);return result;}/*** 如果BeanFactory包含一个与所给名称匹配的bean定义,则返回true* * @param name* @return boolean*/public static boolean containsBean(String name) {return beanFactory.containsBean(name);}/*** 判断以给定名字注册的bean定义是一个singleton还是一个prototype。* 如果与给定名字相应的bean定义没有被找到,将会抛出一个异常(NoSuchBeanDefinitionException)* * @param name* @return boolean* @throws NoSuchBeanDefinitionException* */public static boolean isSingleton(String name) throws NoSuchBeanDefinitionException {return beanFactory.isSingleton(name);}/*** @param name* @return Class 注册对象的类型* @throws NoSuchBeanDefinitionException* */public static Class<?> getType(String name) throws NoSuchBeanDefinitionException {return beanFactory.getType(name);}/*** 如果给定的bean名字在bean定义中有别名,则返回这些别名* * @param name* @return* @throws NoSuchBeanDefinitionException* */public static String[] getAliases(String name) throws NoSuchBeanDefinitionException {return beanFactory.getAliases(name);}}
至于前端自己画个简单的界面即可使用了。
四、问题及解决方案
4.1quartz mysql 死锁问题
quartz文档提到,如果在集群环境下,最好将配置项org.quartz.jobStore.txIsolationLevelSerializable设置为true
问题:
这个选项在mysql下会非常容易出现死锁问题。
2014-12-29 09:55:28.006 [QuartzScheduler_clusterQuartzSchedular-BJ-YQ-64.2491419487774923_ClusterManager] ERROR o.q.impl.jdbcjobstore.JobStoreTX [U][] - ClusterManager: Error managing cluster: Failure updating scheduler state when checking-in: Deadlock found when trying to get lock; try restarting transaction
这个选项存在意义:
quartz需要提升隔离级别来保障自己的运作,不过,由于各数据库实现的隔离级别定义都不一样,所以quartz提供一个设置序列化这样的隔离级别存在,因为例如oracle中是没有未提交读和可重复读这样的隔离级别存在。但是由于mysql默认的是可重复读,比提交读高了一个级别,所以已经可以满足quartz集群的正常运行。
五、相关知识
5.1、QuartzSchedulerThread线程
线程的主要逻辑代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | while (!halted.get()) {int availThreadCount = qsRsrcs.getThreadPool().blockForAvailableThreads();triggers = qsRsrcs.getJobStore().acquireNextTriggers(now + idleWaitTime, Math.min(availThreadCount, qsRsrcs.getMaxBatchSize()),qsRsrcs.getBatchTimeWindow());long triggerTime = triggers.get(0).getNextFireTime().getTime();long timeUntilTrigger = triggerTime - now;while(timeUntilTrigger > 2) {now = System.currentTimeMillis();timeUntilTrigger = triggerTime - now;}List<TriggerFiredResult> bndle = qsRsrcs.getJobStore().triggersFired(triggers);for(int i = 0;i < res.size();i++){JobRunShell shell = qsRsrcs.getJobRunShellFactory().createJobRunShell(bndle);shell.initialize(qs);qsRsrcs.getThreadPool().runInThread(shell);}
} |
- 先获取线程池中的可用线程数量(若没有可用的会阻塞,直到有可用的);
- 获取30m内要执行的trigger(即acquireNextTriggers):
获取trigger的锁,通过select …for update方式实现;获取30m内(可配置)要执行的triggers(需要保证集群节点的时间一致),若@ConcurrentExectionDisallowed且列表存在该条trigger则跳过,否则更新trigger状态为ACQUIRED(刚开始为WAITING);插入firedTrigger表,状态为ACQUIRED;(注意:在RAMJobStore中,有个timeTriggers,排序方式是按触发时间nextFireTime排的;JobStoreSupport从数据库取出triggers时是按照nextFireTime排序); - 等待直到获取的trigger中最先执行的trigger在2ms内;
- triggersFired:
1)更新firedTrigger的status=EXECUTING;
2)更新trigger下一次触发的时间;
3)更新trigger的状态:无状态的trigger->WAITING,有状态的trigger->BLOCKED,若nextFireTime==null ->COMPLETE;
4) commit connection,释放锁; - 针对每个要执行的trigger,创建JobRunShell,并放入线程池执行:
1)execute:执行job
2)获取TRIGGER_ACCESS锁
3)若是有状态的job:更新trigger状态:BLOCKED->WAITING,PAUSED_BLOCKED->BLOCKED
4)若@PersistJobDataAfterExecution,则updateJobData
5)删除firedTrigger
6)commit connection,释放锁
线程执行流程如下图所示:
QuartzSchedulerThread时序图
任务调度执行过程中,trigger的状态变化如下图所示:
该图来自参考文献5
5.2.misfireHandler线程
下面这些原因可能造成 misfired job:
- 系统因为某些原因被重启。在系统关闭到重新启动之间的一段时间里,可能有些任务会被 misfire;
- Trigger 被暂停(suspend)的一段时间里,有些任务可能会被 misfire;
- 线程池中所有线程都被占用,导致任务无法被触发执行,造成 misfire;
- 有状态任务在下次触发时间到达时,上次执行还没有结束;为了处理 misfired job,Quartz 中为 trigger 定义了处理策略,主要有下面两种:MISFIRE_INSTRUCTION_FIRE_ONCE_NOW:针对 misfired job 马上执行一次;MISFIRE_INSTRUCTION_DO_NOTHING:忽略 misfired job,等待下次触发;默认是MISFIRE_INSTRUCTION_SMART_POLICY,该策略在CronTrigger中=MISFIRE_INSTRUCTION_FIRE_ONCE_NOW线程默认1分钟执行一次;在一个事务中,默认一次最多recovery 20个;
执行流程:
- 若配置(默认为true,可配置)成获取锁前先检查是否有需要recovery的trigger,先获取misfireCount;
- 获取TRIGGER_ACCESS锁;
- hasMisfiredTriggersInState:获取misfired的trigger,默认一个事务里只能最大20个misfired trigger(可配置),misfired判断依据:status=waiting,next_fire_time < current_time-misfirethreshold(可配置,默认1min)
- notifyTriggerListenersMisfired
- updateAfterMisfire:获取misfire策略(默认是MISFIRE_INSTRUCTION_SMART_POLICY,该策略在CronTrigger中=MISFIRE_INSTRUCTION_FIRE_ONCE_NOW),根据策略更新nextFireTime;
- 将nextFireTime等更新到trigger表;
- commit connection,释放锁8.如果还有更多的misfired,sleep短暂时间(为了集群负载均衡),否则sleep misfirethreshold时间,后继续轮询;
misfireHandler线程执行流程如下图所示:
misfireHandler线程时序图
5.3.clusterManager线程
初始化:
failedInstance=failed+self+firedTrigger表中的schedulerName在scheduler_state表中找不到的(孤儿)
线程执行:
每个服务器会定时(org.quartz.jobStore.clusterCheckinInterval这个时间)更新SCHEDULER_STATE表的LAST_CHECKIN_TIME,若这个字段远远超出了该更新的时间,则认为该服务器实例挂了;
注意:每个服务器实例有唯一的id,若配置为AUTO,则为hostname+current_time
线程执行的具体流程:
- 检查是否有超时的实例failedInstances;
- 更新该服务器实例的LAST_CHECKIN_TIME;
若有超时的实例: - 获取STATE_ACCESS锁;
- 获取超时的实例failedInstances;
- 获取TRIGGER_ACCESS锁;
- clusterRecover:
- 针对每个failedInstances,通过instanceId获取每个实例的firedTriggers;
- 针对每个firedTrigger:
1) 更新trigger状态:
BLOCKED->WAITING
PAUSED_BLOCKED->PAUSED
ACQUIRED->WAITING
2) 若firedTrigger不是ACQUIRED状态(在执行状态),且jobRequestRecovery=true:
创建一个SimpleTrigger,存储到trigger表,status=waiting,MISFIRE_INSTR=MISFIRE_INSTRUCTION_IGNORE_MISFIRE_POLICY.
3) 删除firedTrigger
clusterManager线程执行时序图如下图所示:
5.4源码分析锁
目前代码中行锁只用到了STATE_ACCESS 和TRIGGER_ACCESS 这两种。
1、TRIGGER_ACCESS
先了解一篇文章,通过源码来分析quartz是如何通过加锁来实现集群环境,触发器状态的一致性。
http://www.360doc.com/content/14/0926/08/15077656_412418636.shtml
可以看到触发器的操作主要用主线程StdScheduleThread来完成,不管是获取需要触发的30S内的触发器,还是触发过程。select和update触发器表时
都会先加锁,后解锁。如果数据库资源竞争比较大的话,锁会影响整个性能。可以考虑将任务信息放在分布式内存,如redis上进行处理。数据库只是定时从redis上load数据下来做统计。
实现都在JobStoreSupport类
加锁类型 | 加锁方法 | 底层数据库操作 | 备注 |
executeInNonManagedTXLock | acquireNextTrigger | selectTriggerToAcquire selectTrigger selectJobDetail insertFiredTrigger | 查询需要点火的trigger 选择需要执行的trigger加入到fired_trigger表 |
for执行 triggerFired | selectJobDetail selectCalendar updateFiredTrigger triggerExists updateTrigger | 点火trigger 修改trigger状态为可执行状态。 | |
recoverJobs | updateTriggerStatesFromOtherStates hasMisfiredTriggersInState doUpdateOfMisfiredTrigger selectTriggersForRecoveringJobs selectTriggersInState deleteFiredTriggers | 非集群环境下重新执行 failed与misfired的trigger | |
retryExecuteInNonManagedTXLock | releaseAcquiredTrigger | updateTriggerStateFromOtherState deleteFiredTrigger | 异常情况下重新释放trigger到初使状态。 |
triggeredJobComplete | selectTriggerStatus removeTrigger updateTriggerState deleteFiredTrigger | 触发JOB任务完成后的处理。 | |
obtainLock | recoverMisfiredJobs | hasMisfiredTriggersInState doUpdateOfMisfiredTrigger | 重新执行misfired的trigger 可以在启动时执行,也可以由misfired线程定期执行。 |
clusterRecover | selectInstancesFiredTriggerRecords updateTriggerStatesForJobFromOtherState storeTrigger deleteFiredTriggers selectFiredTriggerRecords removeTrigger deleteSchedulerState | 集群有结点faied,让JOB能重新执行。 | |
executeInLock 数据库集群里等同于 executeInNonManagedTXLock | storeJobAndTrigger | updateJobDetail insertJobDetail triggerExists selectJobDetail updateTrigger insertTrigger | 保存JOB和TRIGGER配置 |
storeJob | 保存JOB | ||
removeJob | 删除JOB | ||
removeJobs | 批量删除JOB | ||
removeTriggers | 批量删除triggers | ||
storeJobsAndTriggers | 保存JOB和多个trigger配置 | ||
removeTrigger | 删除trigger | ||
replaceTrigger | 替换trigger | ||
storeCalendar | 保存定时日期 | ||
removeCalendar | 删除定时日期 | ||
clearAllSchedulingData | 清除所有定时数据 | ||
pauseTrigger | 停止触发器 | ||
pauseJob | 停止任务 | ||
pauseJobs | 批量停止任务 | ||
resumeTrigger | 恢复触发器 | ||
resumeJob | 恢复任务 | ||
resumeJobs | 批量恢复任务 | ||
pauseTriggers | 批量停止触发器 | ||
resumeTriggers | 批量恢复触发器 | ||
pauseAll | 停止所有 | ||
resumeAll | 恢复所有 |
—
2、STATE_TRIGGER
实现都在JobStoreSupport类
加锁类型 | 加锁方法 | 底层数据库操作 | 备注 |
obtainLock | doCheckin | clusterCheckIn | 判断集群状态 先用LOCK_STATE_ACCESS锁集群状态 再用LOCK_TRIGGER_ACCESS恢复集群运行 |
—
六、参考资料
- Quartz Documentation http://quartz-scheduler.org/documentation
- spring javadoc-api http://docs.spring.io/spring/docs/4.3.0.BUILD-SNAPSHOT/javadoc-api/
- 基于Quartz开发企业级任务调度应用 https://www.ibm.com/developerworks/cn/opensource/os-cn-quartz/
- quartz应用与集群原理分析 http://tech.meituan.com/mt-crm-quartz.html
- quartz详解2:quartz由浅入深 http://ecmcug.itpub.net/11627468/viewspace-1763498/
- quartz详解4:quartz线程管理 http://blog.itpub.net/11627468/viewspace-1766967/
- quartz学习笔记 http://www.cnblogs.com/yunxuange/archive/2012/08/28/2660141.html
- quartz集群调度机制调研及源码分析 http://demo.netfoucs.com/gklifg/article/details/27090179
总结
通过这段时间对quartz资料的整理和结合工作中的运用,深入理解了quartz这一优秀的调度框架。在技术这条路上,做技术千万不要浅尝辄止,一定要深入的去理解所用的东西,才会使自己的能力提升,此外,一些系统的知识分析对自己和他人都是十分有益的。
转载:https://my.oschina.net/songhongxu/blog/802574
如若内容造成侵权/违法违规/事实不符,请联系编程学习网邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
相关文章
- Linux crontab定时任务配置方法(详解)
CRONTAB概念/介绍 crontab命令用于设置周期性被执行的指令。该命令从标准输入设备读取指令,并将其存放于“crontab”文件中,以供之后读取和执行。 cron 系统调度进程。 可以使用它在每天的非高峰负荷时间段运行作业,或在一周或一月中的不同时段运行。cron是系统主要的调度进…...
2024/4/20 20:57:06 - DSP入门前的背景知识
数字信号处理(DigitalSignal Processing,简称DSP)是一门涉及许多学科而又广泛应用于许多领域的新兴学科。20世纪60年代以来,随着计算机和信息技术的飞速发展,数字信号处理技术应运而生并得到迅速的发展。在过去的二十多年时间里,数字信号处理已经在通信等领域得到极为广泛…...
2024/4/20 20:57:05 - android安卓源码海量项目合集大全打包6000套-2续
文章太长,接上一篇。│ Android应用源码之HTMLViewer.zip │ Android应用源码之http 演示Demo.zip │ Android应用源码之http.zip │ Android应用源码之http1.zip │ Android应用源码之IAround.zip │ …...
2024/4/23 17:37:30 - 三极管工作原理分析
转载:https://blog.csdn.net/a10615/article/details/51627619随着科学技的发展,电子技术的应用几乎渗透到了人们生产生活的方方面面。晶体三极管作为电子技术中一个最为基本的常用器件,其原理对于学习电子技术的人自然应该是一个重点。三极管原理的关键是要说明以下三点:1、…...
2024/4/21 0:46:32 - Crontab重启和crontab -e位置
1、Crontab重启Linux:service crond restart 或者/etc/init.d/crond restartubuntu: sudo service cron restartMac:sudo /usr/sbin/cron restart2、crontab -e 文件的位置Linux:/var/spool/cron/(用户名)3、Ubuntu开启crontab日志crontab记录日志修改rsyslogsudo vim /etc…...
2024/4/21 0:46:30 - 三极管工作原理图
好东西 三极管工作原理 三极管是电流放大器件,有三个极,分别叫做集电极C,基极B,发射极E。分成NPN和PNP两种。我们仅以NPN三极管的共发射极放大电路为例来说明一下三极管放大电路的基本原理。 一、电流放大 下面的分析仅对于NPN型硅三极管。如上图所示,我们把从基极B流至…...
2024/4/21 0:46:29 - DSP入门必看(下)
文章来自:http://doggo.blog.sohu.com/4401860.htmleXpressDSP是什么? eXpressDSP是一种实时DSP软件技术,它是一种DSP编程的标准,利用它可以加快你开发DSP软件的速度。以往DSP软件的开发没有任何标准,不同的人写的程序一般无法连接在一起。DSP软件的调试工具也非常不方便。…...
2024/4/21 0:46:29 - aix设置crontab 定时任务
执行步骤1) 编辑指定用户下crontab 如果用户为use 则执行crontab -e use按insert 执行编辑2) 新建定时任务例1:每天11点17执行先压缩后删除命令,注意%是特殊字符需要转义一下17 11 * * * tar cvf /weblog10/Applog/test-`date +\%y\%m\%d`.tar /weblog10/test &&am…...
2024/4/21 0:46:28 - 《监控》之偶遇
偶遇 回到现在,在“躲”的这个阶段,更加不会去回忆胡哥这个十多年没有联系的朋友了。 但是,生活就是那么让人无法预料。 已是7月初了,我已经休假在这个北京东北五环附近的小区21天了,这三周其实我的心态也有变化,开始的两周是害怕,缺乏安全感,需要找到安全感,而最近这…...
2024/4/21 0:46:26 - Quartz是什么?
1.背景介绍quartz是什么?quartz是一个由java编写的任务调度库,由OpenSymphony组织开源出来。绝大多数公司都会用到任务调度这个功能, 比如公司需要定期执行任务调度生成报表, 或者比如博客什么的定时更新之类的,都可以靠Quartz来完成。任务调度:现在有N个任务(程序),要…...
2024/4/21 0:46:25 - 三极管工作原理--我见过最通俗讲法
转自:http://blog.csdn.net/xiaowei_001/article/details/4445119三极管工作原理三极管原理对三极管放大作用的理解,切记一点:能量不会无缘无故的产生,所以,三极管一定不会产生能量,。但三极管厉害的地方在于:它可以通过小电流控制大电流放大的原理就在于:通过小的交流…...
2024/4/21 0:46:24 - linux crontab添加log ,及2>&1 添加时间戳
默认情况下,使用 grep CRON /var/log/syslog 可以看到crontab的一些相关log,但是信息并不太多。你能看到crontab执行了哪个命令,但是看不到命令执行的输出信息。 如果希望看到crontab中执行的脚本或命令的输出信息及错误信息可以使用2>&1将这些信息输出到log文件。 首…...
2024/4/21 0:46:24 - 三极管和MOS管工作原理详解
PN结的形成 PN结是三极管以及场效应管中最基本的组成部分,要想彻底搞明白三极管以及场效应管的工作原理,必须先搞清楚PN结形成的原理和工作特性。 本征半导体以及空穴对 本征半导体(intrinsic semiconductor))完全不含杂质且无晶格缺陷的纯净半导体称为本征半导体。主要常见…...
2024/4/21 0:46:22 - DSP入门看
DSP入门必看如何选择外部时钟? DSP的内部指令周期较高,外部晶振的主频不够,因此DSP大多数片内均有PLL。但每个系列不尽相同。 1)TMS320C2000系列: TMS320C20x:PLL可以2,1,2和4,因此外部时钟可以为5MHz-40MHz。 TMS320F240:PLL可以2,1,1.5,2,2.5,3,4,4.5,5…...
2024/4/21 0:46:21 - 程序猿也爱学英语,有图有真相!
一.本文所涉及的内容(Contents) 本文所涉及的内容(Contents)前言(Introduction)发音(Pronunciation)基本常用单词积累(Vocabulary)新概念英语(New Concept English)究竟需不需要学语法(English Grammar?)走遍美国(Family Album U.S.A.)常用英语(Daily Englis…...
2024/4/21 0:46:21 - Crontab 实现指定时间执行一次脚本的两种方法
在工作中,经常会碰到每隔多少天/小时/分钟执行一次脚本,或某个命令的情况。如果是每隔多少小时,多少分运行一次程序,在crontab中可能比较好实现一些,下面是一些示例及crontab的格式说明:示例如下:# 下午6点到早上6点,每隔15分钟执行一次脚本0,15,30,45 18-06 * * * /bin…...
2024/4/21 0:46:20 - DOS攻击与网络溯源技术
1.DoS攻击DoS攻击(Denial of Service,拒绝服务攻击)通过消耗计算机的某种资源,例如计算资源、网络连接等,造成资源耗尽,导致服务端无法为合法用户提供服务或只能提供降级服务。在SDN网络的集中式架构中,控制器是天然的网络中心,负责整个网络的管理和控制工作,很容易成为DoS攻击…...
2024/4/25 13:31:18 - linux应用之crontab定时任务的设置 (简单操作)
前述:linux应用之crontab定时任务的设置实现Linux定时任务有:cron、anacron、at等,这里主要介绍cron服务。名词解释: cron是服务名称,crond是后台进程,crontab则是定制好的计划任务表。1、 crontab命令概念 crontab命令用于设置周期性被执行的指令。该命令从标准输入设…...
2024/4/20 20:57:12 - 三极管工作原理分析,精辟、透彻,看后你就懂(一定要看)
说明:内容与之前那篇一样,由于之前那篇是转载百度的,现在图片受限,无法阅读。这篇自己添加了图片资源。...
2024/4/24 3:48:34 - 如何突破印象关 提升初期留存
关于如何提升游戏的早期留存,可谓是众说纷纭。在众多理论和观点中,史玉柱的”过三关”理论很好的总结了提升早期留存的关键点和策略,今天笔者试着从心理学的角度对史老师三关理论中的印象关做一个比较详细的分析。史老师的印象关强调的是画面的精美、操控的舒适、音效的质量…...
2024/4/20 20:57:11
最新文章
- [C++][算法基础]最大不相交区间数量(贪心 + 区间问题2)
给定 𝑁 个闭区间 [𝑎𝑖,𝑏𝑖],请你在数轴上选择若干区间,使得选中的区间之间互不相交(包括端点)。 输出可选取区间的最大数量。 输入格式 第一行包含整数 ǔ…...
2024/4/30 19:49:14 - 梯度消失和梯度爆炸的一些处理方法
在这里是记录一下梯度消失或梯度爆炸的一些处理技巧。全当学习总结了如有错误还请留言,在此感激不尽。 权重和梯度的更新公式如下: w w − η ⋅ ∇ w w w - \eta \cdot \nabla w ww−η⋅∇w 个人通俗的理解梯度消失就是网络模型在反向求导的时候出…...
2024/3/20 10:50:27 - Redis群集模式和rsync远程同步
一、Redis群集模式 1.1 概念 1.2 作用 1.2.1 Redis集群的数据分片 1.2.2 Redis集群的主从复制模型 1.3 搭建Redis 群集模式 1.3.1 开启群集功能 1.3.2 启动redis节点 1.3.3 启动集群 1.3.4 测试群集 二、rsync远程同步 2.1 概念 2.2 同步方式 2.3 备份的方式 2.4…...
2024/4/30 9:21:09 - 图像处理相关知识 —— 椒盐噪声
椒盐噪声是一种常见的图像噪声类型,它会在图像中随机地添加黑色(椒)和白色(盐)的像素点,使图像的质量降低。这种噪声模拟了在图像传感器中可能遇到的问题,例如损坏的像素或传输过程中的干扰。 椒…...
2024/4/30 2:25:34 - 第十一届蓝桥杯物联网试题(省赛)
对于通信方面,还是终端A、B都保持接收状态,当要发送的数组不为空再发送数据,发送完后立即清除,接收数据的数组不为空则处理,处理完后立即清除,分工明确 继电器不亮一般可能是电压不够 将数据加空格再加\r…...
2024/4/30 1:48:30 - 【外汇早评】美通胀数据走低,美元调整
原标题:【外汇早评】美通胀数据走低,美元调整昨日美国方面公布了新一期的核心PCE物价指数数据,同比增长1.6%,低于前值和预期值的1.7%,距离美联储的通胀目标2%继续走低,通胀压力较低,且此前美国一季度GDP初值中的消费部分下滑明显,因此市场对美联储后续更可能降息的政策…...
2024/4/29 23:16:47 - 【原油贵金属周评】原油多头拥挤,价格调整
原标题:【原油贵金属周评】原油多头拥挤,价格调整本周国际劳动节,我们喜迎四天假期,但是整个金融市场确实流动性充沛,大事频发,各个商品波动剧烈。美国方面,在本周四凌晨公布5月份的利率决议和新闻发布会,维持联邦基金利率在2.25%-2.50%不变,符合市场预期。同时美联储…...
2024/4/30 18:14:14 - 【外汇周评】靓丽非农不及疲软通胀影响
原标题:【外汇周评】靓丽非农不及疲软通胀影响在刚结束的周五,美国方面公布了新一期的非农就业数据,大幅好于前值和预期,新增就业重新回到20万以上。具体数据: 美国4月非农就业人口变动 26.3万人,预期 19万人,前值 19.6万人。 美国4月失业率 3.6%,预期 3.8%,前值 3…...
2024/4/29 2:29:43 - 【原油贵金属早评】库存继续增加,油价收跌
原标题:【原油贵金属早评】库存继续增加,油价收跌周三清晨公布美国当周API原油库存数据,上周原油库存增加281万桶至4.692亿桶,增幅超过预期的74.4万桶。且有消息人士称,沙特阿美据悉将于6月向亚洲炼油厂额外出售更多原油,印度炼油商预计将每日获得至多20万桶的额外原油供…...
2024/4/30 18:21:48 - 【外汇早评】日本央行会议纪要不改日元强势
原标题:【外汇早评】日本央行会议纪要不改日元强势近两日日元大幅走强与近期市场风险情绪上升,避险资金回流日元有关,也与前一段时间的美日贸易谈判给日本缓冲期,日本方面对汇率问题也避免继续贬值有关。虽然今日早间日本央行公布的利率会议纪要仍然是支持宽松政策,但这符…...
2024/4/27 17:58:04 - 【原油贵金属早评】欧佩克稳定市场,填补伊朗问题的影响
原标题:【原油贵金属早评】欧佩克稳定市场,填补伊朗问题的影响近日伊朗局势升温,导致市场担忧影响原油供给,油价试图反弹。此时OPEC表态稳定市场。据消息人士透露,沙特6月石油出口料将低于700万桶/日,沙特已经收到石油消费国提出的6月份扩大出口的“适度要求”,沙特将满…...
2024/4/27 14:22:49 - 【外汇早评】美欲与伊朗重谈协议
原标题:【外汇早评】美欲与伊朗重谈协议美国对伊朗的制裁遭到伊朗的抗议,昨日伊朗方面提出将部分退出伊核协议。而此行为又遭到欧洲方面对伊朗的谴责和警告,伊朗外长昨日回应称,欧洲国家履行它们的义务,伊核协议就能保证存续。据传闻伊朗的导弹已经对准了以色列和美国的航…...
2024/4/28 1:28:33 - 【原油贵金属早评】波动率飙升,市场情绪动荡
原标题:【原油贵金属早评】波动率飙升,市场情绪动荡因中美贸易谈判不安情绪影响,金融市场各资产品种出现明显的波动。随着美国与中方开启第十一轮谈判之际,美国按照既定计划向中国2000亿商品征收25%的关税,市场情绪有所平复,已经开始接受这一事实。虽然波动率-恐慌指数VI…...
2024/4/30 9:43:09 - 【原油贵金属周评】伊朗局势升温,黄金多头跃跃欲试
原标题:【原油贵金属周评】伊朗局势升温,黄金多头跃跃欲试美国和伊朗的局势继续升温,市场风险情绪上升,避险黄金有向上突破阻力的迹象。原油方面稍显平稳,近期美国和OPEC加大供给及市场需求回落的影响,伊朗局势并未推升油价走强。近期中美贸易谈判摩擦再度升级,美国对中…...
2024/4/27 17:59:30 - 【原油贵金属早评】市场情绪继续恶化,黄金上破
原标题:【原油贵金属早评】市场情绪继续恶化,黄金上破周初中国针对于美国加征关税的进行的反制措施引发市场情绪的大幅波动,人民币汇率出现大幅的贬值动能,金融市场受到非常明显的冲击。尤其是波动率起来之后,对于股市的表现尤其不安。隔夜美国股市出现明显的下行走势,这…...
2024/4/25 18:39:16 - 【外汇早评】美伊僵持,风险情绪继续升温
原标题:【外汇早评】美伊僵持,风险情绪继续升温昨日沙特两艘油轮再次发生爆炸事件,导致波斯湾局势进一步恶化,市场担忧美伊可能会出现摩擦生火,避险品种获得支撑,黄金和日元大幅走强。美指受中美贸易问题影响而在低位震荡。继5月12日,四艘商船在阿联酋领海附近的阿曼湾、…...
2024/4/28 1:34:08 - 【原油贵金属早评】贸易冲突导致需求低迷,油价弱势
原标题:【原油贵金属早评】贸易冲突导致需求低迷,油价弱势近日虽然伊朗局势升温,中东地区几起油船被袭击事件影响,但油价并未走高,而是出于调整结构中。由于市场预期局势失控的可能性较低,而中美贸易问题导致的全球经济衰退风险更大,需求会持续低迷,因此油价调整压力较…...
2024/4/26 19:03:37 - 氧生福地 玩美北湖(上)——为时光守候两千年
原标题:氧生福地 玩美北湖(上)——为时光守候两千年一次说走就走的旅行,只有一张高铁票的距离~ 所以,湖南郴州,我来了~ 从广州南站出发,一个半小时就到达郴州西站了。在动车上,同时改票的南风兄和我居然被分到了一个车厢,所以一路非常愉快地聊了过来。 挺好,最起…...
2024/4/29 20:46:55 - 氧生福地 玩美北湖(中)——永春梯田里的美与鲜
原标题:氧生福地 玩美北湖(中)——永春梯田里的美与鲜一觉醒来,因为大家太爱“美”照,在柳毅山庄去寻找龙女而错过了早餐时间。近十点,向导坏坏还是带着饥肠辘辘的我们去吃郴州最富有盛名的“鱼头粉”。说这是“十二分推荐”,到郴州必吃的美食之一。 哇塞!那个味美香甜…...
2024/4/25 18:39:14 - 氧生福地 玩美北湖(下)——奔跑吧骚年!
原标题:氧生福地 玩美北湖(下)——奔跑吧骚年!让我们红尘做伴 活得潇潇洒洒 策马奔腾共享人世繁华 对酒当歌唱出心中喜悦 轰轰烈烈把握青春年华 让我们红尘做伴 活得潇潇洒洒 策马奔腾共享人世繁华 对酒当歌唱出心中喜悦 轰轰烈烈把握青春年华 啊……啊……啊 两…...
2024/4/26 23:04:58 - 扒开伪装医用面膜,翻六倍价格宰客,小姐姐注意了!
原标题:扒开伪装医用面膜,翻六倍价格宰客,小姐姐注意了!扒开伪装医用面膜,翻六倍价格宰客!当行业里的某一品项火爆了,就会有很多商家蹭热度,装逼忽悠,最近火爆朋友圈的医用面膜,被沾上了污点,到底怎么回事呢? “比普通面膜安全、效果好!痘痘、痘印、敏感肌都能用…...
2024/4/27 23:24:42 - 「发现」铁皮石斛仙草之神奇功效用于医用面膜
原标题:「发现」铁皮石斛仙草之神奇功效用于医用面膜丽彦妆铁皮石斛医用面膜|石斛多糖无菌修护补水贴19大优势: 1、铁皮石斛:自唐宋以来,一直被列为皇室贡品,铁皮石斛生于海拔1600米的悬崖峭壁之上,繁殖力差,产量极低,所以古代仅供皇室、贵族享用 2、铁皮石斛自古民间…...
2024/4/28 5:48:52 - 丽彦妆\医用面膜\冷敷贴轻奢医学护肤引导者
原标题:丽彦妆\医用面膜\冷敷贴轻奢医学护肤引导者【公司简介】 广州华彬企业隶属香港华彬集团有限公司,专注美业21年,其旗下品牌: 「圣茵美」私密荷尔蒙抗衰,产后修复 「圣仪轩」私密荷尔蒙抗衰,产后修复 「花茵莳」私密荷尔蒙抗衰,产后修复 「丽彦妆」专注医学护…...
2024/4/30 9:42:22 - 广州械字号面膜生产厂家OEM/ODM4项须知!
原标题:广州械字号面膜生产厂家OEM/ODM4项须知!广州械字号面膜生产厂家OEM/ODM流程及注意事项解读: 械字号医用面膜,其实在我国并没有严格的定义,通常我们说的医美面膜指的应该是一种「医用敷料」,也就是说,医用面膜其实算作「医疗器械」的一种,又称「医用冷敷贴」。 …...
2024/4/30 9:43:22 - 械字号医用眼膜缓解用眼过度到底有无作用?
原标题:械字号医用眼膜缓解用眼过度到底有无作用?医用眼膜/械字号眼膜/医用冷敷眼贴 凝胶层为亲水高分子材料,含70%以上的水分。体表皮肤温度传导到本产品的凝胶层,热量被凝胶内水分子吸收,通过水分的蒸发带走大量的热量,可迅速地降低体表皮肤局部温度,减轻局部皮肤的灼…...
2024/4/30 9:42:49 - 配置失败还原请勿关闭计算机,电脑开机屏幕上面显示,配置失败还原更改 请勿关闭计算机 开不了机 这个问题怎么办...
解析如下:1、长按电脑电源键直至关机,然后再按一次电源健重启电脑,按F8健进入安全模式2、安全模式下进入Windows系统桌面后,按住“winR”打开运行窗口,输入“services.msc”打开服务设置3、在服务界面,选中…...
2022/11/19 21:17:18 - 错误使用 reshape要执行 RESHAPE,请勿更改元素数目。
%读入6幅图像(每一幅图像的大小是564*564) f1 imread(WashingtonDC_Band1_564.tif); subplot(3,2,1),imshow(f1); f2 imread(WashingtonDC_Band2_564.tif); subplot(3,2,2),imshow(f2); f3 imread(WashingtonDC_Band3_564.tif); subplot(3,2,3),imsho…...
2022/11/19 21:17:16 - 配置 已完成 请勿关闭计算机,win7系统关机提示“配置Windows Update已完成30%请勿关闭计算机...
win7系统关机提示“配置Windows Update已完成30%请勿关闭计算机”问题的解决方法在win7系统关机时如果有升级系统的或者其他需要会直接进入一个 等待界面,在等待界面中我们需要等待操作结束才能关机,虽然这比较麻烦,但是对系统进行配置和升级…...
2022/11/19 21:17:15 - 台式电脑显示配置100%请勿关闭计算机,“准备配置windows 请勿关闭计算机”的解决方法...
有不少用户在重装Win7系统或更新系统后会遇到“准备配置windows,请勿关闭计算机”的提示,要过很久才能进入系统,有的用户甚至几个小时也无法进入,下面就教大家这个问题的解决方法。第一种方法:我们首先在左下角的“开始…...
2022/11/19 21:17:14 - win7 正在配置 请勿关闭计算机,怎么办Win7开机显示正在配置Windows Update请勿关机...
置信有很多用户都跟小编一样遇到过这样的问题,电脑时发现开机屏幕显现“正在配置Windows Update,请勿关机”(如下图所示),而且还需求等大约5分钟才干进入系统。这是怎样回事呢?一切都是正常操作的,为什么开时机呈现“正…...
2022/11/19 21:17:13 - 准备配置windows 请勿关闭计算机 蓝屏,Win7开机总是出现提示“配置Windows请勿关机”...
Win7系统开机启动时总是出现“配置Windows请勿关机”的提示,没过几秒后电脑自动重启,每次开机都这样无法进入系统,此时碰到这种现象的用户就可以使用以下5种方法解决问题。方法一:开机按下F8,在出现的Windows高级启动选…...
2022/11/19 21:17:12 - 准备windows请勿关闭计算机要多久,windows10系统提示正在准备windows请勿关闭计算机怎么办...
有不少windows10系统用户反映说碰到这样一个情况,就是电脑提示正在准备windows请勿关闭计算机,碰到这样的问题该怎么解决呢,现在小编就给大家分享一下windows10系统提示正在准备windows请勿关闭计算机的具体第一种方法:1、2、依次…...
2022/11/19 21:17:11 - 配置 已完成 请勿关闭计算机,win7系统关机提示“配置Windows Update已完成30%请勿关闭计算机”的解决方法...
今天和大家分享一下win7系统重装了Win7旗舰版系统后,每次关机的时候桌面上都会显示一个“配置Windows Update的界面,提示请勿关闭计算机”,每次停留好几分钟才能正常关机,导致什么情况引起的呢?出现配置Windows Update…...
2022/11/19 21:17:10 - 电脑桌面一直是清理请关闭计算机,windows7一直卡在清理 请勿关闭计算机-win7清理请勿关机,win7配置更新35%不动...
只能是等着,别无他法。说是卡着如果你看硬盘灯应该在读写。如果从 Win 10 无法正常回滚,只能是考虑备份数据后重装系统了。解决来方案一:管理员运行cmd:net stop WuAuServcd %windir%ren SoftwareDistribution SDoldnet start WuA…...
2022/11/19 21:17:09 - 计算机配置更新不起,电脑提示“配置Windows Update请勿关闭计算机”怎么办?
原标题:电脑提示“配置Windows Update请勿关闭计算机”怎么办?win7系统中在开机与关闭的时候总是显示“配置windows update请勿关闭计算机”相信有不少朋友都曾遇到过一次两次还能忍但经常遇到就叫人感到心烦了遇到这种问题怎么办呢?一般的方…...
2022/11/19 21:17:08 - 计算机正在配置无法关机,关机提示 windows7 正在配置windows 请勿关闭计算机 ,然后等了一晚上也没有关掉。现在电脑无法正常关机...
关机提示 windows7 正在配置windows 请勿关闭计算机 ,然后等了一晚上也没有关掉。现在电脑无法正常关机以下文字资料是由(历史新知网www.lishixinzhi.com)小编为大家搜集整理后发布的内容,让我们赶快一起来看一下吧!关机提示 windows7 正在配…...
2022/11/19 21:17:05 - 钉钉提示请勿通过开发者调试模式_钉钉请勿通过开发者调试模式是真的吗好不好用...
钉钉请勿通过开发者调试模式是真的吗好不好用 更新时间:2020-04-20 22:24:19 浏览次数:729次 区域: 南阳 > 卧龙 列举网提醒您:为保障您的权益,请不要提前支付任何费用! 虚拟位置外设器!!轨迹模拟&虚拟位置外设神器 专业用于:钉钉,外勤365,红圈通,企业微信和…...
2022/11/19 21:17:05 - 配置失败还原请勿关闭计算机怎么办,win7系统出现“配置windows update失败 还原更改 请勿关闭计算机”,长时间没反应,无法进入系统的解决方案...
前几天班里有位学生电脑(windows 7系统)出问题了,具体表现是开机时一直停留在“配置windows update失败 还原更改 请勿关闭计算机”这个界面,长时间没反应,无法进入系统。这个问题原来帮其他同学也解决过,网上搜了不少资料&#x…...
2022/11/19 21:17:04 - 一个电脑无法关闭计算机你应该怎么办,电脑显示“清理请勿关闭计算机”怎么办?...
本文为你提供了3个有效解决电脑显示“清理请勿关闭计算机”问题的方法,并在最后教给你1种保护系统安全的好方法,一起来看看!电脑出现“清理请勿关闭计算机”在Windows 7(SP1)和Windows Server 2008 R2 SP1中,添加了1个新功能在“磁…...
2022/11/19 21:17:03 - 请勿关闭计算机还原更改要多久,电脑显示:配置windows更新失败,正在还原更改,请勿关闭计算机怎么办...
许多用户在长期不使用电脑的时候,开启电脑发现电脑显示:配置windows更新失败,正在还原更改,请勿关闭计算机。。.这要怎么办呢?下面小编就带着大家一起看看吧!如果能够正常进入系统,建议您暂时移…...
2022/11/19 21:17:02 - 还原更改请勿关闭计算机 要多久,配置windows update失败 还原更改 请勿关闭计算机,电脑开机后一直显示以...
配置windows update失败 还原更改 请勿关闭计算机,电脑开机后一直显示以以下文字资料是由(历史新知网www.lishixinzhi.com)小编为大家搜集整理后发布的内容,让我们赶快一起来看一下吧!配置windows update失败 还原更改 请勿关闭计算机&#x…...
2022/11/19 21:17:01 - 电脑配置中请勿关闭计算机怎么办,准备配置windows请勿关闭计算机一直显示怎么办【图解】...
不知道大家有没有遇到过这样的一个问题,就是我们的win7系统在关机的时候,总是喜欢显示“准备配置windows,请勿关机”这样的一个页面,没有什么大碍,但是如果一直等着的话就要两个小时甚至更久都关不了机,非常…...
2022/11/19 21:17:00 - 正在准备配置请勿关闭计算机,正在准备配置windows请勿关闭计算机时间长了解决教程...
当电脑出现正在准备配置windows请勿关闭计算机时,一般是您正对windows进行升级,但是这个要是长时间没有反应,我们不能再傻等下去了。可能是电脑出了别的问题了,来看看教程的说法。正在准备配置windows请勿关闭计算机时间长了方法一…...
2022/11/19 21:16:59 - 配置失败还原请勿关闭计算机,配置Windows Update失败,还原更改请勿关闭计算机...
我们使用电脑的过程中有时会遇到这种情况,当我们打开电脑之后,发现一直停留在一个界面:“配置Windows Update失败,还原更改请勿关闭计算机”,等了许久还是无法进入系统。如果我们遇到此类问题应该如何解决呢࿰…...
2022/11/19 21:16:58 - 如何在iPhone上关闭“请勿打扰”
Apple’s “Do Not Disturb While Driving” is a potentially lifesaving iPhone feature, but it doesn’t always turn on automatically at the appropriate time. For example, you might be a passenger in a moving car, but your iPhone may think you’re the one dri…...
2022/11/19 21:16:57