共计 2189 个字符,预计需要花费 6 分钟才能阅读完成。
痛点分析
在分布式系统中实现稳定可靠的邮件发送能力面临诸多挑战,以下是几个关键痛点:

- 邮件发送的幂等性挑战 :网络抖动可能导致重复发送,需要设计去重机制保证业务语义正确性
- SMTP/API 混合发送模式下的状态一致性 :当同时使用 SMTP 协议和第三方邮件 API 时,如何保证发送状态的一致性和可追溯性
- 模板渲染的 CPU 密集型操作影响吞吐量 :动态内容渲染在高峰期可能成为系统瓶颈,影响整体吞吐量
技术方案对比
在解决上述痛点时,我们评估了多种技术方案:
- 消息传输层对比
- 直接调用 SMTP:实现简单但缺乏弹性,网络抖动时容易阻塞主流程
-
消息队列异步处理:通过 Kafka/RabbitMQ 实现削峰填谷,系统解耦更彻底
-
模板引擎性能基准
- Mustache:轻量级,编译速度快,适合简单模板
-
Thymeleaf:功能强大但启动开销大,经测试渲染耗时是 Mustache 的 2 - 3 倍
-
存储方案选型
- MySQL:适合强一致性要求的发送记录存储
- MongoDB:schema-free 特性便于存储动态模板和邮件内容
核心实现
状态机设计
采用 Spring State Machine 管理邮件发送状态流转:
@Configuration
@EnableStateMachine
public class MailStateMachineConfig {
@Bean
public StateMachine<MailStates, MailEvents> stateMachine() {
StateMachineBuilder.Builder<MailStates, MailEvents> builder =
StateMachineBuilder.builder();
// 状态配置
builder.configureStates()
.withStates()
.initial(MailStates.PENDING)
.state(MailStates.PROCESSING)
.end(MailStates.SENT)
.end(MailStates.FAILED);
// 事件转换
builder.configureTransitions()
.withExternal()
.source(MailStates.PENDING)
.target(MailStates.PROCESSING)
.event(MailEvents.START_SEND)
.and()
.withExternal()
.source(MailStates.PROCESSING)
.target(MailStates.SENT)
.event(MailEvents.SEND_SUCCESS);
}
}
Kafka 消费者实现
@KafkaListener(topics = "mail-requests")
public void consume(MailRequest request) {
mailStateMachine.sendEvent(MessageBuilder.withPayload(MailEvents.START_SEND)
.setHeader("mailId", request.id())
.build());
try {mailService.send(request);
stateMachine.sendEvent(/* SEND_SUCCESS 事件 */);
} catch (Exception e) {handleFailure(request, e);
}
}
模板优化策略
- 启动时预编译高频模板
- 使用 Caffeine 缓存编译结果
- 对变量插值区域做静态分析,提前校验模板语法
生产环境考量
压测指标
| 并发量 | TP99(ms) | 错误率 |
|---|---|---|
| 100 | 120 | 0.01% |
| 500 | 210 | 0.05% |
| 1000 | 350 | 0.12% |
重试策略
public void retryWithBackoff(MailRequest request) {
int maxRetries = 3;
long initialDelay = 1000;
for (int i = 0; i < maxRetries; i++) {
try {mailService.send(request);
return;
} catch (Exception e) {Thread.sleep(initialDelay * (1 << i)); // 指数退避
}
}
throw new MailSendException("Max retries exceeded");
}
监控指标
metrics:
mail:
sent_total:
type: counter
description: "Total sent emails"
failed_total:
type: counter
labels: [reason]
send_duration_seconds:
type: histogram
buckets: [0.1, 0.5, 1, 5]
避坑指南
- 第三方服务熔断 :当检测到 API 配额接近耗尽时,自动切换备用通道或触发熔断
- 附件处理 :使用 try-with-resources 确保文件流关闭,避免内存泄漏
- 时区处理 :所有时间戳存储为 UTC,仅在展示时转换时区
延伸思考
如何设计跨地域的邮件发送灾备方案?建议考虑:
- 多区域部署发送服务,通过 DNS 实现流量切换
- 建立邮件内容跨区同步机制
- 设计统一的状态同步协议,保证最终一致性
通过上述方案,我们成功将邮件发送吞吐量提升 3 倍,TP99 响应时间降低 60%。系统现已稳定运行 6 个月,日均处理百万级邮件发送请求。
正文完
