一、为什么需要请求重试机制

在开发中,我们经常会遇到服务调用失败的情况,比如网络抖动、目标服务短暂不可用等。如果直接返回错误,用户体验会很差。这时候,请求重试机制就能派上用场了。

举个例子,假设你调用一个第三方支付接口,第一次请求因为网络问题失败了,但几秒后网络恢复。如果有重试机制,系统会自动再试几次,而不是直接告诉用户“支付失败”。

OpenResty 是一个基于 Nginx 和 Lua 的高性能 Web 平台,非常适合用来实现这类功能。它既保持了 Nginx 的高并发能力,又能通过 Lua 脚本灵活控制请求逻辑。

二、OpenResty 实现请求重试的基本思路

在 OpenResty 中,我们可以利用 ngx.location.captureresty.http 库发起 HTTP 请求,并在失败时进行重试。核心逻辑包括:

  1. 设定最大重试次数:避免无限重试导致请求堆积。
  2. 设置合理的重试间隔:比如第一次失败等 1 秒,第二次等 2 秒,避免雪崩。
  3. 错误类型判断:不是所有错误都需要重试,比如 404(资源不存在)重试也没用。

下面是一个简单的示例:

-- 技术栈:OpenResty + Lua
local http = require "resty.http"
local cjson = require "cjson"

-- 最大重试次数
local max_retry = 3
-- 初始重试间隔(秒)
local retry_delay = 1

-- 发起 HTTP 请求(带重试)
local function request_with_retry(url, method, body)
    local httpc = http.new()
    local retry_count = 0
    local resp, err

    while retry_count < max_retry do
        resp, err = httpc:request_uri(url, {
            method = method,
            body = body,
            headers = { ["Content-Type"] = "application/json" }
        })

        -- 请求成功,直接返回
        if resp and resp.status == 200 then
            return resp.body
        end

        -- 如果是 5xx 错误或网络问题,才进行重试
        if (resp and (resp.status >= 500 and resp.status < 600)) or err then
            retry_count = retry_count + 1
            ngx.log(ngx.WARN, "请求失败,准备第 ", retry_count, " 次重试。错误: ", err or resp.status)
            ngx.sleep(retry_delay * retry_count)  -- 等待时间递增
        else
            -- 其他错误(如 4xx)不重试
            break
        end
    end

    -- 重试次数用尽仍失败
    return nil, "请求失败,重试 " .. max_retry .. " 次后仍无法成功。"
end

-- 示例调用
local result, err = request_with_retry("https://api.example.com/pay", "POST", cjson.encode({ amount = 100 }))
if not result then
    ngx.say("支付失败: ", err)
else
    ngx.say("支付成功: ", result)
end

这个示例展示了基本的重试逻辑,但实际生产环境可能需要更复杂的策略,比如结合熔断机制(如 Hystrix 的思路)避免服务雪崩。

三、进阶优化:动态调整重试策略

上面的例子是固定重试次数和间隔,但在真实场景中,可能需要更智能的策略。比如:

  1. 指数退避:每次重试间隔时间按指数增长(1s, 2s, 4s, 8s…),避免集中重试导致服务压力过大。
  2. 随机抖动:在退避时间上加一个随机值,避免多个客户端同时重试。
  3. 熔断机制:如果失败率过高,暂时停止请求,直接返回错误。

优化后的代码示例:

-- 技术栈:OpenResty + Lua
local http = require "resty.http"

-- 动态调整的重试策略
local function request_with_smart_retry(url, method, body)
    local httpc = http.new()
    local max_retry = 5
    local retry_count = 0
    local resp, err

    while retry_count < max_retry do
        resp, err = httpc:request_uri(url, {
            method = method,
            body = body,
            headers = { ["Content-Type"] = "application/json" }
        })

        -- 成功则返回
        if resp and resp.status == 200 then
            return resp.body
        end

        -- 仅对可重试错误进行处理
        if (resp and resp.status >= 500) or err then
            retry_count = retry_count + 1
            local delay = math.min(2 ^ retry_count + math.random(), 10)  -- 指数退避 + 随机抖动,最大 10 秒
            ngx.log(ngx.WARN, "请求失败,", retry_count, " 次重试,等待 ", delay, " 秒")
            ngx.sleep(delay)
        else
            break
        end
    end

    return nil, "请求失败,重试次数用尽"
end

四、应用场景与注意事项

适用场景

  1. 支付/订单系统:避免因短暂故障导致交易失败。
  2. 微服务调用:服务间依赖较强时,重试可以提高整体可用性。
  3. 第三方 API 调用:比如短信发送、地图服务等。

注意事项

  1. 幂等性:确保重试不会导致重复扣款等副作用。
  2. 超时设置:避免单次请求耗时过长影响整体性能。
  3. 日志记录:记录重试次数和错误信息,方便排查问题。

五、总结

请求重试是提高服务可靠性的有效手段,但需要合理设计,避免过度重试导致系统压力过大。OpenResty 结合 Lua 脚本可以灵活实现各种重试策略,适合高并发场景。

如果你正在开发高可用服务,不妨试试这套方案!