邮件发送失败需按异常类型差异化处理:临时故障重试并最终补偿,业务问题直接补偿,代码缺陷应修复而非补偿;推荐事件驱动解耦补偿逻辑,确保幂等与可观测性。

邮件发送失败时触发补偿逻辑,关键不是“捕获异常就立刻补偿”,而是要区分失败类型、确保补偿动作幂等、并合理设计重试与降级策略。
明确哪些异常需要补偿
不是所有异常都该走补偿。比如:
- 网络超时、SMTP 连接拒绝、认证失败:属于临时性故障,适合重试 + 最终补偿(如落库待人工处理)
- 收件人邮箱格式错误、被拉黑、域名不存在:属于业务/数据问题,应记录告警,跳过重试,直接触发补偿(如通知管理员、写入失败队列、改用站内信)
- NullPointerException、ClassCastException 等运行时异常:属于代码缺陷,不应补偿,而应修复 bug 并告警
在 catch 块中调用补偿方法(简单场景)
适用于同步调用、失败后立即补救的轻量逻辑:
try {
mailService.send(email);
} catch (MailSendException e) {
if (isPermanentFailure(e)) {
compensateForMailFailure(email);
} else {
// 记录日志,稍后重试(例如用 ScheduledExecutor 或 MQ 延迟投递)
retryLater(email, e);
}
}
private void compensateForMailFailure(Email email) {
// 示例:保存到 failed_mail 表,触发短信通知负责人
failedMailRepository.save(new FailedMail(email, "SMTP rejected"));
smsService.send("【系统告警】邮件发送失败:" + email.getTo());
}
用事件驱动解耦补偿逻辑(推荐)
避免业务主流程强依赖补偿实现,提升可维护性:
立即学习“Java免费学习笔记(深入)”;
- 发送失败时发布 MailSendFailedEvent 事件(可用 Spring 的 ApplicationEventPublisher)
- 监听该事件的补偿服务执行具体动作:存库、发短信、推消息、调用其他系统接口等
- 补偿服务自身可重试、可监控、可独立部署
这样主流程只关心“发没发成”,不关心“失败后干啥”,也便于后续扩展(比如增加钉钉通知、写入数据湖等)。
补偿必须保证幂等和可观测
补偿逻辑常被重复触发(如重试机制、消息重复消费),所以:
- 数据库操作加唯一约束或先查后插(insert ignore / on conflict do nothing)
- 补偿动作带业务 ID(如 orderNo、emailId),用于去重或状态校验
- 每步补偿写审计日志,记录时间、输入、结果;关键步骤上报 metrics(如 compensation_success_total)
- 失败的补偿本身也要有兜底(如二次告警、人工介入入口)


















