← 返回博客

幂等键:防止重试发出重复邮件

网络超时重试最容易让用户收到两封邮件。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 文档。