Quartz 调度事务邮件 + Rovela(2026):JobDetail、CronTrigger 与按窗口幂等键
用 Java Quartz JobDetail / CronTrigger 对接 Rovela POST /emails:服务端 sending_access,按调度窗口 Idempotency-Key,适合摘要与运维报告。
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:
- 用
JobBuilder/TriggerBuilder声明显式 job key 与 cron(如0 0 9 * * ?) - 将
ROVELA_API_KEY(sending_access)、MAIL_FROM、JDBC/存储与收件人放在仅服务端配置 - 在
execute内按当前窗口聚合摘要/报告 - 用窗口级
Idempotency-Key调用 Rovela - 在 misfire、重试与集群调度下避免重复发信
Rovela 不会创建 Quartz Job、JDBCJobStore 表或 Scheduler 线程。 Quartz 管 schedule → fire → execute;你的 Job 管查询 → 映射字段 → POST /emails;Rovela 管投递。
与下列文章角度不同:
- Spring @Scheduled 邮件 — Spring
@Scheduled注解 cron - Spring Boot OTP 邮件 API — 一次性验证码,不是周期摘要
- node-cron 事务邮件 — Node
node-cron进程内调度 - NestJS Schedule Cron 邮件 — NestJS
@nestjs/schedule@Cron - Laravel Task Scheduler 邮件 — Laravel
Schedule:: - APScheduler Cron 邮件 — Python APScheduler CronTrigger
- Resque-scheduler 邮件 — Ruby Resque-scheduler
- RQ Scheduler 邮件 — Python RQ Scheduler
- delayed_job 周期邮件 — Ruby delayed_job 周期模式
- Solid Queue 周期邮件 — Rails Solid Queue recurring.yml
- Hangfire Recurring 事务邮件 — .NET Hangfire
- Sidekiq-Cron 事务邮件 — Ruby Sidekiq-Cron
- Celery Beat 事务邮件 — Python Celery Beat
- Agenda.js 定时任务邮件 — Mongo
agenda - BullMQ 可重复任务邮件 — Redis BullMQ
- Temporal Schedule 邮件 — Temporal Schedule Spec
- Trigger.dev Cron 邮件 — Trigger.dev
schedules.task - Inngest Cron 事务邮件 — Inngest
cron - Upstash QStash Schedules 邮件 — QStash HTTPS schedule
- Supabase pg_cron 邮件 — Postgres
pg_cron - Kubernetes CronJob 邮件 — K8s CronJob
- Vercel Cron 摘要邮件 — 平台 cron
GET - 定时发送邮件 — 正文已知时用 Rovela
scheduled_at
也不同于一次性的 SimpleTrigger——本文只讲 cron 周期路径。
Quartz 调度什么,Rovela 发送什么
| 关注点 | 归属 |
|---|---|
| Job key、Trigger key、CRON、时区、misfire | Quartz JobDetail + CronTrigger |
| RAMJobStore vs JDBCJobStore / 集群 | 你的 Quartz 配置 |
线程池触发 → Job#execute | Quartz 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 邮件。
前置条件
- Rovela 已验证发信域名。
re_…API Key,Worker 优先sending_access。- 带 Quartz 的 JVM 应用,且
Scheduler已启动。 - 配置:
ROVELA_API_KEY、MAIL_FROM、DIGEST_TO,集群时还有 JDBC。 - 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}。
配置密钥并验证调度
- 在运行 Scheduler 的进程设置
ROVELA_API_KEY(sending_access)、MAIL_FROM、DIGEST_TO。 - 确认 Trigger 的 cron 与时区。
- 确认 Scheduler 已 start;集群时检查 JDBCJobStore。
- 等下一次触发,或在非生产
triggerJob,保持窗口 key 稳定。 - 日志核对
idempotencyKey与 RovelaemailId。
故障对照
| 现象 | 可能原因 | 处理 |
|---|---|---|
| Cron 永不触发 | Scheduler 未启动 | 启动并检查线程池 |
| 两节点双发 | 两个 RAMJobStore | JDBCJobStore 集群 |
| 墙钟不对 | 未设时区 | inTimeZone |
| 恢复后重复摘要 | 不稳定 key | 窗口级 key + 幂等 |
| 任务自重叠 | 长任务 + 短 cron | @DisallowConcurrentExecution |
Rovela 401 / 400 / 429 | Key / 域名 / 限流 | 修环境;保持同一幂等键重试 |
上线清单
- 发信域名已验证;
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 投递事务邮件。