← 返回博客
幂等键:防止重试发出重复邮件
网络超时重试最容易让用户收到两封邮件。Rovela 的幂等键在 24 小时内保证同一个键只发一封,本文讲清规则、键的取法和退避重试模式。
更新于 2026年10月13日

网络超时以后重试发信,是让同一个用户收到两封验证码、两封订单确认的最常见原因。请求其实已经到了服务端,邮件也排进了队列,只是响应在路上丢了。客户端看到的是失败,于是又发了一次。
幂等键就是为这个场景准备的:同一个键发两次,服务端只发一封,第二次直接返回第一次的结果。
用法
在发信请求上带一个 Idempotency-Key 请求头:
curl https://api.rovela.dev/emails \
-H "Authorization: Bearer $ROVELA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: order-10086-confirmed" \
-d '{
"from": "订单通知 <orders@yourdomain.com>",
"to": "buyer@example.com",
"subject": "订单 #10086 已确认",
"html": "<p>我们已收到你的订单。</p>"
}'
第一次请求正常发信,返回 {"object":"email","id":"..."}。24 小时内用同一个键再请求,Rovela 不会再发信,而是原样返回第一次的响应,邮件 ID 也相同。批量接口 POST /emails/batch 同样支持这个请求头,作用于整批请求。
Rovela 的具体规则
- 作用域是组织。不同组织用同一个键互不影响,不会拿到别人的缓存结果。
- 有效期 24 小时。超过 24 小时,同一个键会被当成新请求。
- 只缓存成功的响应。参数错误、额度用完这类失败不会被记住。修正参数后用同一个键重试,会正常发出。
- 长度上限 256 个字符,首尾空白会被去掉。空值等于没带。
键该怎么取
键要绑定「这封信的业务含义」,不能绑定「这一次请求」。
好的键:
order-10086-confirmed:订单号加事件类型。pwd-reset-<token 的 ID>:一次重置流程对应一个键。welcome-<user_id>:每个用户只该收到一封欢迎信。
不好的键:
- 每次请求随机生成的 UUID。重试时换了新键,等于没用。
- 只用用户 ID。同一个用户的第二笔订单会被当成重复请求吞掉。
经验法则:如果两次请求在业务上是同一件事,就该得到同一个键。
重试要配合退避
幂等键解决的是「重试会不会重复」,不解决「什么时候重试」。超时和 5xx 可以重试,4xx 是请求本身有问题,重试也没用:
async function sendWithRetry(payload, key, attempts = 4) {
for (let i = 0; i < attempts; i++) {
try {
const res = await fetch('https://api.rovela.dev/emails', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.ROVELA_API_KEY}`,
'Content-Type': 'application/json',
'Idempotency-Key': key,
},
body: JSON.stringify(payload),
signal: AbortSignal.timeout(10_000),
})
if (res.ok) return res.json()
if (res.status < 500 && res.status !== 429) {
throw new Error(`不可重试:${res.status} ${await res.text()}`)
}
} catch (err) {
if (err.message?.startsWith('不可重试')) throw err
}
await new Promise((r) => setTimeout(r, 2 ** i * 500))
}
throw new Error('重试次数用完')
}
一个边界
Rovela 要等邮件被接受、排进队列之后,才写入幂等记录。如果两条带同一个键的请求在同一瞬间并发到达,两条都可能被处理。幂等键保护的是「先失败、后重试」这种串行场景。不要拿它当分布式锁,也不要用同一个键并发发请求。
小结
给每一封有业务含义的事务邮件带上幂等键,键从业务 ID 推出来,重试时保持不变。这样网络再不稳定,用户也只会收到一封信。
接口细节见 发送邮件 API 文档。