支付接口是源码交易平台最敏感也最脆弱的环节。一旦被恶意刷单或 CC 攻击,轻则服务器负载飙升,重则产生大量无效订单、卡密被耗尽,甚至触发第三方支付平台的风控导致商户号被封。

传统的单机限流(如 Nginx 自带的 limit_req_zone)在多实例部署时各自为政,无法做到全局精确控制。本文介绍一套基于 Nginx Lua + Redis 的分布式 IP 限流方案,支持令牌桶和滑动窗口两种算法,可直接用于保护转卡码系统的支付入口和接口端点。

为什么需要分布式限流?

先看一个典型场景:假设你的转卡码平台有 3 台后端服务器,每台配置了 Nginx limit_req_zone 限制每 IP 每秒 10 个请求。攻击者将请求均匀分配到 3 台服务器,每台看到的是不到 10 QPS 的"正常流量",实际总 QPS 达到 30 —— 限流形同虚设。

分布式限流的核心思想是:所有节点共享同一个计数器,无论请求打到哪台机器,同一个 IP 的计数都能被精确聚合。Redis 由于其高性能和原子操作能力,是实现这一方案的最佳选择。

方案计数器位置全局精确性能复杂度
Nginx limit_req_zone共享内存(单机)极高
Nginx + Redis LuaRedis
API 网关层限流Redis
应用代码限流Redis/本地视实现低-中

环境准备:安装 OpenResty

OpenResty 是一个集成了 Lua 脚本引擎的高性能 Nginx 发行版。我们用它来编写限流逻辑。

# Ubuntu/Debian 安装 OpenResty
wget -qO - https://openresty.org/package/pubkey.gpg | sudo apt-key add -
sudo apt-get -y install --no-install-recommends wget gnupg ca-certificates
echo "deb https://openresty.org/package/debian $(lsb_release -sc) main" | sudo tee /etc/apt/sources.list.d/openresty.list
sudo apt-get update
sudo apt-get install -y openresty

# 安装 Redis(如未安装)
sudo apt-get install -y redis-server
sudo systemctl enable redis
sudo systemctl start redis

# 验证
openresty -v
redis-cli ping # 应返回 PONG

安装完成后,OpenResty 的可执行文件为 /usr/local/openresty/nginx/sbin/nginx,配置文件目录为 /usr/local/openresty/nginx/conf/

方案一:滑动窗口算法实现

滑动窗口通过记录时间戳来精确统计时间窗口内的请求数,比固定窗口更平滑,能有效避免"临界突变"问题。

Lua 限流脚本

创建 /usr/local/openresty/nginx/lua/rate_limit.lua

local redis = require "resty.redis"
local red = redis:new()
red:set_timeout(100) -- 100ms 超时

-- 连接 Redis
local ok, err = red:connect("127.0.0.1", 6379)
if not ok then
    ngx.log(ngx.ERR, "Redis connect failed: ", err)
    ngx.exit(500)
end

-- 限流参数
local key_prefix = "rate_limit:ip:"
local window_sec = 1      -- 窗口大小(秒)
local max_req = 10        -- 窗口内最大请求数
local client_ip = ngx.var.remote_addr
local key = key_prefix .. client_ip

-- 当前时间戳(毫秒)
local now = ngx.time() * 1000
local window_start = now - window_sec * 1000

-- ZREMRANGEBYSCORE + ZCARD 实现滑动窗口
-- 1. 移除窗口外的旧记录
red:zremrangebyscore(key, 0, window_start)
-- 2. 添加当前请求
red:zadd(key, now, tostring(now))
-- 3. 设置 key 过期
red:expire(key, window_sec * 2)
-- 4. 统计窗口内请求数
local count = red:zcard(key)

if count > max_req then
    ngx.status = 429
    ngx.header["Content-Type"] = "application/json"
    ngx.say('{"code":429,"msg":"请求太频繁,请稍后再试"}')
    return ngx.exit(429)
end

-- 限流通过,继续处理
ngx.log(ngx.INFO, "IP ", client_ip, " pass, count=", count)

Nginx 配置

http {
    lua_package_path "/usr/local/openresty/nginx/lua/?.lua;;";

    server {
        listen 80;
        server_name your-domain.com;

        # 转卡码支付入口 — 重点保护
        location /api/pay/ {
            access_by_lua_file /usr/local/openresty/nginx/lua/rate_limit.lua;
            proxy_pass http://backend;
        }

        # 转卡码订单查询接口
        location /api/order/ {
            access_by_lua_file /usr/local/openresty/nginx/lua/rate_limit.lua;
            proxy_pass http://backend;
        }

        location / {
            proxy_pass http://backend;
        }
    }
}

方案二:令牌桶算法实现

令牌桶允许一定程度的突发流量,更适合转卡码系统的支付入口场景 —— 正常用户可以偶尔快速操作,但持续高频访问会被限流。

-- /usr/local/openresty/nginx/lua/token_bucket.lua
local redis = require "resty.redis"
local red = redis:new()
red:set_timeout(100)

local ok, err = red:connect("127.0.0.1", 6379)
if not ok then
    ngx.exit(500)
end

local key_prefix = "token_bucket:ip:"
local rate = 5           -- 每秒补充令牌数
local capacity = 20      -- 桶容量(允许突发)
local client_ip = ngx.var.remote_addr
local key = key_prefix .. client_ip
local now = ngx.time()

-- Lua 脚本:原子操作取令牌
local script = [[
    local key = KEYS[1]
    local rate = tonumber(ARGV[1])
    local capacity = tonumber(ARGV[2])
    local now = tonumber(ARGV[3])

    local bucket = redis.call("HMGET", key, "tokens", "timestamp")
    local tokens = tonumber(bucket[1]) or capacity
    local ts = tonumber(bucket[2]) or now

    -- 补充令牌
    local elapsed = math.max(now - ts, 0)
    tokens = math.min(capacity, tokens + elapsed * rate)

    -- 更新桶状态
    redis.call("HMSET", key, "tokens", tokens, "timestamp", now)
    redis.call("EXPIRE", key, 10)

    if tokens >= 1 then
        redis.call("HINCRBY", key, "tokens", -1)
        return 1  -- 拿到令牌
    else
        return 0  -- 被限流
    end
]]

local ok, err = red:eval(script, 1, key, rate, capacity, now)
if not ok or ok == 0 then
    ngx.status = 429
    ngx.header["Content-Type"] = "application/json"
    ngx.say('{"code":429,"msg":"服务器繁忙,请稍后重试","retry_after":1}')
    return ngx.exit(429)
end

令牌桶的好处是:允许用户偶尔的突发操作(比如连续点击 3-4 次刷新),但持续高频操作会被桶限制,只有等令牌补充后才能继续。

方案三:多级限流 — 分层防御

单层限流永远不够安全。生产环境应该设计 多层防御体系

  1. L1 网络层: iptables + fail2ban 封禁异常 IP
  2. L2 Nginx 层: 分布式 Redis 令牌桶(本文方案)
  3. L3 应用层: 业务逻辑内的二次校验 + 风控
  4. L4 数据层: 数据库连接池 + 慢查询限流

以转卡码系统的支付入口为例,完整的多层防御配置如下:

# Nginx 完整限流配置
http {
    # L2: Lua + Redis 分布式限流
    lua_package_path "/usr/local/openresty/nginx/lua/?.lua;;";

    # L1: Nginx 自带单机限流(兜底)
    limit_req_zone $binary_remote_addr zone=per_ip:10m rate=30r/s;
    limit_conn_zone $binary_remote_addr zone=conn_per_ip:10m;

    server {
        listen 443 ssl;
        server_name api.your-domain.com;

        location /api/pay/create {
            # L1 兜底:单机 30 r/s
            limit_req zone=per_ip burst=10 nodelay;
            limit_conn conn_per_ip 5;

            # L2 分布式:5 t/s, 突发 20
            access_by_lua_file /usr/local/openresty/nginx/lua/token_bucket.lua;

            proxy_pass http://pay_backend;
        }
    }
}

Redis 性能优化

当 QPS 达到数万级别时,Redis 的单线程模型可能成为瓶颈。以下优化策略必不可少:

使用 Redis Pipeline

-- 批量操作减少 RTT
local pipeline = {}
pipeline[1] = {"ZREMRANGEBYSCORE", key, 0, window_start}
pipeline[2] = {"ZADD", key, now, tostring(now)}
pipeline[3] = {"EXPIRE", key, window_sec * 2}
pipeline[4] = {"ZCARD", key}

local results, err = red:commit_pipeline()
if not results then
    ngx.log(ngx.ERR, "Pipeline error: ", err)
    ngx.exit(500)
end
local count = results[4]

本地缓存降级

-- 用 lua-resty-lrucache 做本地缓存降级
local lrucache = require "resty.lrucache"
local cache, err = lrucache.new(200)  -- 缓存 200 个 IP

-- 当 Redis 不可用时,回退到本地计数器
local ok, err = red:connect("127.0.0.1", 6379)
if not ok then
    -- Redis 故障降级:使用本地缓存做粗略限流
    local local_count = cache:get(client_ip) or 0
    if local_count > max_req then
        ngx.exit(429)
    end
    cache:set(client_ip, local_count + 1, window_sec)
    return  -- 通过
end

配置参数建议

场景窗口/速率突发容量Redis 超时
转卡码创建订单3 r/s10100ms
转卡码查询接口10 r/s30200ms
后台管理 API20 r/s50500ms
健康检查接口不限不限500ms

验证与监控

# 用 ab 或 wrk 压力测试
ab -n 100 -c 10 -H "X-Forwarded-For: 1.2.3.4" \
   https://your-domain.com/api/pay/create

# 查看 Redis 中的限流 key
redis-cli keys "rate_limit:ip:*"
redis-cli ZCARD rate_limit:ip:1.2.3.4

# Nginx 日志查看限流情况
tail -f /usr/local/openresty/nginx/logs/access.log | grep "429"
# 创建 Prometheus 监控指标(可选)
location /metrics {
    content_by_lua_block {
        -- 暴露限流统计数据给 Prometheus
        local count = ngx.shared.rate_metrics:get("blocked_count") or 0
        ngx.say('rate_limit_blocked_total ' .. count)
    }
}

与转卡码系统的集成

我们的 源码商城转卡码系统 v3 已经内置了 Nginx Lua 分布式限流模块。部署后只需修改两处配置即可开启:

# 在转卡码系统的 .env 中启用限流
RATE_LIMIT_ENABLED=true
RATE_LIMIT_ALGORITHM=token_bucket    # token_bucket | sliding_window
RATE_LIMIT_RATE=5
RATE_LIMIT_CAPACITY=20
RATE_LIMIT_REDIS_HOST=127.0.0.1
RATE_LIMIT_REDIS_PORT=6379

借助 Codex Desktop 的 AI 编程能力,你可以让 Codex 自动生成适合你业务场景的限流规则 —— 只需给它描述你的业务特征和期望的 QPS,Codex 就能输出完整的 Lua 脚本和 Nginx 配置。

💡 提示: 限流不是万能的。它解决的是"量"的问题,不是"质"的问题。真正的风控体系需要将 IP 限流与设备指纹、行为分析、黑名单库配合使用,形成完整的支付安全防线。

总结

本文实现了三种层层递进的分布式 IP 限流方案:

  • 滑动窗口算法(ZSET 实现):精确统计,适合对精度要求高的接口
  • 令牌桶算法(Redis Hash + Lua 脚本):允许突发,适合支付入口
  • 多级防御架构:Nginx → Redis → 应用层 → 数据库,层层拦截

所有方案均可直接用于保护转卡码平台、支付接口和 API 服务,配合源码商城提供的 转卡码系统 v3 开箱即用,无需从头开发。