revel 框架不内置重试机制,因 web 请求需一次响应,盲目重试会破坏幂等性;重试应限于 service 层下游依赖,须避让事务边界、传 context、加抖动退避。

Revel 框架本身不内置重试机制,Controller.Action 执行失败后不会自动重试——这不是遗漏,而是设计选择:Web 请求的语义天然要求“一次响应”,盲目重试会放大幂等风险(比如重复提交订单、重复发消息)。真要加重试,必须明确区分场景、控制边界、隔离副作用。
什么时候不该在 Revel Controller 里做重试
绝大多数 HTTP handler 场景下,重试应交给客户端或上游网关处理。Revel 的 Controller 是同步响应模型,一旦进入 Render 或 Redirect,就已向客户端发出状态码。此时:
- 服务端重试没有意义:客户端早已收到 500/502,不会等待第二次响应
- 重试可能破坏幂等性:比如
POST /orders失败后内部再调一次,极可能生成重复订单 - 日志和监控失真:原始失败被掩盖,
output.xml或 Prometheus 指标无法反映真实失败率
需要重试时,该把逻辑放在哪一层
真正该重试的,是 Controller 内部调用的「下游依赖」,比如数据库查询、第三方 API、消息队列投递。这些属于「支撑动作」,失败不影响用户感知,但影响业务完整性。推荐分层处理:
-
Service层封装重试:把 DB 查询、HTTP 调用、Redis 操作等抽成独立 service 方法,用retry.Do(如 github.com/avast/retry-go)或自定义退避逻辑包裹 - Controller 只负责「协调」:调用 service 方法,捕获其返回的最终错误(含重试耗尽后的兜底异常),再决定是
RenderError还是降级返回默认值 - 避免在
Filter中全局加重试:Filter 对所有 action 生效,容易误伤非幂等操作,且难以按 error type 细粒度控制
用 retry-go 实现带抖动的指数退避(推荐方案)
比手写 for + time.Sleep 更可靠,也比 Spring Retry 那套 AOP 更轻量。关键点不是“多试几次”,而是让重试行为可预测、可收敛:
import "github.com/avast/retry-go"
func (s *OrderService) GetInventory(ctx context.Context, sku string) (int, error) {
var inv int
err := retry.Do(
func() error {
resp, err := http.DefaultClient.Do(http.NewRequestWithContext(ctx, "GET", "https://api.example.com/inventory/"+sku, nil))
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
return fmt.Errorf("http %d", resp.StatusCode)
}
// 解析 JSON...
return nil
},
retry.Attempts(3),
retry.Delay(100*time.Millisecond),
retry.Multiplier(2), // 指数增长
retry.Jitter(0.2), // ±20% 抖动,防请求对齐
retry.LastErrorOnly(true),
retry.Context(ctx),
)
return inv, err
}
- 必须传
context.Context:防止重试卡死,上游超时能及时中断整个链路 -
retry.Jitter不可省:否则所有实例在同一秒重试,容易触发下游雪崩 - 不要用
retry.Unlimited:最大重试次数建议 ≤3,超过后成功率通常断崖下跌
最容易被忽略的坑:重试与事务边界的错位
如果 Controller 方法开启了数据库事务(比如用 revel.BegunTx),而你在事务内调用一个带重试的 service,会出现严重问题:
- 第一次调用失败 → 重试 → 第二次成功 → 但第一次的 DB 操作已回滚,数据状态不一致
- 重试期间事务锁未释放,拖慢其他请求
正确做法是:重试逻辑必须在事务外执行;若需强一致性,改用 Saga 模式(先记日志,异步补偿),而不是在事务里硬扛重试。











