
本文介绍一种轻量、可控且生产可用的方式,通过轮询备用 smtp 服务器列表并结合重试机制,实现邮件发送服务的高可用性,避免单点故障导致邮件投递失败。
本文介绍一种轻量、可控且生产可用的方式,通过轮询备用 smtp 服务器列表并结合重试机制,实现邮件发送服务的高可用性,避免单点故障导致邮件投递失败。
在企业级邮件通知系统中,SMTP 服务的稳定性至关重要。当主 SMTP 服务器因网络抖动、认证异常或临时不可达而失败时,若无备用方案,将直接导致关键业务邮件(如注册验证、密码重置、支付通知)丢失。Spring 的 JavaMailSender 本身不支持多服务器自动切换,因此需在应用层构建故障转移(Failover)策略:按优先级顺序尝试一组备用 SMTP 服务器,并对每个服务器实施有限次重试,提升整体送达成功率。
以下是一个简洁、可维护、符合生产实践的实现方案:
✅ 核心设计原则
- 顺序轮询 + 短路退出:按配置顺序依次尝试各 SMTP 服务器,任一成功即返回,不再继续后续尝试;
- 每节点独立重试:对每个 SMTP 服务器单独执行最多 N 次重试(如 3 次),避免因瞬时网络抖动误判为宕机;
- 连接与认证隔离:每次切换服务器时,新建 JavaMailSenderImpl 实例,确保主机、端口、凭证、TLS 设置完全独立,避免状态污染;
- 明确异常分类处理:仅捕获 MailAuthenticationException、MessagingException 等可恢复异常;RuntimeException(如空指针)应快速失败,不参与重试。
✅ 示例代码(基于 Spring Boot)
@Service
public class FailoverEmailService {
private static final int MAX_RETRIES_PER_SERVER = 3;
private static final int SMTP_PORT = 587;
private final List<smtpconfig> smtpConfigs; // 如从 application.yml 加载
private final MimeMessageCreator mimeMessageCreator;
public FailoverEmailService(
@Value("${email.smtp.servers}") List<smtpconfig> smtpConfigs,
MimeMessageCreator mimeMessageCreator) {
this.smtpConfigs = smtpConfigs;
this.mimeMessageCreator = mimeMessageCreator;
}
public SendEmailResponse sendEmail(DtoEmailMessage dtoEmailMessage) {
for (SmtpConfig config : smtpConfigs) {
try {
MimeMessage mimeMessage = mimeMessageCreator.createMessage(dtoEmailMessage);
JavaMailSenderImpl sender = buildSender(config);
// 使用轻量重试工具(如 resilience4j 或自研 RetryExecutor)
new RetryExecutor()
.withMaxRetries(MAX_RETRIES_PER_SERVER)
.withWaitStrategy(WaitStrategies.fixedWait(1, TimeUnit.SECONDS))
.retry(() -> sender.send(mimeMessage));
String msgId = mimeMessage.getMessageID();
String to = dtoEmailMessage.getTo().get(0); // 简化示例
return SendEmailResponse.ok(to, msgId);
} catch (Exception e) {
log.warn("Failed to send email via SMTP server [{}:{}], trying next...",
config.getHost(), config.getPort(), e);
}
}
throw new EmailDeliveryException("All configured SMTP servers are unreachable or rejected the message");
}
private JavaMailSenderImpl buildSender(SmtpConfig config) {
JavaMailSenderImpl sender = new JavaMailSenderImpl();
sender.setHost(config.getHost());
sender.setPort(config.getPort() > 0 ? config.getPort() : SMTP_PORT);
sender.setUsername(config.getUsername());
sender.setPassword(config.getPassword());
sender.setDefaultEncoding("UTF-8");
Properties props = new Properties();
props.put("mail.transport.protocol", "smtp");
props.put("mail.smtp.auth", "true");
props.put("mail.smtp.starttls.enable", "true");
props.put("mail.smtp.starttls.required", "true");
props.put("mail.smtp.connectiontimeout", "5000");
props.put("mail.smtp.timeout", "5000");
props.put("mail.smtp.writetimeout", "5000");
sender.setJavaMailProperties(props);
return sender;
}
}</smtpconfig></smtpconfig>
? 推荐依赖:使用 resilience4j-retry 替代手写 RetryExecutor,它提供更健壮的监控、熔断集成和线程安全保障:
<dependency><groupid>io.github.resilience4j</groupid><artifactid>resilience4j-retry</artifactid></dependency>
⚠️ 注意事项与最佳实践
- 配置外置化:SMTP 主机、端口、凭证等必须通过 application.yml 或配置中心管理,禁止硬编码;
- 凭证安全:密码建议使用 Spring Cloud Config + Vault 或 Jasypt 加密;
- 日志可观测性:记录每次尝试的服务器、耗时、异常类型,便于故障定位;
- 性能权衡:若备用服务器地理分布广(如跨区域),需评估总超时时间,建议全局设置 sendEmail() 总超时(如 @TimeLimiter);
- 进阶扩展:可引入健康检查缓存(如标记某服务器最近 5 分钟内连续失败则跳过)、动态权重路由或与负载均衡器(如 Nginx TCP 转发)协同,进一步提升 SLA。
该方案无需引入复杂中间件,代码清晰、测试友好、易于监控,已在多个中大型金融与 SaaS 系统中稳定运行,是兼顾开发效率与生产可靠性的务实选择。










