Spring Boot Quartz 使用介绍

       Quartz是用来实现定时任务的一个框架。如果光说定时任务你可能会说@scheduled也能实现呀。还要Quartz干啥。因为Quartz的功能比@scheduled稍微多一点。主要体现在以下几个方面

  • Quart的定时任务可以随时修改。即使任务已经在调度系统中了。你还是可以随时去改变任务的实行时间(@scheduled实现该功能就比较麻烦了)。

  • 服务重启之后Quartz的任务会自动拉起来,自动在调度系统里面根据触发器设置的时间执行。

  • Quartz支持集群(多台服务器部署了quartz任务,只有有一台服务器执行。而且一台服务器挂了相应的会转到其他服务器执行)。

Quartz集群的实现依赖11张表,我们会在后面讲到。

       接下来咱们来说一说Quartz框架的使用。Quartz使用的关键在Scheduler、Job 、JobDetail、Trigger四个类的熟悉。所以咱们先简单的介绍下这几个类,然后介绍下怎么在spring boot里面使用上Quartz框架。

一 Quartz框架里面主要类的介绍

       咱们主要是介绍下Scheduler、Job 、JobDetail、Trigger四个类。当然还有一些其他的类。咱用一个简单的类图来简单的展示下Quartz框架使用过程中相关的一些类。如下:

quartz (3).png

1.1 Job类

       Job用于定义任务具体的逻辑。简单来说就是用来定义定时任务需要干的事情。比如我们想每天早上10点给某某人发一封邮件。Job干的事情就是发邮件的事情。每个任务对应一个Job(咱们一般会自定义一个Job,继承QuartzJobBean,通过实现里面的executeInternal方法实现具体的任务逻辑)。这样调度器(Scheduler)会根据触发器(Trigger)设置的时间叫这个任务(Job)起来干活了。

       关于Job重点想说的是在Job类里面怎么获取到外部参数。首先我们肯定会在定义任务详情的时候给这个Job的JobDataMap属性里面设置一些参数。然后在Job执行的时候,我们通过JobDataMap jobDataMap = jobExecutionContext.getMergedJobDataMap();获取到JobDataMap(JobDataMap就相当于一个Map),取出相应的参数了。

比如如下的一个实例我们简单的实现一个SendEmailJob类。

/**
 * 定时发送邮件任务 -- 只是一个模拟任务,大家根据实际情况编写
 */
public class SendEmailJob extends QuartzJobBean {
    @Override
    protected void executeInternal(JobExecutionContext jobExecutionContext) {

        JobDataMap jobDataMap = jobExecutionContext.getMergedJobDataMap();
        if (jobDataMap != null && !jobDataMap.isEmpty()) {
            System.out.println("******************************");
            System.out.println("******************************");
            System.out.println("******************************");
            System.out.println("job name = " + jobDataMap.get(QuartzActionServiceImpl.TASK_ID_KEY));
            System.out.println("开始发送邮件了");
        }

    }
}

1.2 JobDetail类

       JobDetail定义任务详情。包含执行任务的Job,任务的一些身份信息(可以帮助找到这个任务),给任务设置JobDataMap(把参数带到任务里面去)。JobDetail里面常用方法如下:

public interface JobDetail {

    /**
     * job的身份,通过他来找到对应的job
     */
    public JobKey getKey();

    /**
     * job描述
     */
    public String getDescription();

    /**
     * 执行job的具体类,定时任务的动作都在这个类里面完成
     */
    public Class<? extends Job> getJobClass();

    /**
     * 给job传递数据(把需要的参数带到job执行的类里面去)
     */
    public JobDataMap getJobDataMap();

    /**
     * 任务孤立的时候是否需要继续报错(孤立:没有触发器关联该任务)
     */
    public boolean isDurable();

    /**
     * 和@PersistJobDataAfterExecution注解一样
     * PersistJobDataAfterExecution注解是添加在Job类上的:表示 Quartz 将会在成功执行 execute()
     * 方法后(没有抛出异常)更新 JobDetail 的 JobDataMap,下一次执行相同的任务(JobDetail)
     * 将会得到更新后的值,而不是原始的值
     */
    public boolean isPersistJobDataAfterExecution();

    /**
     * 和DisallowConcurrentExecution注解的功能一样
     * DisallowConcurrentExecution注解添加到Job之后,Quartz 将不会同时执行多个 Job 实例,
     * 怕有数据更新的时候不知道取哪一个数据
     */
    public boolean isConcurrentExectionDisallowed();

    /**
     * 指示调度程序在遇到“恢复”或“故障转移”情况时是否应重新执行作业
     */
    public boolean requestsRecovery();


    /**
     * JobDetail是通过构建者模式来实现的
     */
    public JobBuilder getJobBuilder();
}

       JobDetail实例一般是通过JobBuilder来创建(Build模式)。来一个实例。我们定义一个简答的JobDetail,Job执行逻辑类是SendEmailJob类,Job名字是sendEmail,Job组是groupName(job名字和job组是job的唯一标识)。同时我们还给job传递了一个字符串参数sendEmail。最终代码如下:

            JobDetail jobDetail = JobBuilder
                    .newJob(SendEmailJob.class)
                    .withIdentity("sendEmail", "groupName")
                    .build();
            // 参数使用
            JobDataMap map = jobDetail.getJobDataMap();
            map.put("JOB_NAME", "sendEmail");

注意:JobKey是任务的唯一标识。修改任务,删除任务都是需要通过他来找到指定的任务。

1.3 Trigger类

       Trigger触发器,设置Job什么时候执行。Quartz框架默认给咱们提供了四种触发器。如下:

触发器 试用场景
SimpleTrigger 简单触发器,适用于 按指定的时间间隔执行多少次任务的情况
CronTrigger Cron触发器,通过Cron表达式来控制任务的执行时间
DailyTimeIntervalTrigger 日期触发器,在给定的时间范围内或指定的星期内以秒、分钟或者小时为周期进行重复的情况
CalendarIntervalTrigger 日历触发器,根据一个给定的日历时间进行重复

切记,一个Trigger只能绑定一个Job。但是一个Job可以被多个Trigger绑定。

1.3.1 SimpleTrigger

       SimpleTrigger简单触发器,设置任务每隔多长时间,执行多少次。SimpleTrigger由SimpleScheduleBuilder构建生成。所以咱们简单的来看下SimpleScheduleBuilder里面常用的一些函数:

    /**
     * 每分钟都执行,执行无限次
     */
    public static SimpleScheduleBuilder repeatMinutelyForever();

    /**
     * 每隔minutes分钟执行一次,循环无限次
     */
    public static SimpleScheduleBuilder repeatMinutelyForever(int minutes);

    /**
     * 每秒执行一次,循循环无限次
     */
    public static SimpleScheduleBuilder repeatSecondlyForever();

    /**
     * 每隔seconds秒执行一次,循环无限次
     */
    public static SimpleScheduleBuilder repeatSecondlyForever(int seconds);

    /**
     * 每小时执行一次,循环无限次
     */
    public static SimpleScheduleBuilder repeatHourlyForever();

    /**
     * 每hours小时执行一次,循环无限次
     */
    public static SimpleScheduleBuilder repeatHourlyForever(int hours);

    /**
     *  每分钟执行一次,执行count次
     */
    public static SimpleScheduleBuilder repeatMinutelyForTotalCount(int count);

    /**
     * 每minutes执行一次,执行count次
     */
    public static SimpleScheduleBuilder repeatMinutelyForTotalCount(int count, int minutes);

    /**
     * 每秒执行一次,执行count次
     */
    public static SimpleScheduleBuilder repeatSecondlyForTotalCount(int count);

    /**
     * 每seconds执行一次,执行count次
     */
    public static SimpleScheduleBuilder repeatSecondlyForTotalCount(int count, int seconds);

    /**
     * 每小时执行一次,执行count次
     */
    public static SimpleScheduleBuilder repeatHourlyForTotalCount(int count);

    /**
     * 每hours执行一次,执行count次
     */
    public static SimpleScheduleBuilder repeatHourlyForTotalCount(int count, int hours) ;


    /**
     * 任务执行的时间间隔 -- 单位毫秒
     */
    public SimpleScheduleBuilder withIntervalInMilliseconds(long intervalInMillis);

    /**
     * 任务执行的时间间隔 -- 单位秒
     */
    public SimpleScheduleBuilder withIntervalInSeconds(int intervalInSeconds);

    /**
     * 任务执行的时间间隔 -- 单位分
     */
    public SimpleScheduleBuilder withIntervalInMinutes(int intervalInMinutes);

    /**
     * 任务执行的时间间隔 -- 单位小时
     */
    public SimpleScheduleBuilder withIntervalInHours(int intervalInHours);

    /**
     * S任务执行次数
     */
    public SimpleScheduleBuilder withRepeatCount(int triggerRepeatCount);

    /**
     * 任务执行无限次
     */
    public SimpleScheduleBuilder repeatForever();

    /**
     * 这个不是忽略已经错失的触发的意思,而是说忽略MisFire策略。它会在资源合适的时候,重新触发所有的MisFire任务,并且不会影响现有的调度时间。
     *
     * 比如,SimpleTrigger每15秒执行一次,而中间有5分钟时间它都MisFire了,一共错失了20个,5分钟后,假设资源充足了,并且任务允许并发,它会被一次性触发
     */
    public SimpleScheduleBuilder withMisfireHandlingInstructionIgnoreMisfires() {
        misfireInstruction = Trigger.MISFIRE_INSTRUCTION_IGNORE_MISFIRE_POLICY;
        return this;
    }

    /**
     * 忽略已经MisFire的任务,并且立即执行调度。这通常只适用于只执行一次的任务
     */

    public SimpleScheduleBuilder withMisfireHandlingInstructionFireNow() {
        misfireInstruction = SimpleTrigger.MISFIRE_INSTRUCTION_FIRE_NOW;
        return this;
    }

    /**
     * 在下一次调度时间点,重新开始调度任务,包括MisFire的
     */
    public SimpleScheduleBuilder withMisfireHandlingInstructionNextWithExistingCount() {
        misfireInstruction = SimpleTrigger.MISFIRE_INSTRUCTION_RESCHEDULE_NEXT_WITH_EXISTING_COUNT;
        return this;
    }

    /**
     * 在下一次调度时间点,重新开始调度任务,忽略已经MisFire的任务
     */
    public SimpleScheduleBuilder withMisfireHandlingInstructionNextWithRemainingCount() {
        misfireInstruction = SimpleTrigger.MISFIRE_INSTRUCTION_RESCHEDULE_NEXT_WITH_REMAINING_COUNT;
        return this;
    }

    /**
     * 将startTime设置当前时间,立即重新调度任务,包括MisFire的
     */
    public SimpleScheduleBuilder withMisfireHandlingInstructionNowWithExistingCount() {
        misfireInstruction = SimpleTrigger.MISFIRE_INSTRUCTION_RESCHEDULE_NOW_WITH_EXISTING_REPEAT_COUNT;
        return this;
    }

    /**
     * 将startTime设置当前时间,立即重新调度任务,会忽略已经MisFire的任务
     */
    public SimpleScheduleBuilder withMisfireHandlingInstructionNowWithRemainingCount() {
        misfireInstruction = SimpleTrigger.MISFIRE_INSTRUCTION_RESCHEDULE_NOW_WITH_REMAINING_REPEAT_COUNT;
        return this;
    }

稍微解释下为什么会出现MisFire的任务,比如触发器设定每3秒钟触发一次,但是工作需要10秒钟的执行时间.因此,在一次任务结束执行前,触发器已经错失触发。

如果是只执行一次的调度,使用MISFIRE_INSTRUCTION_FIRE_NOW,如果是无限次的调度(repeatCount是无限的),使用MISFIRE_INSTRUCTION_RESCHEDULE_NEXT_WITH_REMAINING_COUNT否则,使用MISFIRE_INSTRUCTION_RESCHEDULE_NOW_WITH_EXISTING_REPEAT_COUNT!

      &#160咱们用一个简单的实例来说明SimpleTrigger的使用。比如咱们定义一个触发器,每隔30S执行一次,总共执行100c次。代码如下:

            // 使用simpleTrigger规则, 任务每隔30S执行一次,执行100次
            SimpleTrigger trigger = TriggerBuilder
                    .newTrigger()
                    .withIdentity(jobName, jobGroupName)
                    .withSchedule(SimpleScheduleBuilder.repeatSecondlyForTotalCount(30, 100))
                    .startNow()
                    .build();

1.3.2 CronTrigger

       CronTrigger使用Cron表达是来定义任务的触发时间。相对来说比较灵活,对于复杂的业务需求来说更加的实用。关于Cron表达式的使用,这里咱们就不讲了。大家可以自行去搜下。CronTrigger由CronScheduleBuilder构建而成。那咱们就看下CronScheduleBuilder里面主要函数的意义:

    /**
     * 创建CronScheduleBuilder对象,设置Cron表达式 如果Cron表达式解析异常抛RuntimeException
     */
    public static CronScheduleBuilder cronSchedule(String cronExpression);

    /**
     * 创建CronScheduleBuilder对象,设置Cron表达式 如果Cron表达式解析异常需要自己处理
     */
    public static CronScheduleBuilder cronScheduleNonvalidatedExpression(
            String cronExpression) throws ParseException;

    /**
     * 创建CronScheduleBuilder对象,参数是CronExpression
     */
    public static CronScheduleBuilder cronSchedule(CronExpression cronExpression);

    /**
     * 每天的hour时,minute分执行任务
     */
    public static CronScheduleBuilder dailyAtHourAndMinute(int hour, int minute);

    /**
     * 每个礼拜的哪几天(daysOfWeek)的hour时minute分执行任务
     */

    public static CronScheduleBuilder atHourAndMinuteOnGivenDaysOfWeek(
            int hour, int minute, Integer... daysOfWeek);

    /**
     * 每个礼拜的dayOfWeek,的hour时minute分执行任务
     */
    public static CronScheduleBuilder weeklyOnDayAndHourAndMinute(
            int dayOfWeek, int hour, int minute);

    /**
     * 每个月的那天(dayOfMonth)的hour时minure分执行任务
     */
    public static CronScheduleBuilder monthlyOnDayAndHourAndMinute(
            int dayOfMonth, int hour, int minute);

    /**
     * 设置时区
     */
    public CronScheduleBuilder inTimeZone(TimeZone timezone);

    /**
     * 这个不是忽略已经错失的触发的意思,而是说忽略MisFire策略。它会在资源合适的时候,重新触发所有的MisFire任务,并且不会影响现有的调度时间。
     */
    public CronScheduleBuilder withMisfireHandlingInstructionIgnoreMisfires() {
        misfireInstruction = Trigger.MISFIRE_INSTRUCTION_IGNORE_MISFIRE_POLICY;
        return this;
    }

    /**
     * 不对MisFire的任务做任何处理,错过了就是错过了
     */
    public CronScheduleBuilder withMisfireHandlingInstructionDoNothing() {
        misfireInstruction = CronTrigger.MISFIRE_INSTRUCTION_DO_NOTHING;
        return this;
    }

    /**
     * 针对MisFire的任务马上执行一次
     */
    public CronScheduleBuilder withMisfireHandlingInstructionFireAndProceed() {
        misfireInstruction = CronTrigger.MISFIRE_INSTRUCTION_FIRE_ONCE_NOW;
        return this;
    }

还是用两个简单的实例来看下怎么创建CronTrigger触发器。

  • 定义一个每天15:30执行的一个任务。代码如下:
            String cronExpression = String.format("0 %d %d ? * *", 15, 30);
            CronTrigger trigger = TriggerBuilder.newTrigger()
                    .withIdentity(jobName, jobGroupName)// 触发器名,触发器组
                    .withSchedule(CronScheduleBuilder.cronSchedule(cronExpression)) // CronScheduleBuilder
                    .build();
  • 定义一个每个礼拜星期六,15:30执行的任务,代码如下:
            CronTrigger trigger = TriggerBuilder.newTrigger()
                    .withIdentity(jobName, jobGroupName)// 触发器名,触发器组
                    //每个礼拜星期六的15"30执行
                    .withSchedule(CronScheduleBuilder.weeklyOnDayAndHourAndMinute(DateBuilder.SATURDAY, 15, 30)) 
                    .build();

1.3.3 DailyTimeIntervalTrigger

       DailyTimeIntervalTrigger指定每天的某个时间段内,以一定的时间间隔执行任务。并且它可以支持指定星期。DailyTimeIntervalTrigger用DailyTimeIntervalScheduleBuilder构建对象。DailyTimeIntervalScheduleBuilder常用方法如下:


    /**
     * 每次任务的间隔时间
     */
    public DailyTimeIntervalScheduleBuilder withInterval(int timeInterval, DateBuilder.IntervalUnit unit);

    /**
     * 每次任务的间隔时间 -- 单位秒
     */
    public DailyTimeIntervalScheduleBuilder withIntervalInSeconds(int intervalInSeconds);

    /**
     * 每次任务的间隔时间 -- 单位分
     */
    public DailyTimeIntervalScheduleBuilder withIntervalInMinutes(int intervalInMinutes);

    /**
     * 每次任务的间隔时间 -- 单位小时
     */
    public DailyTimeIntervalScheduleBuilder withIntervalInHours(int intervalInHours) {
        withInterval(intervalInHours, DateBuilder.IntervalUnit.HOUR);
        return this;
    }

    /**
     * 每周的哪几天执行
     */
    public DailyTimeIntervalScheduleBuilder onDaysOfTheWeek(Set<Integer> onDaysOfWeek);

    /**
     * 每周的哪几天执行
     */
    public DailyTimeIntervalScheduleBuilder onDaysOfTheWeek(Integer ... onDaysOfWeek);

    /**
     * 周一到周五每天都执行
     */
    public DailyTimeIntervalScheduleBuilder onMondayThroughFriday();

    /**
     * 星期六和星期天每天执行
     */
    public DailyTimeIntervalScheduleBuilder onSaturdayAndSunday();

    /**
     * 每天执行
     */
    public DailyTimeIntervalScheduleBuilder onEveryDay();

    /**
     * 任务什么时候开始(时,分,秒),比如可以设置8:30开始
     */
    public DailyTimeIntervalScheduleBuilder startingDailyAt(TimeOfDay timeOfDay);

    /**
     * 任务什么时候结束(时,分,秒),比如可以设置15:30结束
     */
    public DailyTimeIntervalScheduleBuilder endingDailyAt(TimeOfDay timeOfDay);

    /**
     * 在任务开始时间的和任务间隔的基础上,执行多少次。算的任务的结束时间
     * 比如开始时间是8:30  时间间隔是30分钟,设置2次。那么结束时间就是9:00
     */
    public DailyTimeIntervalScheduleBuilder endingDailyAfterCount(int count);

    /**
     * 这个不是忽略已经错失的触发的意思,而是说忽略MisFire策略。它会在资源合适的时候,重新触发所有的MisFire任务,并且不会影响现有的调度时间。
     */
    public DailyTimeIntervalScheduleBuilder withMisfireHandlingInstructionIgnoreMisfires() {
        misfireInstruction = Trigger.MISFIRE_INSTRUCTION_IGNORE_MISFIRE_POLICY;
        return this;
    }

    /**
     * 不对MisFire的任务做任何处理
     */
    public DailyTimeIntervalScheduleBuilder withMisfireHandlingInstructionDoNothing() {
        misfireInstruction = DailyTimeIntervalTrigger.MISFIRE_INSTRUCTION_DO_NOTHING;
        return this;
    }

    /**
     * 针对MisFire的任务马上执行一次
     */
    public DailyTimeIntervalScheduleBuilder withMisfireHandlingInstructionFireAndProceed() {
        misfireInstruction = CalendarIntervalTrigger.MISFIRE_INSTRUCTION_FIRE_ONCE_NOW;
        return this;
    }

    /**
     * 任务重复多少次
     */
    public DailyTimeIntervalScheduleBuilder withRepeatCount(int repeatCount);

同样我们定义一个简单的DailyTimeIntervalTrigger,周一到周五的每天8:00到17:00,每隔一个小时执行一次,执行100次。代码如下:

            // 周一到周五的每天8:00到17:00,每隔一个小时执行一次,执行100次
            DailyTimeIntervalScheduleBuilder daily = DailyTimeIntervalScheduleBuilder.dailyTimeIntervalSchedule()
                    .startingDailyAt(TimeOfDay.hourAndMinuteOfDay(8, 0)) //每天8:00开始
                    .endingDailyAt(TimeOfDay.hourAndMinuteOfDay(17, 0)) //17:00 结束
                    .onDaysOfTheWeek(DateBuilder.MONDAY,DateBuilder.TUESDAY,DateBuilder.WEDNESDAY,DateBuilder.THURSDAY,DateBuilder.FRIDAY) //周一至周五执行
                    .withIntervalInHours(1) //每间隔1小时执行一次
                    .withRepeatCount(100); //最多重复100次(实际执行100+1次)
            DailyTimeIntervalTrigger trigger = TriggerBuilder
                    .newTrigger()
                    .withIdentity(jobName, jobGroupName)
                    .withSchedule(daily)
                    .build();

1.3.4 CalendarIntervalTrigger

       CalendarIntervalTrigger日历触发器,执行的周期已日历为间隔。CalendarIntervalTrigger对象由CalendarIntervalScheduleBuilder构建而成。CalendarIntervalScheduleBuilder常用方法介绍如下:

    /**
     * 每次任务执行间隔
     */
    public CalendarIntervalScheduleBuilder withInterval(int timeInterval, DateBuilder.IntervalUnit unit);

    /**
     * 每次任务执行间隔 -- 单位秒
     */
    public CalendarIntervalScheduleBuilder withIntervalInSeconds(int intervalInSeconds);

    /**
     * 每次任务执行间隔 -- 单位分钟
     */
    public CalendarIntervalScheduleBuilder withIntervalInMinutes(int intervalInMinutes);

    /**
     * 每次任务执行间隔 -- 单位小时
     */
    public CalendarIntervalScheduleBuilder withIntervalInHours(int intervalInHours);

    /**
     * 每次任务执行间隔 -- 单位天
     */
    public CalendarIntervalScheduleBuilder withIntervalInDays(int intervalInDays);

    /**
     * 每次任务执行间隔 -- 单位周
     */
    public CalendarIntervalScheduleBuilder withIntervalInWeeks(int intervalInWeeks);

    /**
     * 每次任务执行间隔 -- 单位月
     */
    public CalendarIntervalScheduleBuilder withIntervalInMonths(int intervalInMonths);

    /**
     * 每次任务执行间隔 -- 单位年
     */
    public CalendarIntervalScheduleBuilder withIntervalInYears(int intervalInYears);

    /**
     * 这个不是忽略已经错失的触发的意思,而是说忽略MisFire策略。它会在资源合适的时候,重新触发所有的MisFire任务,并且不会影响现有的调度时间。
     */
    public CalendarIntervalScheduleBuilder withMisfireHandlingInstructionIgnoreMisfires() {
        misfireInstruction = Trigger.MISFIRE_INSTRUCTION_IGNORE_MISFIRE_POLICY;
        return this;
    }

    /**
     * 不对MisFire任务做任何处理,错过了就是错过了
     */
    public CalendarIntervalScheduleBuilder withMisfireHandlingInstructionDoNothing() {
        misfireInstruction = CalendarIntervalTrigger.MISFIRE_INSTRUCTION_DO_NOTHING;
        return this;
    }

    /**
     * 针对MisFired的任务马上执行一次
     */
    public CalendarIntervalScheduleBuilder withMisfireHandlingInstructionFireAndProceed() {
        misfireInstruction = CalendarIntervalTrigger.MISFIRE_INSTRUCTION_FIRE_ONCE_NOW;
        return this;
    }
    /**
     * 设置时区
     */
    public CalendarIntervalScheduleBuilder inTimeZone(TimeZone timezone);

    /**
     * 如果间隔为天或更大,该属性才有效。
     * 我们用一个例子来说明。比如有一个循环任务每天8:30执行。如果设置了该属性为true, Quartz能确保每天的8:30执行。
     * 因为有的时候夏令时转换的时候时间改变了,可能下次就不是8:30执行了
     */
    public CalendarIntervalScheduleBuilder preserveHourOfDayAcrossDaylightSavings(boolean preserveHourOfDay) {
        this.preserveHourOfDayAcrossDaylightSavings = preserveHourOfDay;
        return this;
    }

    /**
     * 如果间隔为天或更大,并且PreserveHourofDayAcrossDayLightSavings属性设置为true,该属性才有效
     * 并且在触发器将触发的给定日期上不存在一天中的小时,则将跳过该天,如果该属性设置为true,则触发器将高级一个第二间隔
     */
    public CalendarIntervalScheduleBuilder skipDayIfHourDoesNotExist(boolean skipDay) {
        this.skipDayIfHourDoesNotExist = skipDay;
        return this;
    }

同样的,咱们还是以一个简单的实例来说明下CalendarIntervalTrigger的使用。定义一个15::30开始,每隔一个星期执行一次的任务。代码如下:

            // 15:30开始,每隔一个星期执行一次的任务
            // 15:30
            Calendar c = Calendar.getInstance();
            c.setTime(new Date());
            c.setLenient(true);
            c.set(Calendar.HOUR_OF_DAY, 15);
            c.set(Calendar.MINUTE, 30);

            CalendarIntervalTrigger trigger = TriggerBuilder.newTrigger()
                    .withIdentity(jobName, jobGroupName)// 触发器名,触发器组
                    .startAt(c.getTime())
                    .withSchedule(CalendarIntervalScheduleBuilder.calendarIntervalSchedule().withIntervalInWeeks(1)) //每隔星期执行
                    .build();

       以上就是四种触发的大概介绍,每种触发器适用的场景都不一样。要是这四种触发器都不满足你的需求,那估计就得想办法自定义一个触发器了。自定义触发器的内容太高级了,咱就不扯这个了。

1.4 Scheduler

       Scheduler调度器,是Quartz框架的心脏,用来管理Trigger和Job,并保证Job能在Trigger设置的时间被触发执行。一般情况下调度器启动之后,咱们不需要做任何处理。Scheduler主要函数介绍如下:(其实很多都是看名字就大概指定每个函数的作用)

    /**
     * 方法获取的是正在执行的Job
     */
    List<JobExecutionContext> getCurrentlyExecutingJobs() throws SchedulerException;



    /**
     * 获取Scheduler上的监听器ListenerManager, 比如可以监听job,trigger添加移除的状态等
     */
    ListenerManager getListenerManager()  throws SchedulerException;

    /**
     * 把jobDetail添加到调度系统中,并且把任务和Trigger关联起来
     */
    Date scheduleJob(JobDetail jobDetail, Trigger trigger)
            throws SchedulerException;

    /**
     * 开始调度Trigger关联的job
     */
    Date scheduleJob(Trigger trigger) throws SchedulerException;

    /**
     * 开始调度job,同时设置多个
     */
    void scheduleJobs(Map<JobDetail, Set<? extends Trigger>> triggersAndJobs, boolean replace) throws SchedulerException;

    /**
     * 开始调度job,而且这个job可以关联一个或多个触发器Trigger
     */
    void scheduleJob(JobDetail jobDetail, Set<? extends Trigger> triggersForJob, boolean replace) throws SchedulerException;

    /**
     * 从触发器中移除Trigger(Trigger对应的任务会被移除掉)
     */
    boolean unscheduleJob(TriggerKey triggerKey)
            throws SchedulerException;

    /**
     * 从触发器中移除Trigger(Trigger对应的任务会被移除掉)
     */
    boolean unscheduleJobs(List<TriggerKey> triggerKeys)
            throws SchedulerException;

    /**
     * 移除triggerKey,添加newTrigger
     */
    Date rescheduleJob(TriggerKey triggerKey, Trigger newTrigger)
            throws SchedulerException;

    /**
     * 添加job到触发器中,当然这个时候任务是不会执行的,触发关联到了触发器Trigger上
     */
    void addJob(JobDetail jobDetail, boolean replace)
            throws SchedulerException;

    /**
     * 添加任务
     */
    void addJob(JobDetail jobDetail, boolean replace, boolean storeNonDurableWhileAwaitingScheduling)
            throws SchedulerException;

    /**
     * 删除任务
     */
    boolean deleteJob(JobKey jobKey)
            throws SchedulerException;

    /**
     * 删除任务
     */
    boolean deleteJobs(List<JobKey> jobKeys)
            throws SchedulerException;

    /**
     * 立即执行任务
     */
    void triggerJob(JobKey jobKey)
            throws SchedulerException;

    /**
     * 立即执行任务
     */
    void triggerJob(JobKey jobKey, JobDataMap data)
            throws SchedulerException;

    /**
     * 暂停任务
     */
    void pauseJob(JobKey jobKey)
            throws SchedulerException;

    /**
     * 暂停任务
     */
    void pauseJobs(GroupMatcher<JobKey> matcher) throws SchedulerException;

    /**
     * 暂停触发器对应的任务
     */
    void pauseTrigger(TriggerKey triggerKey)
            throws SchedulerException;

    /**
     * 暂停触发器对应的任务
     */
    void pauseTriggers(GroupMatcher<TriggerKey> matcher) throws SchedulerException;

    /**
     * 恢复任务
     */
    void resumeJob(JobKey jobKey)
            throws SchedulerException;

    /**
     * 恢复任务
     */
    void resumeJobs(GroupMatcher<JobKey> matcher) throws SchedulerException;

    /**
     * 恢复触发器对应的任务
     */
    void resumeTrigger(TriggerKey triggerKey)
            throws SchedulerException;

    /**
     * 恢复触发器对应的任务
     */
    void resumeTriggers(GroupMatcher<TriggerKey> matcher) throws SchedulerException;

    /**
     * 暂停所有的任务
     */
    void pauseAll() throws SchedulerException;

    /**
     * 恢复所有的任务
     */
    void resumeAll() throws SchedulerException;

    /**
     * 获取所有任务的jobGroup名字
     */
    List<String> getJobGroupNames() throws SchedulerException;

    /**
     * 获取jobKey
     */
    Set<JobKey> getJobKeys(GroupMatcher<JobKey> matcher) throws SchedulerException;

    /**
     * 获取任务对应的触发器Trigger
     *
     */
    List<? extends Trigger> getTriggersOfJob(JobKey jobKey)
            throws SchedulerException;

    /**
     * 获取所有触发器的Group name
     */
    List<String> getTriggerGroupNames() throws SchedulerException;

    /**
     * 获取TriggerKey
     */
    Set<TriggerKey> getTriggerKeys(GroupMatcher<TriggerKey> matcher) throws SchedulerException;

    /**
     * 获取所有暂停任务对应的触发器的Group Name
     */
    Set<String> getPausedTriggerGroups() throws SchedulerException;

    /**
     * 获取JobDetail
     *
     */
    JobDetail getJobDetail(JobKey jobKey)
            throws SchedulerException;

    /**
     * 获取触发器Trigger
     */
    Trigger getTrigger(TriggerKey triggerKey)
            throws SchedulerException;

    /**
     * 获取触发器的状态
     */
    Trigger.TriggerState getTriggerState(TriggerKey triggerKey)
            throws SchedulerException;

    /**
     * 恢复触发器的状态
     */
    void resetTriggerFromErrorState(TriggerKey triggerKey)
            throws SchedulerException;
    /**
     * 添加Calendar
     * 这里稍微解释下Calendar:Quartz的Calendar可以用于排除一些特定的日期不执行任务
     */
    void addCalendar(String calName, Calendar calendar, boolean replace, boolean updateTriggers)
            throws SchedulerException;

    /**
     * 删除Calendar
     */
    boolean deleteCalendar(String calName) throws SchedulerException;

    /**
     * 获取Calendar
     */
    Calendar getCalendar(String calName) throws SchedulerException;

    /**
     * 获取Calendar对应的名字
     */
    List<String> getCalendarNames() throws SchedulerException;

    /**
     * 中断某个任务
     */
    boolean interrupt(JobKey jobKey) throws UnableToInterruptJobException;

    /**
     * 中断任务
     * JobExecutionContext#getFireInstanceId()
     */
    boolean interrupt(String fireInstanceId) throws UnableToInterruptJobException;

    /**
     * 判断对应job是否存在
     */
    boolean checkExists(JobKey jobKey) throws SchedulerException;

    /**
     * 判断对应触发器是否存在
     */
    boolean checkExists(TriggerKey triggerKey) throws SchedulerException;

二 Spring Boot里面使用Quartz框架

       下面咱们简单介绍下Spring Boot工程下怎么Quartz框架的使用。

  • 第一步,先引入Quartz框架
        <!-- quartz -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-quartz</artifactId>
        </dependency>
  • 第二步,配置Quartz运行需要的一些参数,application.yml文件里面配置。每个参数对应的值大家可以根据自己的实际情况修改。

Quartz框架里面的某些功能是需要数据库的。所以配置文件里面还需要有数据库相关的配置,关于数据库datasource相关的配置(url,username,password,driver-class-name啥啥的)我们就不讲了。这里我们大概展示下Quartz相关的配置。

spring:
  quartz:
    #相关属性配置
    properties:
      org:
        quartz:
          scheduler:
            instanceName: clusteredScheduler
            instanceId: AUTO
          jobStore:
            class: org.quartz.impl.jdbcjobstore.JobStoreTX
            driverDelegateClass: org.quartz.impl.jdbcjobstore.StdJDBCDelegate
            tablePrefix: QRTZ_
            isClustered: true
            clusterCheckinInterval: 10000
            useProperties: false
          threadPool:
            class: org.quartz.simpl.SimpleThreadPool
            threadCount: 10
            threadPriority: 5
            threadsInheritContextClassLoaderOfInitializingThread: true
    #数据库方式
    job-store-type: jdbc
  • 第三步,创建Quartz需要的11张表(这是Quartz双机热备的基础)。Quartz相关建表语句在org.quartz.impl.jdbcjobstore包下都能找到,里面有各种各样数据库的建表语句。比如实例中我们用的是sql,innodb引擎。所以对应的建表语句文件为tables_mysql_innodb.sql文件,创表语句如下:
#
# In your Quartz properties file, you'll need to set
# org.quartz.jobStore.driverDelegateClass = org.quartz.impl.jdbcjobstore.StdJDBCDelegate
#
#
# By: Ron Cordell - roncordell
#  I didn't see this anywhere, so I thought I'd post it here. This is the script from Quartz to create the tables in a MySQL database, modified to use INNODB instead of MYISAM.

DROP TABLE IF EXISTS QRTZ_FIRED_TRIGGERS;
DROP TABLE IF EXISTS QRTZ_PAUSED_TRIGGER_GRPS;
DROP TABLE IF EXISTS QRTZ_SCHEDULER_STATE;
DROP TABLE IF EXISTS QRTZ_LOCKS;
DROP TABLE IF EXISTS QRTZ_SIMPLE_TRIGGERS;
DROP TABLE IF EXISTS QRTZ_SIMPROP_TRIGGERS;
DROP TABLE IF EXISTS QRTZ_CRON_TRIGGERS;
DROP TABLE IF EXISTS QRTZ_BLOB_TRIGGERS;
DROP TABLE IF EXISTS QRTZ_TRIGGERS;
DROP TABLE IF EXISTS QRTZ_JOB_DETAILS;
DROP TABLE IF EXISTS QRTZ_CALENDARS;

CREATE TABLE QRTZ_JOB_DETAILS(
SCHED_NAME VARCHAR(120) NOT NULL,
JOB_NAME VARCHAR(190) NOT NULL,
JOB_GROUP VARCHAR(190) NOT NULL,
DESCRIPTION VARCHAR(250) NULL,
JOB_CLASS_NAME VARCHAR(250) NOT NULL,
IS_DURABLE VARCHAR(1) NOT NULL,
IS_NONCONCURRENT VARCHAR(1) NOT NULL,
IS_UPDATE_DATA VARCHAR(1) NOT NULL,
REQUESTS_RECOVERY VARCHAR(1) NOT NULL,
JOB_DATA BLOB NULL,
PRIMARY KEY (SCHED_NAME,JOB_NAME,JOB_GROUP))
ENGINE=InnoDB;

CREATE TABLE QRTZ_TRIGGERS (
SCHED_NAME VARCHAR(120) NOT NULL,
TRIGGER_NAME VARCHAR(190) NOT NULL,
TRIGGER_GROUP VARCHAR(190) NOT NULL,
JOB_NAME VARCHAR(190) NOT NULL,
JOB_GROUP VARCHAR(190) NOT NULL,
DESCRIPTION VARCHAR(250) NULL,
NEXT_FIRE_TIME BIGINT(13) NULL,
PREV_FIRE_TIME BIGINT(13) NULL,
PRIORITY INTEGER NULL,
TRIGGER_STATE VARCHAR(16) NOT NULL,
TRIGGER_TYPE VARCHAR(8) NOT NULL,
START_TIME BIGINT(13) NOT NULL,
END_TIME BIGINT(13) NULL,
CALENDAR_NAME VARCHAR(190) NULL,
MISFIRE_INSTR SMALLINT(2) NULL,
JOB_DATA BLOB NULL,
PRIMARY KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP),
FOREIGN KEY (SCHED_NAME,JOB_NAME,JOB_GROUP)
REFERENCES QRTZ_JOB_DETAILS(SCHED_NAME,JOB_NAME,JOB_GROUP))
ENGINE=InnoDB;

CREATE TABLE QRTZ_SIMPLE_TRIGGERS (
SCHED_NAME VARCHAR(120) NOT NULL,
TRIGGER_NAME VARCHAR(190) NOT NULL,
TRIGGER_GROUP VARCHAR(190) NOT NULL,
REPEAT_COUNT BIGINT(7) NOT NULL,
REPEAT_INTERVAL BIGINT(12) NOT NULL,
TIMES_TRIGGERED BIGINT(10) NOT NULL,
PRIMARY KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP),
FOREIGN KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP)
REFERENCES QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP))
ENGINE=InnoDB;

CREATE TABLE QRTZ_CRON_TRIGGERS (
SCHED_NAME VARCHAR(120) NOT NULL,
TRIGGER_NAME VARCHAR(190) NOT NULL,
TRIGGER_GROUP VARCHAR(190) NOT NULL,
CRON_EXPRESSION VARCHAR(120) NOT NULL,
TIME_ZONE_ID VARCHAR(80),
PRIMARY KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP),
FOREIGN KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP)
REFERENCES QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP))
ENGINE=InnoDB;

CREATE TABLE QRTZ_SIMPROP_TRIGGERS
  (
    SCHED_NAME VARCHAR(120) NOT NULL,
    TRIGGER_NAME VARCHAR(190) NOT NULL,
    TRIGGER_GROUP VARCHAR(190) NOT NULL,
    STR_PROP_1 VARCHAR(512) NULL,
    STR_PROP_2 VARCHAR(512) NULL,
    STR_PROP_3 VARCHAR(512) NULL,
    INT_PROP_1 INT NULL,
    INT_PROP_2 INT NULL,
    LONG_PROP_1 BIGINT NULL,
    LONG_PROP_2 BIGINT NULL,
    DEC_PROP_1 NUMERIC(13,4) NULL,
    DEC_PROP_2 NUMERIC(13,4) NULL,
    BOOL_PROP_1 VARCHAR(1) NULL,
    BOOL_PROP_2 VARCHAR(1) NULL,
    PRIMARY KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP),
    FOREIGN KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP)
    REFERENCES QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP))
ENGINE=InnoDB;

CREATE TABLE QRTZ_BLOB_TRIGGERS (
SCHED_NAME VARCHAR(120) NOT NULL,
TRIGGER_NAME VARCHAR(190) NOT NULL,
TRIGGER_GROUP VARCHAR(190) NOT NULL,
BLOB_DATA BLOB NULL,
PRIMARY KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP),
INDEX (SCHED_NAME,TRIGGER_NAME, TRIGGER_GROUP),
FOREIGN KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP)
REFERENCES QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP))
ENGINE=InnoDB;

CREATE TABLE QRTZ_CALENDARS (
SCHED_NAME VARCHAR(120) NOT NULL,
CALENDAR_NAME VARCHAR(190) NOT NULL,
CALENDAR BLOB NOT NULL,
PRIMARY KEY (SCHED_NAME,CALENDAR_NAME))
ENGINE=InnoDB;

CREATE TABLE QRTZ_PAUSED_TRIGGER_GRPS (
SCHED_NAME VARCHAR(120) NOT NULL,
TRIGGER_GROUP VARCHAR(190) NOT NULL,
PRIMARY KEY (SCHED_NAME,TRIGGER_GROUP))
ENGINE=InnoDB;

CREATE TABLE QRTZ_FIRED_TRIGGERS (
SCHED_NAME VARCHAR(120) NOT NULL,
ENTRY_ID VARCHAR(95) NOT NULL,
TRIGGER_NAME VARCHAR(190) NOT NULL,
TRIGGER_GROUP VARCHAR(190) NOT NULL,
INSTANCE_NAME VARCHAR(190) NOT NULL,
FIRED_TIME BIGINT(13) NOT NULL,
SCHED_TIME BIGINT(13) NOT NULL,
PRIORITY INTEGER NOT NULL,
STATE VARCHAR(16) NOT NULL,
JOB_NAME VARCHAR(190) NULL,
JOB_GROUP VARCHAR(190) NULL,
IS_NONCONCURRENT VARCHAR(1) NULL,
REQUESTS_RECOVERY VARCHAR(1) NULL,
PRIMARY KEY (SCHED_NAME,ENTRY_ID))
ENGINE=InnoDB;

CREATE TABLE QRTZ_SCHEDULER_STATE (
SCHED_NAME VARCHAR(120) NOT NULL,
INSTANCE_NAME VARCHAR(190) NOT NULL,
LAST_CHECKIN_TIME BIGINT(13) NOT NULL,
CHECKIN_INTERVAL BIGINT(13) NOT NULL,
PRIMARY KEY (SCHED_NAME,INSTANCE_NAME))
ENGINE=InnoDB;

CREATE TABLE QRTZ_LOCKS (
SCHED_NAME VARCHAR(120) NOT NULL,
LOCK_NAME VARCHAR(40) NOT NULL,
PRIMARY KEY (SCHED_NAME,LOCK_NAME))
ENGINE=InnoDB;

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);

commit;

Quartz框架需要依赖11张表,关于怎么操作这些表,我们不需要知道Quartz内部已经做好了。咱们大概了解下每个表含义。如下:

表名 解释
QRTZ_CALENDARS 以 Blob 类型存储 Quartz 的 Calendar 信息
QRTZ_CRON_TRIGGERS 存储 Cron Trigger,包括 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 类型存储(用于 Quartz 用户用 JDBC 创建他们自己定制的 Trigger 类型,JobStore 并不知道如何存储实例的时候)
QRTZ_TRIGGER_LISTENERS 存储已配置的 TriggerListener 的信息
QRTZ_TRIGGERS 存储已配置的 Trigger 的信息
  • 第四步,具体的实现逻辑。简单的写了个Demo,Demo链接地址 https://github.com/tuacy/microservice-framework。代码在module quartz-manage 里面的代码。ps:如果你的代码跑步起来,第一可以看下是不是数据库的问题修改下数据库相关的配置。第二,你是否执行了建表语句。简单的demo,里面的代码也比较粗糙。

三 Quartz Calendar

       注意是Quartz的Calendar 对象并不是Java API里面的 java.util.Calendar。他们不一样。Quartz里面的Calendar主要用来屏蔽一个时间区间,使Trigger 在这个区间中不被触发。比如咱们是做政府项目的,政府项目么,咱们知道政府工作单位节假日和双休日是不上班的的。那么咱们可能就有需要系统里面所有的任务在节假日或者双休日都不被触发。这个时候Quartz Calendar就排上大用场了。Quartz里面的Calendar通过Scheduler来注册。Quartz为了方便大家的使用也给提供了好几种Calendar,大家可以根据实际情况使用。类图如下所示。

关于Calendar怎么设置,上面Scheduler常用函数里面有。

quartz (2).png
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,425评论 4 361
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,058评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,186评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,848评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,249评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,554评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,830评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,536评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,239评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,505评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,004评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,346评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,999评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,060评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,821评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,574评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,480评论 2 267

推荐阅读更多精彩内容