← 返回博客

Quartz 调度事务邮件 + Rovela(2026):JobDetail、CronTrigger 与按窗口幂等键

用 Java Quartz JobDetail / CronTrigger 对接 Rovela POST /emails:服务端 sending_access,按调度窗口 Idempotency-Key,适合摘要与运维报告。

更新于 2026年11月9日
Quartz 调度事务邮件 + Rovela(2026):JobDetail、CronTrigger 与按窗口幂等键

Quartz 负责调度 — Rovela 负责投递

Quartz 是 JVM 上经典的任务调度库。你声明 JobDetail(跑什么)和一个或多个 Trigger(何时跑)——常见是用 CronScheduleBuilder 构建的 CronTrigger。Scheduler 线程池触发到期任务;你的 Job#execute 带着 JobExecutionContext 运行,可用于键、JobDataMap 与触发时间(Quartz 文档)。

适合摘要、运维报告、试用到期提醒等——正文依赖触发时的应用/数据库实时状态,而不是能提前用 Rovela scheduled_at 冻住的内容。

你的 Quartz Job 调用 POST https://api.rovela.dev/emails 时,Rovela 负责排队、DKIM 签名与投递(Send Email API、Quick Start、Authentication)。Quartz 管节奏与「是否到期」;Job 管聚合、sending_access 密钥,以及按调度窗口稳定的 Idempotency-Key。Rovela 只负责投递。

本指南串联 Quartz CronTrigger → Job → Rovela:

  1. 用 JobBuilder / TriggerBuilder 声明显式 job key 与 cron(如 0 0 9 * * ?)
  2. 将 ROVELA_API_KEY(sending_access)、MAIL_FROM、JDBC/存储与收件人放在仅服务端配置
  3. 在 execute 内按当前窗口聚合摘要/报告
  4. 用窗口级 Idempotency-Key 调用 Rovela
  5. 在 misfire、重试与集群调度下避免重复发信

Rovela 不会创建 Quartz Job、JDBCJobStore 表或 Scheduler 线程。 Quartz 管 schedule → fire → execute;你的 Job 管查询 → 映射字段 → POST /emails;Rovela 管投递。

与下列文章角度不同:

也不同于一次性的 SimpleTrigger——本文只讲 cron 周期路径。

Quartz 调度什么,Rovela 发送什么

关注点归属
Job key、Trigger key、CRON、时区、misfireQuartz JobDetail + CronTrigger
RAMJobStore vs JDBCJobStore / 集群你的 Quartz 配置
线程池触发 → Job#executeQuartz Scheduler
重试 / Listener你的 Job + Quartz listeners
ROVELA_API_KEY(sending_access)JVM 进程环境或密钥库
按窗口查库 / 调 API你的 Job
渲染 HTML + text你的 Job
POST /emails、DKIM、排队、退信Rovela
Idempotency-Key = quartz-daily-digest:{yyyy-mm-dd}你的 Job

Rovela 返回 200 表示已接受并入队,不是「已进收件箱」。

影响发信任务的 Quartz 约束

  • Scheduler 必须常驻。
  • 使用显式 JobKey / TriggerKey。
  • Quartz cron 通常是六段或七段(含前导秒字段),与五段 Unix crontab 不同。
  • misfire 策略要明确。 JVM 在 09:00 宕机时,决定恢复后补发一次还是跳过。
  • 多节点用 JDBCJobStore 集群,否则两个 RAM 调度器可能双发。
  • sending_access 仅服务端。 不要把明文 Key 塞进未加密的 JobDataMap 或 Git。
  • 优先 HTTPS POST /emails。

何时用 Quartz + POST /emails(而不是 scheduled_at)

HTML 必须在触发时包含新鲜指标、已在 JVM 跑 Quartz、需要 misfire/集群控制时,用 Quartz cron → 即时发送。正文已知只需稍后投递时,用 Rovela scheduled_at。只要 Spring 注解 cron、不要完整 JobDetail API 时,见 Spring @Scheduled 邮件。

前置条件

  1. Rovela 已验证发信域名。
  2. re_… API Key,Worker 优先 sending_access。
  3. 带 Quartz 的 JVM 应用,且 Scheduler 已启动。
  4. 配置:ROVELA_API_KEY、MAIL_FROM、DIGEST_TO,集群时还有 JDBC。
  5. Quartz 可实例化的 Job 类。

注册 JobDetail + CronTrigger

<dependency>
  <groupId>org.quartz-scheduler</groupId>
  <artifactId>quartz</artifactId>
  <version>2.3.2</version>
</dependency>
JobDetail job = JobBuilder.newJob(DailyDigestEmailJob.class)
    .withIdentity("daily-digest-email", "mailers")
    .build();

Trigger trigger = TriggerBuilder.newTrigger()
    .withIdentity("daily-digest-email-trigger", "mailers")
    .withSchedule(CronScheduleBuilder
        .cronSchedule("0 0 9 * * ?")
        .inTimeZone(java.util.TimeZone.getTimeZone("UTC"))
        .withMisfireHandlingInstructionDoNothing())
    .build();

scheduler.scheduleJob(job, trigger);
scheduler.start();

调用 Rovela 的 Job

(完整 Java 示例与英文版相同:@DisallowConcurrentExecution、quartz-daily-digest:{day} 幂等键、HttpClient POST https://api.rovela.dev/emails,切勿记录 ROVELA_API_KEY。)

每 15 分钟运维脉搏:cron 0 */15 * * * ?,幂等键按一刻钟分桶。

常用 Quartz 六段 CRON:

表达式含义
0 0 9 * * ?每日 09:00:00
0 */15 * * * ?每 15 分钟
0 30 7 ? * MON周一 07:30
0 0 */3 * * ?每三小时

窗口级 Idempotency-Key

Rovela 的 Idempotency-Key(最长 256 字符)让 24 小时内相同请求重试返回原响应、不再发第二封。对 Quartz 很重要:重试会重入 execute;Rovela 已 200 但任务未完成时进程崩溃可能再投递;集群 failover / misfire 会让墙钟漂移——应按业务窗口键控。

推荐键:quartz-daily-digest:{yyyy-mm-dd}、quartz-ops-pulse:{yyyy-mm-dd}T{HH}-{MM}、quartz-tokyo-ops:{yyyy-mm-dd}、quartz-weekly-report:{yyyy}-W{ww}、quartz-tenant-digest:{tenantId}:{yyyy-mm-dd}。

配置密钥并验证调度

  1. 在运行 Scheduler 的进程设置 ROVELA_API_KEY(sending_access)、MAIL_FROM、DIGEST_TO。
  2. 确认 Trigger 的 cron 与时区。
  3. 确认 Scheduler 已 start;集群时检查 JDBCJobStore。
  4. 等下一次触发,或在非生产 triggerJob,保持窗口 key 稳定。
  5. 日志核对 idempotencyKey 与 Rovela emailId。

故障对照

现象可能原因处理
Cron 永不触发Scheduler 未启动启动并检查线程池
两节点双发两个 RAMJobStoreJDBCJobStore 集群
墙钟不对未设时区inTimeZone
恢复后重复摘要不稳定 key窗口级 key + 幂等
任务自重叠长任务 + 短 cron@DisallowConcurrentExecution
Rovela 401 / 400 / 429Key / 域名 / 限流修环境;保持同一幂等键重试

上线清单

  • 发信域名已验证;MAIL_FROM 匹配
  • sending_access 仅在 Scheduler 环境/密钥库
  • Scheduler 常驻;多节点已配置集群存储
  • 显式 JobKey + TriggerKey + cron + 时区 + misfire
  • 需要时启用 @DisallowConcurrentExecution
  • 窗口级 Idempotency-Key
  • 日志有 emailId 且不打印密钥
  • 连续失败 / Scheduler 宕机有告警

FAQ

调度器能不写任务方法直接调 Rovela 吗?没有实质意义——调度器只唤醒你的代码。聚合与 sending_access 必须在你的方法里。Rovela 不托管你的调度器或 Worker。

任务里还要用 scheduled_at 吗?通常不必。调度器已在发送时刻触发且正文是实时的,直接 POST /emails。若现在受理、稍后再投且不想再唤醒进程,才用 scheduled_at(定时发送邮件)。

sending_access 能做什么?足够 POST /emails / batch 入队。读信、改期、取消需要 full_access 或 Dashboard session。Worker 保持 sending_access。

200 等于创始人已收到摘要?否——只表示 Rovela 已接受入队。投递状态看 GET /emails/:id、events 或 webhook。

为何既要调度器重试又要 Idempotency-Key?调度器在代码失败时重试任务;Rovela 的 key 在同一窗口内 HTTP 成功两次(或超时但已接受)时合并重复发送。两层都要。

上线 Quartz 任务;让 Rovela 投递

用显式 key 与 cron 注册 JobDetail + CronTrigger,在 Job#execute 里做实时聚合与 POST /emails,把 Idempotency-Key 限定在调度窗口(及租户),并仅在服务端保存 sending_access。Quartz 按时唤醒;Rovela 投递事务邮件。