验证码的目标是让用户及时完成验证;接口成功只是链路中的一步。
一、验证码七分钟后才到,问题不止是失败率
林悦把营销邮件和验证码放在同一个队列里。一次批量发送占住出口,验证码排了七分钟;用户反复点重发,后来收到几封邮件,却分不清哪一封仍有效。
这是教学情境,不是已核验的生产事故。它说明两个需要分别观察的问题:消息是否发送失败,以及即使未失败,是否已错过业务有效期。拆队列能减少排队竞争,但收件过滤、服务商限流与网络故障仍然可能影响结果。
二、先定义「到达」再设置目标
| 阶段 | 可记录什么 | 不能据此证明什么 |
|---|---|---|
| 业务事件与入队 | 事件 ID、入队时间、有效截止时间 | 尚未提交给邮件服务商 |
| 服务商接受 | 提交返回、服务商消息 ID | 不代表收件服务器已接受 |
| 收件服务器接受 | 服务商的 delivery 事件或 SMTP 结果 | 不代表进入收件箱或用户已读 |
| 用户完成动作 | 完成验证、下载凭证、明确反馈 | 没有完成动作不自动等于邮件未到 |
邮件服务商可给出收件服务器接受的观测;普通发送日志无法精确知道每封邮件何时进入用户收件箱。种子邮箱可辅助测试,但不代表全部真实用户。
以下是本例的初始工程目标,不是邮件协议保证或行业标准。统计起点统一为业务事件创建,终点为收件服务器首次接受;未完成与超时单独计数,不能只对成功样本算百分位后宣称所有请求达标。
| 类型 | 典型场景 | 本例观测目标 | 必须补充的检查 |
|---|---|---|---|
| 身份验证 | 验证码 | 10 秒内服务器接受比例 ≥95% | 验证完成率、验证码有效期 |
| 账号恢复 | 密码重置 | 60 秒内服务器接受比例 ≥95% | 链接是否仍有效、恢复完成率 |
| 交易凭证 | 收据、订阅确认 | 5 分钟内服务器接受比例 ≥95% | 站内可查凭证及补发入口 |
| 资金异常 | 支付失败、额度不足 | 15 分钟内服务器接受比例 ≥95% | 站内状态、必要时的适当替代提醒 |
事务与批量发送至少隔离队列和容量;子域名便于分类,但不会自动隔离共享 IP、账号限额与组织域名声誉。按地区、服务商与消息类型查看分布,同时保留一致的业务目标;不能为消除告警而不断放宽用户实际需要的时限。
三、去重键、持久状态与外部发送边界
「发送前查一次记录」无法阻止两个并发工作进程同时发送。先在数据库中持久化通知任务,以业务事件、收件人和消息类型组成唯一键;同一事务提交业务变更与 outbox,工作进程原子领取并记录状态。失败后保留任务,不要把「已领取」永久当成「发送完成」。
验证码的业务事件可以是一个登录挑战及其代次,不能仅用收件人和模板在 60 秒内全部合并,否则两个独立登录请求会互相吞掉。重发冷却与账号、地址、IP 等滥用限额分别设计;用户看到的是当前挑战状态,而不是后台队列细节。
| 情况 | 状态与处理 | 避免的错误 |
|---|---|---|
| 同一业务事件重复触发 | 唯一键命中,返回已有任务状态 | 重复建立多份发送任务 |
| 外部服务明确拒绝且可重试 | 记录原因,截止前按策略重试 | 不区分错误就无限重试 |
| 提交超时,是否接受未知 | 标为 unknown,优先查服务商记录或用其支持的幂等键重试 | 直接标成功,或盲目切备用重复发 |
| 服务商已接受,尚未有 delivery | 等待其投递状态,检查有效期 | 应用与服务商同时不断补发 |
| 截止后仍未提交 | 标 expired,不再发已失效验证码 | 半小时后把旧挑战发给用户 |
本地事务无法与外部邮件发送原子提交;没有服务商幂等或查询能力时,故障恢复可能在重复和漏发之间权衡,不能承诺端到端 exactly-once。Message-ID 本身也不是通用去重保证。
同一挑战的补发若复用凭证,不能因重试无限延长有效期。若产品明确创建新代次并作废旧凭证,应同步更新界面提示;一次性使用、尝试次数与失效规则都在验证服务执行,不能依赖邮件按发送顺序到达。
四、应用重试与 SMTP 重投不是一套时钟
| 层次 | 典型问题 | 本例处理 |
|---|---|---|
| 应用到服务商 API | HTTP 429、明确的临时服务错误 | 按文档及 Retry-After,有限退避加抖动 |
| 凭据或配置 | 无权限、认证失败、模板错误 | 暂停并告警,修配置后再恢复 |
| 邮件传输 | 临时 SMTP 失败 | 由传输方按其队列策略处理,不重复另建应用发送 |
| 最终退信 | 地址无效、策略拒绝等 | 按细分原因处理,地址无效才进入相应抑制;策略拒绝不等于地址不存在 |
本例对尚未被服务商接受的短效验证任务,安排首发之外最多 3 次应用重试,等待上限分别为 1、2、4 秒并加入抖动;若 Retry-After 超过剩余有效时间,直接停止这轮发送。每次出队和提交前都检查截止时间。这个例子不规定服务商的 SMTP 重投间隔,更不把分钟级退避用于已经过期的验证码。
服务商接受后的传输重试可能晚于业务有效期,未必能撤回;验证端必须拒绝过期凭证。收据没有同样短的有效期,可设更长恢复窗口,但仍需有限保留、最终失败状态和人工补发路径。
备用渠道必须先配好认证、容量、抑制与偏好同步,并演练 unknown 状态下如何避免双发。不能默认「切短信」就一定能恢复:号码可用性、成本和通知资格都要满足,站内可查状态也很重要。
五、告警、工单与可复算的收益
本例每 5 分钟检查队列最老等待时间、有效期前接受比例、明确失败数与 unknown 数。低流量窗口先展示分子、分母与事件详情;只出现一次失败就报 100%,不应伪装成稳定趋势。关键请求可立即告警,批量比例告警另设最小样本与去抖。
告警至少包含时间窗、消息类型、服务商、任务状态与脱敏追踪 ID;不要把验证码、重置链接或完整收件地址发到公共聊天。告警通道应独立于故障的邮件链路,并有可执行的恢复卡片。
以下只是重试收益的教学模型,假定 3000 条事务对应独立用户请求,每个失败请求最多生成一张相关工单;实际应按请求或用户去重,延迟工单另统计。
月独立事务 = 3000
首次失败 = 3000 × 2% = 60
其中可恢复比例 = 80%(所有允许重试后的累计结果,不是每次成功率)
最终失败 = 60 × (1 − 80%) = 12,即全量的 0.4%
失败请求联系支持的比例 = 40%
不重试:预期工单 = 60 × 40% = 24
预期工时 = 24 × 12 分钟 = 288 分钟 = 4.8 小时
重试后:预期工单 = 12 × 40% = 4.8(期望值,不是实际 4.8 张)
预期工时 = 4.8 × 12 = 57.6 分钟 = 0.96 小时
预期释放 = 4.8 − 0.96 = 3.84 小时/月
按 60 美元/小时估值 = 230.40 美元/月,不是现金收入
这组假设不能证明拆队列造成了全部改善,也不包括重试费用、维护与误发成本。真实验收同时查看业务完成、过期补发、重复发送和人工恢复,不能只看最终失败率。
行动清单与实操验收
- 固定每个观测阶段与时间起终点,超时和 unknown 不排除在报表之外。
- 用持久唯一键与原子领取约束同一任务,检查工作进程崩溃后的恢复。
- 区分登录挑战、重发冷却与外部投递尝试。
- 用可控故障分别模拟明确拒绝、接受后丢响应、延迟回调与凭证过期。
- 确认旧验证码不能在过期或使用后通过验证。
- 演练备用渠道,检查抑制、额度与重复发送处理。
验收应能从一个业务事件追到任务、每次提交和最终状态;有条件时用自有测试邮箱检查实际体验。本文未进行真实邮件服务、DNS 或短信账号验证,不能据此声称生产投递达标。
关键参考与核验范围
核验日期:2026-10-25。协议、服务契约与本文的教学目标分开使用。
- RFC 5321 §4.5.4.1 :SMTP 传输重试机制,不是本文应用 API 重试计划的来源。
- Amazon SES 事件说明 :用于核对收件服务器接受与后续退信的差别;这是 SES 契约,不代表所有服务商事件完全相同。
- RFC 5322 :邮件格式与标识;不提供外部发送 exactly-once 保证。
10 秒、3000 条、80% 与时间估值均为教学输入。阈值需结合业务时限、可观测性和实际负载制定,不能当作协议要求。
系列导航
- 上一篇:消息清点:一个产品到底需要发多少条消息
- 下一篇:发信域名与认证:SPF、DKIM、DMARC 的最小配置
- 回到目录:一个人的消息系统:事务邮件、通知分层与送达率
- 相关阅读:第 61–70 天:上线前的十五件事
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。