본문 바로가기

Backend Development/Spring boot

[Spring Boot] Quartz 배치 (scheduler) clustering

Spring application을 구축하다 보면 spring scheduler를 이용해서 cron job을 많이 사용하게 된다. 그러나 이중화나 쿠버네티스에서 여러 pod를 실행하게 되면 다수의 was에서 job이 중복해서 실행이 되게 된다.

 

이를 위해서 특정 Node의 was에서만 job을 실행되게 인자나 property를 뺄 수 있으나 배포시에 노드마다 설정을 지정해주어야 하는 번거로움이 생긴다.

 

이럴때 Spring boot + Quartz 배치를 사용하고 Clustering 기능을 켜면 Cluster 기능으로 fail over를 방지 할 수도 있고 동시에 여러잡이 실행되는것도 방지 할 수 있다.

 

Spring Quartz 라이브러리 Import

 

pom.xml에 아래 spring-boot-starter-quartz 라이브러리를 추가해 준다.

       <dependency>
         <groupId>org.springframework.boot</groupId>
         <artifactId>spring-boot-starter-quartz</artifactId>
      </dependency>

 

Quartz 설정 클래스, Property 추가

 

AutowiringSpringBeanJobFactory.java

 

아래 QuartzConfiguration.java에서 사용할 객체로 Quartz job에서 application에서 사용하고 있는 bean들을 access하기 위해서 아래 객체를 활용한다.

public class AutowiringSpringBeanJobFactory extends SpringBeanJobFactory implements ApplicationContextAware {
    private transient AutowireCapableBeanFactory beanFactory;

    @Override
    public void setApplicationContext(final ApplicationContext context) {
        beanFactory = context.getAutowireCapableBeanFactory();
    }

    @Override
    protected Object createJobInstance(final TriggerFiredBundle bundle) throws Exception {
        final Object job = super.createJobInstance(bundle);
        beanFactory.autowireBean(job);

        return job;
    }
}

 

QuartzConfiguration.java

 

Quartz 설정을 위한 configuration bean이다. job, Quartz trigger의 동작을 listen하고자 한다면 아래 JobListener, TriggersListener를 구현하여 주입해준다.

@Configuration
public class QuartzConfiguration {

    @Autowired
    private TriggersListener triggersListener;

    @Autowired
    private JobsListener jobsListener;

    @Autowired
    private QuartzProperties quartzProperties;

    @Bean
    public SchedulerFactoryBean schedulerFactoryBean(ApplicationContext applicationContext) {
        SchedulerFactoryBean schedulerFactoryBean = new SchedulerFactoryBean();

        AutowiringSpringBeanJobFactory jobFactory = new AutowiringSpringBeanJobFactory();
        jobFactory.setApplicationContext(applicationContext);
        schedulerFactoryBean.setJobFactory(jobFactory);

        schedulerFactoryBean.setApplicationContext(applicationContext);

        Properties properties = new Properties();
        properties.putAll(quartzProperties.getProperties());

        schedulerFactoryBean.setGlobalTriggerListeners(triggersListener);
        schedulerFactoryBean.setGlobalJobListeners(jobsListener);
        schedulerFactoryBean.setOverwriteExistingJobs(true);
        schedulerFactoryBean.setQuartzProperties(properties);
        schedulerFactoryBean.setWaitForJobsToCompleteOnShutdown(true);

        return schedulerFactoryBean;
    }
}

 

JobsListener.java (QuartzConfiguration에 주입)

@Component
public class JobsListener implements JobListener {

    @Override
    public String getName() {
        return "globalJob";
    }

    @Override
    public void jobToBeExecuted(JobExecutionContext context) {
        JobKey jobKey = context.getJobDetail().getKey();
        log.info("jobToBeExecuted :: jobkey : {}", jobKey);
    }

    @Override
    public void jobExecutionVetoed(JobExecutionContext context) {
        JobKey jobKey = context.getJobDetail().getKey();
        log.info("jobExecutionVetoed :: jobkey : {}", jobKey);
    }

    @Override
    public void jobWasExecuted(JobExecutionContext context, JobExecutionException jobException) {
        JobKey jobKey = context.getJobDetail().getKey();
        log.info("jobWasExecuted :: jobkey : {}", jobKey);
    }
}

 

TriggersListener.java  (QuartzConfiguration에 주입)

@Component
public class TriggersListener implements TriggerListener {

    @Override
    public String getName() {
        return "globalTrigger";
    }

    @Override
    public void triggerFired(Trigger trigger, JobExecutionContext context) {
        JobKey jobKey = trigger.getJobKey();
        log.info("triggerFired at {} :: jobkey : {}", trigger.getStartTime(), jobKey);
    }

    @Override
    public boolean vetoJobExecution(Trigger trigger, JobExecutionContext context) {
        return false;
    }

    @Override
    public void triggerMisfired(Trigger trigger) {
        JobKey jobKey = trigger.getJobKey();
        log.info("triggerMisfired at {} :: jobkey : {}", trigger.getStartTime(), jobKey);
    }

    @Override
    public void triggerComplete(Trigger trigger, JobExecutionContext context, Trigger.CompletedExecutionInstruction triggerInstructionCode) {
        JobKey jobKey = trigger.getJobKey();
        log.info("triggerComplete at {} :: jobkey : {}", trigger.getStartTime(), jobKey);
    }
}

 

JobInitialization.java

 

실제로 application에서 설정한 job을 띄워주는 configuration 이다 @PostConstruct 어노테이션을 달아서 was 구동시에 수행되도록 한다.

@Configuration
public class JobInitialization {

    @Autowired
    private QuartzService quartzService;

    @PostConstruct
    public void start() {
        List<StartQuartzJobDVO> startQuartzJobDVOS = quartzService.selectStartQuartzJobs();

        for (StartQuartzJobDVO startQuartzJobDVO : startQuartzJobDVOS) {
            QuartzJobRequestDVO quartzJobRequestDVO = QuartzJobRequestDVO.builder()
                .jobName(startQuartzJobDVO.getJobName())
                .jobGroup(startQuartzJobDVO.getJobGroup())
                .jobClass(startQuartzJobDVO.getJobClass())
                .cronExpression(startQuartzJobDVO.getCronExpression())
                .jobDescription(startQuartzJobDVO.getJobDescription())
                .build();

            quartzService.addQuartzJob(quartzJobRequestDVO);
            quartzService.updateQuartzJobAsScheduled(startQuartzJobDVO.getScheduleId());
        }
    }
}

 

QuartzService.java

 

실제 job을 불러오고 quartz  job을 등록하는 부분. QuartzMapper에서 테이블 select, update를 구현한다.

@Service
public class QuartzService {

    @Autowired
    private SchedulerFactoryBean schedulerFactoryBean;

    @Autowired
    private ApplicationContext applicationContext;

    @Autowired
    QuartzMapper quartzMapper;

    public List<StartQuartzJobDVO> selectStartQuartzJobs() {
        return quartzMapper.selectStartQuartzJobs();
    }

    public void addQuartzJob(QuartzJobRequestDVO quartzJobRequestDVO) {
        JobDetail jobDetail;
        Trigger trigger;

        try {
            trigger = JobUtils.createCronTrigger(quartzJobRequestDVO);
            jobDetail = JobUtils.createCronJob(quartzJobRequestDVO, applicationContext);
            schedulerFactoryBean.getScheduler().scheduleJob(jobDetail, trigger);
        } catch (ParseException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (SchedulerException e) {
            e.printStackTrace();
        }
    }

    @Transactional
    public int updateQuartzJobAsScheduled(Integer scheduleId) {
        return quartzMapper.updateQuartzJobAsScheduled(scheduleId);
    }

    public BatchJobSelectResponseDVO selectBatchJob() throws SchedulerException {
        Scheduler scheduler = schedulerFactoryBean.getScheduler();
        List<BatchJobDVO> jobs = new ArrayList<>();

        for (String jobGroupName : scheduler.getJobGroupNames()) {
            for (JobKey jobkey : scheduler.getJobKeys(GroupMatcher.jobGroupEquals(jobGroupName))) {
                JobDetail jobDetail = scheduler.getJobDetail(jobkey);
                List<Trigger> triggers = (List<Trigger>) scheduler.getTriggersOfJob(jobkey);
                BatchJobDVO batchJobDVO = BatchJobDVO.builder()
                    .jobName(jobkey.getName())
                    .jobGroup(jobkey.getGroup())
                    .jobClass(jobDetail.getJobClass().getName())
                    .jobDescription(jobDetail.getDescription())
                    .scheduleTime(DateTimeUtils.toString(triggers.get(0).getStartTime()))
                    .lastFiredTime(DateTimeUtils.toString(triggers.get(0).getPreviousFireTime()))
                    .nextFireTime(DateTimeUtils.toString(triggers.get(0).getNextFireTime()))
                    .jobStatus(isJobRunning(jobkey) ? "RUNNING" : getJobStatus(jobkey))
                    .build();

                jobs.add(batchJobDVO);
            }
        }

        return BatchJobSelectResponseDVO.builder().jobs(jobs).build();
    }

    private boolean isJobRunning(JobKey jobkey) throws SchedulerException {
        List<JobExecutionContext> currentJobs = schedulerFactoryBean.getScheduler().getCurrentlyExecutingJobs();

        if (currentJobs != null) {
            for (JobExecutionContext jobctx : currentJobs) {
                if (jobkey.getName().equals(jobctx.getJobDetail().getKey().getName())) {
                    return true;
                }
            }
        }

        return false;
    }

    private String getJobStatus(JobKey jobKey) throws SchedulerException {
        Scheduler scheduler = schedulerFactoryBean.getScheduler();
        JobDetail jobDetail = scheduler.getJobDetail(jobKey);

        List<? extends Trigger> triggers = scheduler.getTriggersOfJob(jobDetail.getKey());

        if (triggers != null && triggers.size() > 0) {
            Trigger.TriggerState triggerState = scheduler.getTriggerState(triggers.get(0).getKey());

            if (Trigger.TriggerState.NORMAL.equals(triggerState)) {
                return "SCHEDULED";
            }

            return triggerState.name().toUpperCase();
        }

        return null;
    }
}

 

QuartzMapper.java

@Mapper
public interface QuartzMapper {
    List<StartQuartzJobDVO> selectStartQuartzJobs();

    int updateQuartzJobAsScheduled(@Param("scheduleId") Integer scheduleId);

    int addTestHistory(RestHistoryVO param);
}

 

quartz.xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.sdp.common.schedule.QuartzMapper">

   <resultMap id="StartQuzrtzJobMap" type="com.sdp.common.schedule.dvo.StartQuartzJobDVO">
      <result column="SCH_ID" property="scheduleId"/>
      <result column="JOB_NAME" property="jobName"/>
      <result column="JOB_GROUP" property="jobGroup"/>
      <result column="JOB_CLASS_NAME" property="jobClass"/>
      <result column="CRON_EXPRESSION" property="cronExpression"/>
      <result column="DESCRIPTION" property="jobDescription"/>
   </resultMap>

   <select id="selectStartQuartzJobs" parameterType="hashmap" resultMap="StartQuzrtzJobMap">
      /* QuartzMapper:selectStartQuartzJobs */
      <![CDATA[
         SELECT
            SCH_ID,
            JOB_NAME,
            JOB_GROUP,
            JOB_CLASS_NAME,
            TRIGGER_NAME,
            TRIGGER_GROUP,
            CRON_EXPRESSION,
            DESCRIPTION,
            CREATED_USER,
            CREATED_DATE,
            UPDATED_USER,
            UPDATED_DATE,
            SCHED_YN,
            USE_YN
         FROM SCHEDULE_INFO
         WHERE USE_YN = 'Y'
         AND SCHED_YN = 'Y'
      ]]>
   </select>

   <update id="updateQuartzJobAsScheduled" parameterType="Integer">
         UPDATE SCHEDULE_INFO
         SET
            SCHED_YN = 'N'
         WHERE
            SCH_ID = #{scheduleId}
   </update>

   <insert id="addTestHistory" parameterType="com.sdp.vo.rest.RestHistoryVO">
      /* restAopMapper:addRestCallHistory */
      <![CDATA[
         INSERT INTO    REST_HISTORY (
            REST_ID,
            REST_URL,
            REST_TY,
            REST_PARAMTR,
            CONECT_IP,
            CALL_RESULT,
            CALLMAN,
            CALLDT
         ) VALUES (
            #{restId},
            #{restUrl},
            #{restTy},
            #{restParamtr},
            #{conectIp},
            #{callResult},
            #{callman},
            CURRENT_TIMESTAMP
         )
      ]]>
   </insert>
</mapper>

 

QuartzTestJob.java

 

Quartz job을 구현시 QuartzJobBean을 상속해서 구현하면 Cron trigger설정에 맞게 job이 실행된다.

public class QuartzTestJob extends QuartzJobBean {

    private static final Logger logger = LoggerFactory.getLogger(QuartzTestJob.class);

    private QuartzTestJobService service = ApplicationContextProvider.getApplicationContext()
        .getBean(QuartzTestJobService.class);

    @Override
    protected void executeInternal(JobExecutionContext context) throws JobExecutionException {
        service.executeService();
    }
}

 

 

QuartzTestJobService.java

 

테스트로 만들어본 Job 예제. 매 분 0초에 현재 시간 (yyyyMMddhhmm)을 구해서 DB에 insert한다. 만일 Quartz 를 여러개 돌렸을때 동일 분에 해당하는 키가 DB에 여러개 있다며 Quartz job의 DiallowConcurrent 속성이 제대로 설정이 안된것이므로 설정을 다시 살펴보아야 할것이다.

@Service
public class QuartzTestJobService {

    @Autowired
    private QuartzMapper quartzMapper;

    public void executeService() {
        RestHistoryVO vo = new RestHistoryVO();
        vo.setCalldtlsId(Integer.parseInt(Instant.now()
            .atZone(ZoneId.of("Asia/Seoul"))
            .format(DateTimeFormatter.ofPattern("MMddHHmm").withLocale(Locale.ENGLISH))));
        vo.setRestId(Instant.now()
            .atZone(ZoneId.of("Asia/Seoul"))
            .format(DateTimeFormatter.ofPattern("yyyyMMddHHmm").withLocale(Locale.ENGLISH)));
        vo.setRestUrl("test");
        vo.setRestTy("test");
        vo.setRestParamtr("");

        quartzMapper.addTestHistory(vo);
    }

}

 

application-dev.properties

 

Spring boot의 property에 아래 설정을 추가해 준다. Oracle DB를 사용했으므로 그에 맞는 dataSource를 지정해준다.

spring.quartz.scheduler-name=QuartzScheduler
spring.quartz.job-store-type=jdbc
spring.quartz.properties.org.quartz.scheduler.instanceId=AUTO
spring.quartz.properties.org.quartz.threadPool.threadCount=5
spring.quartz.properties.org.quartz.threadPool.threadNamePrefix=QuartzScheduler
spring.quartz.properties.org.quartz.jobStore.class=org.quartz.impl.jdbcjobstore.JobStoreTX
spring.quartz.properties.org.quartz.jobStore.driverDelegateClass=org.quartz.impl.jdbcjobstore.StdJDBCDelegate
spring.quartz.properties.org.quartz.jobStore.dataSource=quartzDataSource
spring.quartz.properties.org.quartz.jobStore.useProperties=true
spring.quartz.properties.org.quartz.jobStore.misfireThreshold=6000
spring.quartz.properties.org.quartz.jobStore.tablePrefix=QRTZ_
spring.quartz.properties.org.quartz.jobStore.isClustered=true
spring.quartz.properties.org.quartz.dataSource.quartzDataSource.URL=jdbc:log4jdbc:oracle:thin:@172.30.1.45:1521:ORCLCDB
spring.quartz.properties.org.quartz.dataSource.quartzDataSource.driver=net.sf.log4jdbc.sql.jdbcapi.DriverSpy
spring.quartz.properties.org.quartz.dataSource.quartzDataSource.user=sdp
spring.quartz.properties.org.quartz.dataSource.quartzDataSource.password=sdp
spring.quartz.properties.org.quartz.dataSource.quartzDataSource.provider=hikaricp

 

Schedule_info table script

 

Application에서 사용할 Job의 내용을 담는다.

  CREATE TABLE "SDP"."SCHEDULE_INFO" 
   (	"SCH_ID" NUMBER(10,0) GENERATED ALWAYS AS IDENTITY MINVALUE 1 MAXVALUE 9999999999999999999999999999 INCREMENT BY 1 START WITH 29 CACHE 20 NOORDER  NOCYCLE  NOKEEP  NOSCALE , 
	"SCHED_NAME" VARCHAR2(120 BYTE), 
	"JOB_NAME" VARCHAR2(200 BYTE), 
	"JOB_GROUP" VARCHAR2(200 BYTE), 
	"JOB_CLASS_NAME" VARCHAR2(250 BYTE), 
	"TRIGGER_NAME" VARCHAR2(200 BYTE), 
	"TRIGGER_GROUP" VARCHAR2(200 BYTE), 
	"CRON_EXPRESSION" VARCHAR2(200 BYTE), 
	"DESCRIPTION" VARCHAR2(250 BYTE), 
	"SCHED_YN" CHAR(1 BYTE), 
	"USE_YN" CHAR(1 BYTE), 
	"CREATED_USER" NUMBER(10,0), 
	"CREATED_DATE" TIMESTAMP (6), 
	"UPDATED_USER" NUMBER(10,0), 
	"UPDATED_DATE" TIMESTAMP (6)
   ) SEGMENT CREATION IMMEDIATE 
  PCTFREE 10 PCTUSED 40 INITRANS 1 MAXTRANS 255 
 NOCOMPRESS LOGGING
  STORAGE(INITIAL 65536 NEXT 1048576 MINEXTENTS 1 MAXEXTENTS 2147483645
  PCTINCREASE 0 FREELISTS 1 FREELIST GROUPS 1
  BUFFER_POOL DEFAULT FLASH_CACHE DEFAULT CELL_FLASH_CACHE DEFAULT)
  TABLESPACE "USERS" ;

 

Quartz table 생성 스크립트 (Oracle DB용)

더보기

delete from qrtz_fired_triggers;
delete from qrtz_simple_triggers;
delete from qrtz_simprop_triggers;
delete from qrtz_cron_triggers;
delete from qrtz_blob_triggers;
delete from qrtz_triggers;
delete from qrtz_job_details;
delete from qrtz_calendars;
delete from qrtz_paused_trigger_grps;
delete from qrtz_locks;
delete from qrtz_scheduler_state;

drop table qrtz_calendars;
drop table qrtz_fired_triggers;
drop table qrtz_blob_triggers;
drop table qrtz_cron_triggers;
drop table qrtz_simple_triggers;
drop table qrtz_simprop_triggers;
drop table qrtz_triggers;
drop table qrtz_job_details;
drop table qrtz_paused_trigger_grps;
drop table qrtz_locks;
drop table qrtz_scheduler_state;


CREATE TABLE qrtz_job_details
  (
    SCHED_NAME VARCHAR2(120) NOT NULL,
    JOB_NAME  VARCHAR2(200) NOT NULL,
    JOB_GROUP VARCHAR2(200) NOT NULL,
    DESCRIPTION VARCHAR2(250) NULL,
    JOB_CLASS_NAME   VARCHAR2(250) NOT NULL, 
    IS_DURABLE VARCHAR2(1) NOT NULL,
    IS_NONCONCURRENT VARCHAR2(1) NOT NULL,
    IS_UPDATE_DATA VARCHAR2(1) NOT NULL,
    REQUESTS_RECOVERY VARCHAR2(1) NOT NULL,
    JOB_DATA BLOB NULL,
    CONSTRAINT QRTZ_JOB_DETAILS_PK PRIMARY KEY (SCHED_NAME,JOB_NAME,JOB_GROUP)
);
CREATE TABLE qrtz_triggers
  (
    SCHED_NAME VARCHAR2(120) NOT NULL,
    TRIGGER_NAME VARCHAR2(200) NOT NULL,
    TRIGGER_GROUP VARCHAR2(200) NOT NULL,
    JOB_NAME  VARCHAR2(200) NOT NULL, 
    JOB_GROUP VARCHAR2(200) NOT NULL,
    DESCRIPTION VARCHAR2(250) NULL,
    NEXT_FIRE_TIME NUMBER(19) NULL,
    PREV_FIRE_TIME NUMBER(19) NULL,
    PRIORITY NUMBER(13) NULL,
    TRIGGER_STATE VARCHAR2(16) NOT NULL,
    TRIGGER_TYPE VARCHAR2(8) NOT NULL,
    START_TIME NUMBER(19) NOT NULL,
    END_TIME NUMBER(19) NULL,
    CALENDAR_NAME VARCHAR2(200) NULL,
    MISFIRE_INSTR NUMBER(2) NULL,
    JOB_DATA BLOB NULL,
    CONSTRAINT QRTZ_TRIGGERS_PK PRIMARY KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP),
    CONSTRAINT QRTZ_TRIGGER_TO_JOBS_FK FOREIGN KEY (SCHED_NAME,JOB_NAME,JOB_GROUP) 
      REFERENCES QRTZ_JOB_DETAILS(SCHED_NAME,JOB_NAME,JOB_GROUP) 
);
CREATE TABLE qrtz_simple_triggers
  (
    SCHED_NAME VARCHAR2(120) NOT NULL,
    TRIGGER_NAME VARCHAR2(200) NOT NULL,
    TRIGGER_GROUP VARCHAR2(200) NOT NULL,
    REPEAT_COUNT NUMBER(7) NOT NULL,
    REPEAT_INTERVAL NUMBER(12) NOT NULL,
    TIMES_TRIGGERED NUMBER(10) NOT NULL,
    CONSTRAINT QRTZ_SIMPLE_TRIG_PK PRIMARY KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP),
    CONSTRAINT QRTZ_SIMPLE_TRIG_TO_TRIG_FK FOREIGN KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP) 
REFERENCES QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP)
);
CREATE TABLE qrtz_cron_triggers
  (
    SCHED_NAME VARCHAR2(120) NOT NULL,
    TRIGGER_NAME VARCHAR2(200) NOT NULL,
    TRIGGER_GROUP VARCHAR2(200) NOT NULL,
    CRON_EXPRESSION VARCHAR2(120) NOT NULL,
    TIME_ZONE_ID VARCHAR2(80),
    CONSTRAINT QRTZ_CRON_TRIG_PK PRIMARY KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP),
    CONSTRAINT QRTZ_CRON_TRIG_TO_TRIG_FK FOREIGN KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP) 
      REFERENCES QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP)
);
CREATE TABLE qrtz_simprop_triggers
  (          
    SCHED_NAME VARCHAR2(120) NOT NULL,
    TRIGGER_NAME VARCHAR2(200) NOT NULL,
    TRIGGER_GROUP VARCHAR2(200) NOT NULL,
    STR_PROP_1 VARCHAR2(512) NULL,
    STR_PROP_2 VARCHAR2(512) NULL,
    STR_PROP_3 VARCHAR2(512) NULL,
    INT_PROP_1 NUMBER(10) NULL,
    INT_PROP_2 NUMBER(10) NULL,
    LONG_PROP_1 NUMBER(19) NULL,
    LONG_PROP_2 NUMBER(19) NULL,
    DEC_PROP_1 NUMERIC(13,4) NULL,
    DEC_PROP_2 NUMERIC(13,4) NULL,
    BOOL_PROP_1 VARCHAR2(1) NULL,
    BOOL_PROP_2 VARCHAR2(1) NULL,
    TIME_ZONE_ID VARCHAR2(80) NULL,
    CONSTRAINT QRTZ_SIMPROP_TRIG_PK PRIMARY KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP),
    CONSTRAINT QRTZ_SIMPROP_TRIG_TO_TRIG_FK FOREIGN KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP) 
      REFERENCES QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP)
);
CREATE TABLE qrtz_blob_triggers
  (
    SCHED_NAME VARCHAR2(120) NOT NULL,
    TRIGGER_NAME VARCHAR2(200) NOT NULL,
    TRIGGER_GROUP VARCHAR2(200) NOT NULL,
    BLOB_DATA BLOB NULL,
    CONSTRAINT QRTZ_BLOB_TRIG_PK PRIMARY KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP),
    CONSTRAINT QRTZ_BLOB_TRIG_TO_TRIG_FK FOREIGN KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP) 
        REFERENCES QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP)
);
CREATE TABLE qrtz_calendars
  (
    SCHED_NAME VARCHAR2(120) NOT NULL,
    CALENDAR_NAME  VARCHAR2(200) NOT NULL, 
    CALENDAR BLOB NOT NULL,
    CONSTRAINT QRTZ_CALENDARS_PK PRIMARY KEY (SCHED_NAME,CALENDAR_NAME)
);
CREATE TABLE qrtz_paused_trigger_grps
  (
    SCHED_NAME VARCHAR2(120) NOT NULL,
    TRIGGER_GROUP  VARCHAR2(200) NOT NULL, 
    CONSTRAINT QRTZ_PAUSED_TRIG_GRPS_PK PRIMARY KEY (SCHED_NAME,TRIGGER_GROUP)
);
CREATE TABLE qrtz_fired_triggers 
  (
    SCHED_NAME VARCHAR2(120) NOT NULL,
    ENTRY_ID VARCHAR2(140) NOT NULL,
    TRIGGER_NAME VARCHAR2(200) NOT NULL,
    TRIGGER_GROUP VARCHAR2(200) NOT NULL,
    INSTANCE_NAME VARCHAR2(200) NOT NULL,
    FIRED_TIME NUMBER(19) NOT NULL,
    SCHED_TIME NUMBER(19) NOT NULL,
PRIORITY NUMBER(13) NOT NULL,
    STATE VARCHAR2(16) NOT NULL,
    JOB_NAME VARCHAR2(200) NULL,
    JOB_GROUP VARCHAR2(200) NULL,
    IS_NONCONCURRENT VARCHAR2(1) NULL,
    REQUESTS_RECOVERY VARCHAR2(1) NULL,
    CONSTRAINT QRTZ_FIRED_TRIGGER_PK PRIMARY KEY (SCHED_NAME,ENTRY_ID)
);
CREATE TABLE qrtz_scheduler_state 
  (
    SCHED_NAME VARCHAR2(120) NOT NULL,
    INSTANCE_NAME VARCHAR2(200) NOT NULL,
    LAST_CHECKIN_TIME NUMBER(19) NOT NULL,
    CHECKIN_INTERVAL NUMBER(13) NOT NULL,
    CONSTRAINT QRTZ_SCHEDULER_STATE_PK PRIMARY KEY (SCHED_NAME,INSTANCE_NAME)
);
CREATE TABLE qrtz_locks
  (
    SCHED_NAME VARCHAR2(120) NOT NULL,
    LOCK_NAME  VARCHAR2(40) NOT NULL, 
    CONSTRAINT QRTZ_LOCKS_PK PRIMARY KEY (SCHED_NAME,LOCK_NAME)
);

create index idx_qrtz_j_req_recovery on qrtz_job_details(SCHED_NAME,REQUESTS_RECOVERY);
create index idx_qrtz_j_grp on qrtz_job_details(SCHED_NAME,JOB_GROUP);

create index idx_qrtz_t_j on qrtz_triggers(SCHED_NAME,JOB_NAME,JOB_GROUP);
create index idx_qrtz_t_jg on qrtz_triggers(SCHED_NAME,JOB_GROUP);
create index idx_qrtz_t_c on qrtz_triggers(SCHED_NAME,CALENDAR_NAME);
create index idx_qrtz_t_g on qrtz_triggers(SCHED_NAME,TRIGGER_GROUP);
create index idx_qrtz_t_state on qrtz_triggers(SCHED_NAME,TRIGGER_STATE);
create index idx_qrtz_t_n_state on qrtz_triggers(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP,TRIGGER_STATE);
create index idx_qrtz_t_n_g_state on qrtz_triggers(SCHED_NAME,TRIGGER_GROUP,TRIGGER_STATE);
create index idx_qrtz_t_next_fire_time on qrtz_triggers(SCHED_NAME,NEXT_FIRE_TIME);
create index idx_qrtz_t_nft_st on qrtz_triggers(SCHED_NAME,TRIGGER_STATE,NEXT_FIRE_TIME);
create index idx_qrtz_t_nft_misfire on qrtz_triggers(SCHED_NAME,MISFIRE_INSTR,NEXT_FIRE_TIME);
create index idx_qrtz_t_nft_st_misfire on qrtz_triggers(SCHED_NAME,MISFIRE_INSTR,NEXT_FIRE_TIME,TRIGGER_STATE);
create index idx_qrtz_t_nft_st_misfire_grp on qrtz_triggers(SCHED_NAME,MISFIRE_INSTR,NEXT_FIRE_TIME,TRIGGER_GROUP,TRIGGER_STATE);

create index idx_qrtz_ft_trig_inst_name on qrtz_fired_triggers(SCHED_NAME,INSTANCE_NAME);
create index idx_qrtz_ft_inst_job_req_rcvry on qrtz_fired_triggers(SCHED_NAME,INSTANCE_NAME,REQUESTS_RECOVERY);
create index idx_qrtz_ft_j_g on qrtz_fired_triggers(SCHED_NAME,JOB_NAME,JOB_GROUP);
create index idx_qrtz_ft_jg on qrtz_fired_triggers(SCHED_NAME,JOB_GROUP);
create index idx_qrtz_ft_t_g on qrtz_fired_triggers(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP);
create index idx_qrtz_ft_tg on qrtz_fired_triggers(SCHED_NAME,TRIGGER_GROUP);

 

Application에서 사용하고 있는 Job 내용을 담는 Schedule_info 테이블 row 내용. Test 용으로 QuartzTestJob을 생성했다. Cron Expression은 매분 0초에 실행되도록 한다.

 

Quzrtz Job 복수 실행 결과

 

War를 여러개 띄워서 Quartz job을 실행해보면 매분 하나의 Entry만 insert되어 있는것을 볼수 있다. 즉 같은 Cron Expression을 가진 Job이 여러개 실행되어 있지만 매 순간 하나만 진입하여 실행됨을 볼 수 있다. 

 

이렇게 원자성이 보장이 되니 고민없이 Pod 수를 늘리거나 이중화를 할수 있어서 편해진다.

 

-- The End --