Skip to content

Quartz

相关概念

Quartz是一个任务调度框架 Job 表示一个工作,要执行的具体内容 JobDetail 表示一个具体的可执行的调度程序,Job 是这个可执行程调度程序所要执行的内容,另外 JobDetail 还包含了这个任务调度的方案和策略 Trigger 代表一个调度参数的配置,什么时候去调。 Scheduler 代表一个调度容器,一个调度容器中可以注册多个 JobDetail 和 Trigger。当 Trigger 与 JobDetail 组合,就可以被 Scheduler 容器调度了。 JobBuilder 定义和创建JobDetail实例的接口; TriggerBuilder 定义和创建Trigger实例的接口;

使用示例

Demo

1.导依赖

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

2.配置以及配置类 application.yml

tuya:
  quartz:
    config: config/quartz-dev.properties

quartz-dev.properties

org.quartz.scheduler.instanceName = GlobalScheduler
org.quartz.threadPool.threadCount = 10
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.dataSource = globalJobDataSource

org.quartz.dataSource.globalJobDataSource.driver = com.mysql.cj.jdbc.Driver
org.quartz.dataSource.globalJobDataSource.URL = jdbc:mysql://xxx:xxx/xxx?useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&transformedBitIsBoolean=true&autoReconnect=true&failOverReadOnly=false
org.quartz.dataSource.globalJobDataSource.user = xxx
org.quartz.dataSource.globalJobDataSource.password = xxx
org.quartz.dataSource.globalJobDataSource.maxConnections = 5

AutowireBeanJobFactory(一般放启动类同级)


@Configuration
public class QuartzConfiguration {

  @Bean
  public AutowireBeanJobFactory autowireBeanJobFactory() {
    return new AutowireBeanJobFactory();
  }

  /**
   * attention:
   * Details:定义quartz调度工厂
   */
  @Bean(name = "globalScheduler")
  public SchedulerFactoryBean schedulerFactory() {
    SchedulerFactoryBean bean = new SchedulerFactoryBean();
    // Quartz中的job自动注入spring容器托管的对象
    bean.setJobFactory(autowireBeanJobFactory());
    // 任意一个已定义的Job会覆盖现在的Job
    bean.setOverwriteExistingJobs(true);
    // 延时启动,应用启动1秒后,定时器才开始启动
    bean.setStartupDelay(1);
    return bean;
  }
}

DruidConnectionProvider

package top.xinzhang0618.buge.core.quartz;

import com.alibaba.druid.pool.DruidDataSource;
import lombok.Getter;
import lombok.Setter;
import org.quartz.utils.ConnectionProvider;

import java.sql.Connection;
import java.sql.SQLException;

/**
 * @author xinzhang
 * @date 2020/8/20 15:05
 */
@Getter
@Setter
public class DruidConnectionProvider implements ConnectionProvider {

  private String driver;
  private String url;
  private String user;
  private String password;
  private String validationQuery;
  private int maxConnection;
  private int idleConnectionValidationSeconds;
  private boolean validateOnCheckout;

  private DruidDataSource datasource;

  @Override
  public Connection getConnection() throws SQLException {
    return datasource.getConnection();
  }

  @Override
  public void shutdown() throws SQLException {
    if (this.datasource != null) {
      datasource.close();
    }
  }

  @Override
  public void initialize() throws SQLException {
    if (this.url == null) {
      throw new SQLException("DBPool could not be created: DB URL cannot be null");
    }

    if (this.driver == null) {
      throw new SQLException("DBPool driver could not be created: DB driver class name cannot be null!");
    }

    if (this.maxConnection < 0) {
      throw new SQLException("DBPool maxConnectins could not be created: Max connections must be greater than zero!");
    }

    datasource = new DruidDataSource();
    datasource.setDriverClassName(this.driver);
    datasource.setUrl(this.url);
    datasource.setUsername(this.user);
    datasource.setPassword(this.password);
    datasource.setMaxActive(this.maxConnection);

    if (this.validationQuery != null) {
      datasource.setValidationQuery(this.validationQuery);
      if (!this.validateOnCheckout) {
        datasource.setTestOnReturn(true);
      } else {
        datasource.setTestOnBorrow(true);
      }
      datasource.setValidationQueryTimeout(this.idleConnectionValidationSeconds);
    }
  }

}

测试demo

@Autowired
private Scheduler globalScheduler;

@PostMapping("/addJob/{productPublishId}")
public void addJob(@PathVariable("productPublishId") Long productPublishId) throws SchedulerException {
  PublishProduct publishProduct = productPublishService.getByKey(productPublishId);
  JobKey jobKey = JobKey.jobKey("PUBLISH_PRODUCT_" + publishProduct.getPublishProductId());
  JobDetail jobDetail = JobBuilder.newJob(PublishJob.class)
      .withIdentity(jobKey).build();
  jobDetail.getJobDataMap().put("PUBLISH_ID", productPublishId);
  jobDetail.getJobDataMap().put("PUBLISH_TITLE", publishProduct.getPublishTitle());
  TriggerKey triggerKey = TriggerKey.triggerKey("PUBLISH_PRODUCT_" + publishProduct.getPublishProductId());
  CronTrigger trigger = TriggerBuilder.newTrigger().withIdentity(triggerKey).withSchedule(CronScheduleBuilder
      .cronSchedule("0 58 14 29 4 ? 2019"))
      .startNow()
      .build();
  globalScheduler.scheduleJob(jobDetail, trigger);

}

public static class PublishJob implements Job {

  @Override
  public void execute(JobExecutionContext context) throws JobExecutionException {
    long publishId = context.getJobDetail().getJobDataMap().getLong("PUBLISH_ID");
    String publishTitle = context.getJobDetail().getJobDataMap().getString("PUBLISH_TITLE");
    System.out.println("PublishJob------------>" + publishTitle + "  " + publishId);
  }
}

定时投放任务

/**
 * 定时投放
 */
private void timedPublish(PublishProduct publishProduct) {
  JobKey jobKey = JobKey.jobKey("PUBLISH_PRODUCT_" + publishProduct.getPublishProductId());
  JobDetail jobDetail = JobBuilder.newJob(PublishJob.class)
      .withIdentity(jobKey).build();
  jobDetail.getJobDataMap().put("PUBLISH_PRODUCT_ID", publishProduct.getPublishProductId());
  jobDetail.getJobDataMap().put("PUBLISH_TITLE", publishProduct.getPublishTitle());
  jobDetail.getJobDataMap().put("TENANT_ID", BizContext.getTenantId());
  jobDetail.getJobDataMap().put("OSS_SETTING", BizContext.getOssSetting());
  TriggerKey triggerKey = TriggerKey.triggerKey("PUBLISH_PRODUCT_" + publishProduct.getPublishProductId());
  LocalDateTime planTime = publishProduct.getPlanTime();
  DateTimeFormatter formatter = DateTimeFormatter.ofPattern("ss mm HH dd MM ? yyyy");
  String formatTimeStr = planTime.format(formatter);
  CronTrigger trigger = TriggerBuilder.newTrigger().withIdentity(triggerKey).withSchedule(CronScheduleBuilder
      .cronSchedule(formatTimeStr))
      .startNow()
      .build();
  try {
    globalScheduler.scheduleJob(jobDetail, trigger);
    publishProduct.setPublishStatus(PublishStatus.ON_PUBLISH);
    update(publishProduct);
    LOGGER.log(publishProduct.getPublishProductId(), ActionConstants.MODIFY);
  } catch (SchedulerException e) {
    publishLOGGER.error("定时投放失败", e);
    throw new BizException("定时投放失败", e);
  }
}
/**
 * 定时投放任务
 */
public class PublishJob implements Job {

  private static final Logger LOGGER = TuyaLoggerFactory.getLogger(PublishJob.class);

  @Autowired
  private PublishProductService publishProductService;

  @Override
  public void execute(JobExecutionContext context) throws JobExecutionException {
    long publishProductId = context.getJobDetail().getJobDataMap().getLong("PUBLISH_PRODUCT_ID");
    String publishTitle = context.getJobDetail().getJobDataMap().getString("PUBLISH_TITLE");
    BizContext.setTenantId(context.getJobDetail().getJobDataMap().getLong("TENANT_ID"));
    BizContext.setOssSetting((TenantOssSetting) context.getJobDetail().getJobDataMap().get("OSS_SETTING"));
    PublishProduct publishProduct = publishProductService.getByKey(publishProductId);
    publishProductService.goPublish(publishProduct);
    LOGGER.info("定时投放, publishProductId:{},publishTitle:{}", publishProductId, publishTitle);
  }
}

定时计费

JobConfiguration

@Configuration
public class JobConfiguration {

  private static final Logger LOGGER = TuyaLoggerFactory.getLogger(JobConfiguration.class);
  private static final String PRODUCT_IMAGE_DAILY_ACCOUNT = "PRODUCT_IMAGE_DAILY_ACCOUNT";
  @Autowired
  private Scheduler globalScheduler;
  @Value("${tuya.quarz.productImageDailyAccountJobCorn:0 30 23 * * ?}")
  private String productImageDailyAccountJobCorn;


  @PostConstruct
  public void init() {
    executeProductImageDailyAccountJob();
  }


  private void executeProductImageDailyAccountJob() {
    try {
      JobKey jobKey = JobKey.jobKey(PRODUCT_IMAGE_DAILY_ACCOUNT);
      if (globalScheduler.checkExists(jobKey)) {
        globalScheduler.deleteJob(jobKey);
      }
      JobDetail jobDetail = QuartzTool
        .buildJobDetail(PRODUCT_IMAGE_DAILY_ACCOUNT, ProductImageDailyAccountJob.class);
      CronTrigger trigger = QuartzTool
        .buildCronTrigger(PRODUCT_IMAGE_DAILY_ACCOUNT, productImageDailyAccountJobCorn);
      globalScheduler.scheduleJob(jobDetail, trigger);
    } catch (SchedulerException e) {
      LOGGER.error("定时计费异常, 异常信息: {}", e.getMessage());
    }
  }
}

ProductImageDailyAccountJob

public class ProductImageDailyAccountJob implements Job {

  private static final Logger LOGGER = TuyaLoggerFactory.getLogger(ProductImageDailyAccountJob.class);
  private static final String GENERATE_SUCCESS_TIMES = "GENERATE_SUCCESS_TIMES";
  private static final String SYSTEM = "SYSTEM";
  private static final String PRODUCT_BALANCE_NOTIFY_MAIL_TEMPLATE = "productBalanceNotifyMail.vm";
  @Autowired
  private RedisTemplate<String, Object> redisTemplate;
  @Autowired
  private TenantService tenantService;
  @Autowired
  private TenantSettingService tenantSettingService;
  @Autowired
  private StoreService storeService;
  @Autowired
  private ProductBillService productBillService;
  @Autowired
  private ProductBalanceService productBalanceService;
  @Autowired
  private JavaMailSender javaMailSender;
  @Value("${spring.mail.username}")
  private String sender;

  @Override
  public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
    List<Tenant> tenantList = tenantService.list(new TenantQuery());
    for (Tenant tenant : tenantList) {
      BizContext.setTenantId(tenant.getTenantId());
      int totalTimes = 0;
      Map<Long, Integer> entries = redisTemplate.<Long, Integer>opsForHash()
        .entries(tenant.getTenantId() + ":" + ProductType.PRODUCT_IMAGE + ":" + GENERATE_SUCCESS_TIMES);
      if (!Assert.isEmpty(entries)) {
        for (Long storeId : entries.keySet()) {
          String storeName = storeService.getByKey(storeId).getStoreName();
          ProductBill productBill = new ProductBill();
          productBill.setTenantId(tenant.getTenantId());
          productBill.setCreator(SYSTEM);
          productBill.setConsumer(storeName);
          productBill.setTimes(entries.get(storeId));
          totalTimes += entries.get(storeId);
          productBill.setProductType(ProductType.PRODUCT_IMAGE);
          productBill.setAccountPeriod(LocalDate.now());
          productBillService.create(productBill);
          redisTemplate.<Long, Integer>opsForHash()
            .delete(tenant.getTenantId() + ":" + ProductType.PRODUCT_IMAGE + ":" + GENERATE_SUCCESS_TIMES, storeId);
        }
        productBalanceService
          .reduceProductBalanceRemainTimesAndFreezeTimes(tenant.getTenantId(), ProductType.PRODUCT_IMAGE, totalTimes);
      }
    }
    List<ProductBalance> productBalanceList = productBalanceService.list(new ProductBalanceQuery());
    Set<Long> tenantIds = productBalanceList.stream()
      .filter(productBalance -> {
        TenantSetting eg = new TenantSetting();
        eg.setTenantId(productBalance.getTenantId());
        eg.setTenantSettingType(TenantSettingType.APPLICATION);
        String tenantSettingContent = tenantSettingService.getByExample(eg).getTenantSettingContent();
        Integer warningValue = JsonUtil.toJSONObject(tenantSettingContent).getInteger("warningValue");
        return productBalance.getRemainTimes() <= warningValue;
      })
      .map(ProductBalance::getTenantId).collect(Collectors.toSet());
    sendRemainTimesNotifyEmail(tenantIds);
  }

  /**
   * 发送剩余次数提醒邮件
   *
   * @param tenantIds 租户id列表
   */
  private void sendRemainTimesNotifyEmail(Set<Long> tenantIds) {
    try {
      for (Long tenantId : tenantIds) {
        TenantSetting eg = new TenantSetting();
        eg.setTenantSettingType(TenantSettingType.APPLICATION);
        eg.setTenantId(tenantId);
        TenantSetting tenantSetting = tenantSettingService.getByExample(eg);
        String email = JsonUtil.toJSONObject(tenantSetting.getTenantSettingContent()).getString("email");
        List<String> receivers = StringUtil.words(email);
        String[] toAddress = receivers.toArray(new String[receivers.size()]);
        String subject = "图鸦提醒邮件,产品余额不足,请及时充值!";
        String tenantName = tenantService.getByKey(tenantId).getTenantName();

        HashMap<String, Object> params = new HashMap<>(2);
        params.put("tenantName", tenantName);
        ProductBalanceQuery query = new ProductBalanceQuery();
        query.setTenantId(tenantId);
        List<ProductBalance> productBalanceList = productBalanceService.list(query);
        params.put("productBalanceList", productBalanceList);
        String text = VelocityUtil.generateStringByTemplate(PRODUCT_BALANCE_NOTIFY_MAIL_TEMPLATE, params);

        MimeMessage message = javaMailSender.createMimeMessage();
        MimeMessageHelper helper = new MimeMessageHelper(message, true, "UTF-8");
        helper.setFrom(sender);
        helper.setTo(toAddress);
        helper.setSubject(subject);
        helper.setText(text, true);
        javaMailSender.send(message);
      }
    } catch (MessagingException e) {
      LOGGER.error("发送图片生成剩余次数提醒邮件失败, 堆栈信息: {}", e);
    }
  }
}

QuarzTool

核心corn包的工具类参考 QuartzTool


public abstract class QuartzTool {
  public QuartzTool() {
  }

  public static Scheduler scheduler(String name, JobFactory jobFactory) throws Exception {
    SchedulerFactoryBean schedulerFactoryBean = new SchedulerFactoryBean();
    schedulerFactoryBean.setSchedulerName(name);
    schedulerFactoryBean.setJobFactory(jobFactory);
    schedulerFactoryBean.setAutoStartup(true);
    schedulerFactoryBean.afterPropertiesSet();
    schedulerFactoryBean.start();
    return schedulerFactoryBean.getObject();
  }

  public static JobDetail jobDetail(String name, Class<? extends Job> clazz) {
    JobDetailFactoryBean jobDetailFactoryBean = new JobDetailFactoryBean();
    jobDetailFactoryBean.setName(name);
    jobDetailFactoryBean.setJobClass(clazz);
    jobDetailFactoryBean.afterPropertiesSet();
    return jobDetailFactoryBean.getObject();
  }

  public static SimpleTrigger simpleTrigger(String name, int interval, JobDetail jobDetail) {
    SimpleTriggerFactoryBean simpleTriggerFactoryBean = new SimpleTriggerFactoryBean();
    simpleTriggerFactoryBean.setName(name);
    simpleTriggerFactoryBean.setJobDetail(jobDetail);
    simpleTriggerFactoryBean.setRepeatInterval((long)(interval * 1000));
    simpleTriggerFactoryBean.afterPropertiesSet();
    return simpleTriggerFactoryBean.getObject();
  }

  public static CronTrigger cronTrigger(String name, String cron, JobDetail jobDetail) throws ParseException {
    CronTriggerFactoryBean cronTriggerFactoryBean = new CronTriggerFactoryBean();
    cronTriggerFactoryBean.setName(name);
    cronTriggerFactoryBean.setJobDetail(jobDetail);
    cronTriggerFactoryBean.setCronExpression(cron);
    cronTriggerFactoryBean.afterPropertiesSet();
    return cronTriggerFactoryBean.getObject();
  }

  public static String buildCron(LocalDateTime time) {
    DateTimeFormatter formatter = DateTimeFormatter.ofPattern("ss mm HH dd MM ? yyyy");
    return time.format(formatter);
  }

  public static JobDetail buildJobDetail(String jobKey, Class<? extends Job> clazz) {
    return buildJobDetail(jobKey, clazz, (Map)null);
  }

  public static JobDetail buildJobDetail(String jobKey, Class<? extends Job> clazz, Map<String, Object> jobData) {
    JobBuilder jobBuilder = JobBuilder.newJob(clazz).withIdentity(jobKey);
    if (jobData != null) {
      jobBuilder.usingJobData(new JobDataMap(jobData));
    }

    return jobBuilder.build();
  }

  public static CronTrigger buildCronTrigger(String triggerKey, String cron) {
    return (CronTrigger)TriggerBuilder.newTrigger().withIdentity(triggerKey).withSchedule(CronScheduleBuilder.cronSchedule(cron)).startNow().build();
  }

  public static SimpleTrigger buildSimpleTrigger(String triggerKey, int seconds) {
    return (SimpleTrigger)TriggerBuilder.newTrigger().withIdentity(triggerKey).withSchedule(SimpleScheduleBuilder.simpleSchedule().withIntervalInSeconds(seconds).repeatForever()).startNow().build();
  }

  public static <T> T getJobData(JobExecutionContext jobExecutionContext, String key) {
    return jobExecutionContext.getJobDetail().getJobDataMap() != null && jobExecutionContext.getJobDetail().getJobDataMap().containsKey(key) ? jobExecutionContext.getJobDetail().getJobDataMap().get(key) : null;
  }
}

AutowireBeanJobFactory

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

  public AutowireBeanJobFactory() {
  }

  public void setApplicationContext(ApplicationContext context) {
    this.beanFactory = context.getAutowireCapableBeanFactory();
  }

  protected Object createJobInstance(TriggerFiredBundle bundle) throws Exception {
    Object job = super.createJobInstance(bundle);
    this.beanFactory.autowireBean(job);
    return job;
  }
}

工具类使用参考

String name = giftStrategy.getGiftStrategyId() + EXECUTE + execute.getGiftStrategyExecuteId();
    JobDetail jobDetail = QuartzTool
        .jobDetail(name, StaticStrategyExecuteJob.class);
    jobDetail.getJobDataMap().put(GIFT_STRATEGY_ID, giftStrategy.getGiftStrategyId());
    jobDetail.getJobDataMap().put(GIFT_STRATEGY_EXECUTE_ID, execute.getGiftStrategyExecuteId());
    SimpleTrigger simpleTrigger = QuartzTool.simpleTrigger(name, 0, jobDetail);
    try {
      globalScheduler.scheduleJob(jobDetail, simpleTrigger);
    } catch (SchedulerException e) {
      throw new OmsException("静态策略执行定时任务添加失败, giftStrategyId: {0}, GiftStrategyExecuteId: {1}",
          giftStrategy.getGiftStrategyId(), execute.getGiftStrategyExecuteId());
    }

quarz错过触发时机的处理

参考: https://yq.aliyun.com/articles/114262

触发器超时的情况

1.系统重启错过定时任务 2.Trigger被暂停时, 有任务被错过 3.线程池中的所有线程被占用, 导致misfire 4.有状态任务在下次触发时间到达时, 上次执行还没结束

#判定job为misfire的阈值,这里设置为4S
org.quartz.jobStore.misfireThreshold = 4000

调度器怎么处理超时

  1. timeout < misfireThreshold 超时的触发器(超时时间小于misfireThreshold)在获取到运行线程后,将会立即运行前面错过的作业job,然后按照前面制定的周期性任务正常运行。
  2. timeout >= misfireThreshold 调度器引擎为简单触发器SimpleTrigger和表达式CronTrigger提供了多种处理策略,我们可以在定义触发器时指定需要的策略。 2.1 对于SimpleTrigger的处理策略 ● MISFIRE_INSTRUCTION_FIRE_NOW : 调度引擎在MisFire的情况下,将任务(JOB)马上执行一次。需要注意的是 这个指令通常被用做只执行一次的Triggers,也就是没有重复的情况(non-repeating),如果这个Triggers的被安排的执行次数大于0。那么这个执行与 MISFIRE_INSTRUCTION_RESCHEDULE_NOW_WITH_REMAINING_REPEAT_COUNT 相同。 ● MISFIRE_INSTRUCTION_RESCHEDULE_NOW_WITH_EXISTING_REPEAT_COUNT: 调度引擎重新调度该任务,repeat count 保持不变,按照原有制定的执行方案执行repeat count次,但是,如果当前时间,已经晚于 end-time,那么这个触发器将不会再被触发。举个例子:比如一个触发器设置的时间是 10:00 执行时间间隔10秒 重复10次。那么当10:07秒的时候调度引擎可以执行这个触发器的任务,然后按照原有制定的时间间隔执行10次。但是如果触发器设置的执行时间是10:00,结束时间为10:10,由于种种原因导致该触发器在10:11分才能被调度引擎触发,这时,触发器将不会被触发了。 ● MISFIRE_INSTRUCTION_RESCHEDULE_NEXT_WITH_REMAINING_COUNT: 这个策略跟上面的 MISFIRE_INSTRUCTION_RESCHEDULE_NOW_WITH_EXISTING_REPEAT_COUNT 策略类似,唯一的区别就是调度器触发触发器的时间不是“现在” 而是下一个 scheduled time。 ● MISFIRE_INSTRUCTION_RESCHEDULE_NOW_WITH_REMAINING_REPEAT_COUNT: 这个策略跟上面的策略 MISFIRE_INSTRUCTION_RESCHEDULE_NOW_WITH_EXISTING_REPEAT_COUNT 比较类似,调度引擎重新调度该任务,repeat count 是剩余应该执行的次数,也就是说本来这个任务应该执行10次,但是已经错过了3次,那么这个任务就还会执行7次。 ● MISFIRE_INSTRUCTION_RESCHEDULE_NEXT_WITH_REMAINING_COUNT: 这个策略跟上面的 MISFIRE_INSTRUCTION_RESCHEDULE_NEXT_WITH_REMAINING_COUNT 策略类似,区别就是repeat count 是剩余应该执行的次数而不是全部的执行次数。比如一个任务应该在2:00执行,repeat count=5,时间间隔5秒, 但是在2:07才获得执行的机会,那任务不会立即执行,而是按照机会在2点10秒执行。 ● MISFIRE_INSTRUCTION_IGNORE_MISFIRE_POLICY: 这个策略跟上面的 MISFIRE_INSTRUCTION_RESCHEDULE_NOW_WITH_EXISTING_REPEAT_COUNT 策略类似,但这个策略是忽略所有的超时状态,快速执行之前错过的次数,然后再按照之前制定的周期触发触发器。举个例子,一个SimpleTrigger 每个15秒钟触发, 但是超时了5分钟才获得执行的机会,那么这个触发器会被快速连续调用20次, 追上前面落下的执行次数。 2.2 对于CronTrigger的处理策略 ● MISFIRE_INSTRUCTION_FIRE_ONCE_NOW: 指示触发器超时后会被立即安排执行。 ● MISFIRE_INSTRUCTION_DO_NOTHING: 这个策略与策略 MISFIRE_INSTRUCTION_FIRE_ONCE_NOW 正好相反,它不会被立即触发,而是获取下一个被触发的时间,并且如果下一个被触发的时间超出了end-time 那么触发器就不会被执行。