支付接口是源码交易平台最敏感也最脆弱的环节。一旦被恶意刷单或 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 Lua | Redis | ✅ | 高 | 中 |
| 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 次刷新),但持续高频操作会被桶限制,只有等令牌补充后才能继续。
方案三:多级限流 — 分层防御
单层限流永远不够安全。生产环境应该设计 多层防御体系:
- L1 网络层: iptables + fail2ban 封禁异常 IP
- L2 Nginx 层: 分布式 Redis 令牌桶(本文方案)
- L3 应用层: 业务逻辑内的二次校验 + 风控
- 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/s | 10 | 100ms |
| 转卡码查询接口 | 10 r/s | 30 | 200ms |
| 后台管理 API | 20 r/s | 50 | 500ms |
| 健康检查接口 | 不限 | 不限 | 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 开箱即用,无需从头开发。