OpenResty -- Nginx + Lua 访问 Redis

摘要

OpenResty 简介

  • OpenResty® 是一个基于 Nginx 与 Lua 的高性能 Web 平台,其内部集成了大量精良的 Lua 库、第三方模块以及大多数的依赖项。用于方便地搭建能够处理超高并发、扩展性极高的动态 Web 应用、Web 服务和动态网关。

  • OpenResty® 通过汇聚各种设计精良的 Nginx 模块(主要由 OpenResty 团队自主开发),从而将 Nginx 有效地变成一个强大的通用 Web 应用平台。这样,Web 开发人员和系统工程师可以使用 Lua 脚本语言调动 Nginx 支持的各种 C 以及 Lua 模块,快速构造出足以胜任 10K 乃至 1000K 以上单机并发连接的高性能 Web 应用系统。

  • OpenResty® 的目标是让你的Web服务直接跑在 Nginx 服务内部,充分利用 Nginx 的非阻塞 I/O 模型,不仅仅对 HTTP 客户端请求,甚至于对远程后端诸如 MySQL、PostgreSQL、Memcached 以及 Redis 等都进行一致的高性能响应。

  • 一句话:OpenResty 就是加载了 Lua 模块的 Nginx。

OpenResty 安装

MacOS

1
2
brew tap openresty/brew
brew install openresty
  • 安装时报如下错误,提示找不到GeoIP

GeoIP 是一个 IP 地址 → 地理位置映射库和模块,核心作用是:根据客户端 IP,解析出国家、城市、经纬度、运营商等地理信息。

1
2
3
4
5
6
7
8
checking for GeoIP library ... not found
checking for GeoIP library in /usr/local/ ... not found
checking for GeoIP library in /usr/pkg/ ... not found
checking for GeoIP library in /opt/local/ ... not found
checking for GeoIP library in /opt/homebrew/ ... not found

./configure: error: the GeoIP module requires the GeoIP library.
You can either do not enable the module or install the library.
  • 解决办法:这是因为 Homebrew 官方仓库已经不再支持 GeoIP,而是使用 GeoIP2 替代,但 Openresty 暂不支持 GeoIP2,所以这里只能选择不启用 GeoIP 模块。

1
2
3
4
5
6
7
8
# 编辑 openresty 的安装脚本
brew edit openresty/brew/openresty

# 将下面的行注释掉后保存并退出
# args << "--with-http_geoip_module"

# 重新安装
brew reinstall openresty
  • OpenResty 命令说明

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
openresty -h
nginx version: openresty/1.29.2.1
Usage: nginx [-?hvVtTq] [-s signal] [-p prefix]
[-e filename] [-c filename] [-g directives]

Options:
-?,-h : this help
-v : show version and exit
-V : show version and configure options then exit
-t : test configuration and exit
-T : test configuration, dump it and exit
-q : suppress non-error messages during configuration testing
-s signal : send signal to a master process: stop, quit, reopen, reload
-p prefix : set prefix path (default: /usr/local/Cellar/openresty/1.29.2.1_1/nginx/)
-e filename : set error log file (default: /usr/local/var/log/nginx/error.log)
-c filename : set configuration file (default: /usr/local/etc/openresty/nginx.conf)
-g directives : set global directives out of configuration file
参数 完整写法 中文含义 典型使用场景 示例
-h / -? openresty -h 显示帮助信息并退出 快速查看可用参数 openresty -h
-v openresty -v 显示版本号 确认运行版本 openresty -v
-V openresty -V 显示版本号 + 编译参数 排查模块、编译选项、依赖 openresty -V
-t openresty -t 校验配置文件合法性并退出 修改配置后验证语法 openresty -t
-T openresty -T 校验配置并打印完整配置内容 排查 include 文件、调试配置加载顺序 openresty -T
-q openresty -t -q 测试配置时只输出错误信息 CI / 自动化脚本 openresty -t -q
-s openresty -s reload 向 master 进程发送控制信号 服务管理(重载、停止等) openresty -s reload
stop 立即停止服务(强制) 紧急停服 openresty -s stop
quit 优雅停止服务(处理完请求后退出) 平滑下线 openresty -s quit
reload 平滑重载配置 发布配置变更 openresty -s reload
reopen 重新打开日志文件 日志切割后使用 openresty -s reopen
-p openresty -p /path 指定运行前缀目录(prefix) 多实例部署、定制目录结构 openresty -p /opt/openresty
-e openresty -e file 指定错误日志路径 临时调试错误日志 openresty -e /tmp/error.log
-c openresty -c file 指定配置文件路径 使用非默认配置启动 openresty -c ./nginx.conf
-g openresty -g "daemon off;" 设置全局指令(覆盖配置文件) 容器化 / 临时调试 openresty -g "daemon off;"
  • 安装后的目录

1
2
3
4
# 安装目录
/usr/local/opt/openresty
# 配置文件目录
/usr/local/etc/openresty/

Linux

  • 官网参考

  • 这里以 Amazon Linux 2023(内核 6.1) 系统为例,从这里找到对应的安装方法

1
2
3
sudo yum install -y yum-utils
sudo yum-config-manager --add-repo https://openresty.org/package/amazon/openresty.repo
sudo yum install -y openresty
  • 安装后的目录

1
2
3
4
# 安装目录
/usr/local/openresty
# 配置文件目录
/usr/local/openresty/nginx/conf

一个简单的示例

  • vim test-nginx.conf

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
worker_processes  1;
events {
worker_connections 1024;
}
http {
server {
listen 8080;
location / {
default_type text/html;
# 这是 OpenResty 提供的 Lua 指令: 在 HTTP 请求的 Content 阶段 执行 Lua 代码,用于生成响应内容。
content_by_lua_block {
-- ngx: OpenResty 提供的全局对象,封装了 Nginx API,可用于:写响应、读请求、操作 header、访问共享内存、控制状态码
-- ngx.say(...): 向 HTTP 响应体写入内容,自动追加换行符 \n,可多次调用
-- 等价于:ngx.print("<p>hello, world</p>\n")
ngx.say("<p>hello, world</p>")
}
}
}
}
  • 实际响应效果,浏览器收到:

1
2
3
4
HTTP/1.1 200 OK
Content-Type: text/html

<p>hello, world</p>
  • 启动

1
openresty -p `pwd` -c ./test-nginx.conf
  • 访问

1
2
3
curl http://127.0.0.1:8080
# 结果
<p>hello, world</p>

Nginx 请求处理阶段划分 与 Lua 指令

  • 在 OpenResty 中,一个 HTTP 请求大致经历以下核心阶段:

1
rewrite  →  access  →  content  →  log
阶段 主要职责 典型指令
rewrite URL 重写、变量计算、跳转 rewrite、set、rewrite_by_lua*
access 访问控制、鉴权、限流 allow/deny、auth_request、access_by_lua*
content 生成响应内容 proxy_pass、root、fastcgi_pass、content_by_lua*
log 日志记录、统计 access_log、log_by_lua*

*_by_lua 的指令是 OpenResty 在对应阶段注入 Lua 执行逻辑。

  • Lua 指令与 Nginx 阶段关系对照表

Nginx 阶段 Lua 指令 是否可读请求 是否可写响应 是否推荐输出内容 典型业务
rewrite rewrite_by_lua* ⚠️(不建议) URL 重写、变量
access access_by_lua* ⚠️(仅拒绝时) 鉴权、限流
content content_by_lua* 动态服务
log log_by_lua* ⚠️ 日志、统计

*_by_lua* 指令 支持三种形式:

写法形式 语法示例 含义说明 Lua 代码来源 是否支持多行 是否支持复杂逻辑 热更新友好度 推荐使用场景 注意事项
_by_lua_block { ... } content_by_lua_block { ngx.say("hello") } 在 Nginx 配置文件中直接以内嵌代码块方式书写 Lua nginx.conf 内嵌 ✅ 支持 ⚠️ 一般 ⚠️ 中等(需 reload) 简单逻辑、Demo、调试 配置文件可读性下降
_by_lua_file /path/a.lua access_by_lua_file lua/auth.lua; 从外部 Lua 文件加载并执行 独立 Lua 脚本文件 ✅ 支持 ✅ 强 ✅ 高(代码可版本管理) 生产环境、复杂业务 路径必须正确
_by_lua "inline lua" content_by_lua "ngx.say('ok')"; 将 Lua 代码作为字符串参数传入 Nginx 指令字符串 ❌ 不友好 ❌ 弱 ❌ 差 临时测试、单行逻辑 转义复杂,难维护

工程实践推荐等级

使用方式 推荐等级 理由
_by_lua_file ⭐⭐⭐⭐⭐ 可维护、可测试、可版本管理
_by_lua_block ⭐⭐⭐ 适合简单逻辑
_by_lua "..." 仅适合临时验证
  • 示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
server {
listen 8080;

# 方式1.content_by_lua_block :lua 内嵌代码块
location /hello-lua {
default_type 'text/plain';
content_by_lua_block {
ngx.say("Hello World! Lua & Nginx .")
}
}

# 方式2.content_by_lua_file :lua 脚本文件路径
location /hello-lua-file {
default_type 'text/html';
content_by_lua_file ./lua/hello.lua;
}

# 方式3.access_by_lua 在请求访问阶段处理用于访问控制 :lua 字符串
location /hello-lua-access {
default_type 'text/html';
access_by_lua '
local message = "403 - Hello World! Lua & Nginx access_by_lua"
ngx.say(message)
';
}

# 方式4.content_by_lua 在内容处理阶段接受请求并输出响应:lua 字符串
location /hello-lua-content {
default_type 'text/html';
content_by_lua "ngx.print('Hello World!')";
}
}

如何获取客户端请求数据?

  • OpenResty 把 HTTP 请求映射为 Lua API,主要分为 5 类

✅ 1. 获取 URL / Query 参数(GET)

  • 示例请求

1
GET /api?user=tom&age=18
  • Lua 获取方式

1
2
3
4
local args = ngx.req.get_uri_args()
-- args 是一个 table,获取不到参数返回 nil
local user = args["user"]
local age = args["age"]

✅ 2. 获取 POST 表单参数(application/x-www-form-urlencoded)

  • 示例请求

1
2
3
4
5
# 示例请求
POST /api
Content-Type: application/x-www-form-urlencoded

user=tom&age=18
  • Lua 获取方式

1
2
3
4
5
6
-- 必须先读取 Body
ngx.req.read_body()
local args = ngx.req.get_post_args()

local user = args["user"]
local age = args["age"]

✅ 3. 获取 JSON Body(application/json)

  • 示例请求

1
2
3
4
5
# 示例请求
POST /api
Content-Type: application/json

{"user":"tom","age":18}
  • Lua 获取方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
-- 必须先读取 Body
ngx.req.read_body()
local body = ngx.req.get_body_data()

-- 加载 JSON 库
local cjson = require "cjson.safe"
-- 解析为 JSON 对象
local data, err = cjson.decode(body)
if not data then
ngx.log(ngx.WARN, "json decode failed: ", err)
return ngx.exit(400)
end

local user = data.user
local age = data.age

✅ 4. 获取 HTTP Headers

1
2
3
4
5
6
7
8
9
10
-- 获取所有 Header
local headers = ngx.req.get_headers()

for k, v in pairs(headers) do
ngx.say(k, " = ", v)
end

-- 获取单个 Header
local ua = ngx.var.http_user_agent
local token = ngx.var.http_authorization

✅ 5. 获取请求方法、URI、路径等

1
2
3
4
5
6
7
8
9
10
11
12
13
-- 比如请求:curl http://127.0.0.1:8080/api\?name\=zhangsan\&age\=20
-- 请求方法,如 GET,POST
local method = ngx.req.get_method() -- GET
-- 请求 URI
local uri = ngx.var.uri -- /api
-- QueryString
local args = ngx.var.args -- name=zhangsan&age=20
-- 域名或IP
local host = ngx.var.host -- 127.0.0.1
-- 端口
local port = ngx.var.server_port -- 8080
-- 请求协议
local scheme = ngx.var.scheme -- http

总结

数据类型 Lua API
客户端 IP ngx.var.remote_addr
请求方法 ngx.req.get_method()
完整 URI ngx.var.request_uri
Path ngx.var.uri
QueryString ngx.var.args
GET 参数 ngx.req.get_uri_args()
POST 表单 ngx.req.get_post_args()
Raw Body ngx.req.get_body_data()
JSON Body cjson.decode()
Headers ngx.req.get_headers()
单个 Header ngx.var.http_xxx
Cookie ngx.var.http_cookie

Nginx + Lua + Redis 限流完整示例

限流设计说明

  • 限流规则

1
2
3
4
5
维度:客户端 IP
窗口:60 秒
阈值:10 次
算法:固定窗口计数器(0秒-60秒的每个整分钟内),实现复杂度极低,性能极高,但有边界突刺问题,可以使用滑动窗口算法(第一次访问后的最近 60 秒内)
存储:Redis
  • Redis Key 设计

1
2
rate:{client_ip}:{minute_timestamp}
# TTL:70 秒(防止残留)
  • vim redis-nginx.conf

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
# 指定 Nginx Worker 进程数量,生产环境:等于 CPU 核心数,建议配置为 auto,自动适配
worker_processes auto;
# 指定错误日志路径,所有错误都会写入该日志文件,包括 Lua ngx.log(ngx.ERR, ...)
error_log logs/error.log;
# 定义事件模型参数
events {
# 单个 Worker 最大并发连接数
worker_connections 1024;
}

http {
# Lua 模块加载路径
# 语法规则
# ?.lua 表示模块文件名占位符。
# ;; 表示 保留默认路径,否则会覆盖系统默认路径。
lua_package_path "/usr/local/openresty/lualib/?.lua;;";

# ----------------------------
# Redis 原子限流 Lua 脚本
# ----------------------------

# 创建一块 共享内存区,名称:redis_scripts,大小:1MB(Worker 之间共享内存)
# 常用于:缓存 Lua 脚本、Token、计数器、配置信息
lua_shared_dict redis_scripts 1m;

# Worker 初始化阶段加载 Lua,在 每个 Worker 启动时执行一次
# 适合:加载配置、初始化缓存、预加载脚本、启动定时器
init_worker_by_lua_block {
-- 将 Lua 脚本加载到 Nginx Worker 内存
local script = [[
-- 对指定 Key 进行自增
local cnt = redis.call("INCR", KEYS[1])
-- 如果是第一次创建 Key
if cnt == 1 then
-- 设置 Key 过期时间(秒)
redis.call("EXPIRE", KEYS[1], ARGV[1])
end
-- 返回当前计数
return cnt
]]
-- 将 Lua 脚本存入共享内存,避免每次请求拼接 Lua 脚本字符串,提升性能
local dict = ngx.shared.redis_scripts
-- Key:rate_limit_lua,Value:Lua 脚本
dict:set("rate_limit_lua", script)
}

server {
listen 8080;

location /api {
# 在 content 阶段执行 Lua,完全由 Lua 生成响应内容
content_by_lua_block {
-- 加载 OpenResty 官方 Redis 客户端
local redis = require "resty.redis"
-- 创建 Redis 对象
local red = redis:new()
-- Redis 连接信息
local redis_ip = "127.0.0.1"
local redis_port = 6379
local redis_timeout = 500
local redis_user = "admin"
local redis_pass = "123456"

-- 关闭redis连接的工具方法,其实是放入连接池
local function close_redis(red)
local pool_max_idle_time = 10000 -- 连接的空闲时间,单位是毫秒
local pool_size = 100 --连接池大小
-- 将连接放回连接池,后续请求可复用
local ok, err = red:set_keepalive(pool_max_idle_time, pool_size)
if not ok then
-- 失败时记录错误日志
ngx.log(ngx.ERR, "放入redis连接池失败: ", err)
end
end

-- 超时(毫秒)
red:set_timeout(redis_timeout)

-- 建立 Redis 连接,若连接池有空闲连接会复用
local ok, err = red:connect(redis_ip, redis_port)
if not ok then
ngx.log(ngx.ERR, "redis connect failed: ", err)
-- 连接失败返回 500 错误码
return ngx.exit(500)
end

-- ACL 认证(仅新连接)
-- 判断是否是新连接,避免重复认证浪费性能
if red:get_reused_times() == 0 then
local ok, err = red:auth(redis_user, redis_pass)
if not ok then
ngx.log(ngx.ERR, "redis auth failed: ", err)
-- 认证失败返回 500 错误码
return ngx.exit(500)
end
end

-- 客户端 IP,这里 remote_addr 是nginx的内置变量
local client_ip = ngx.var.remote_addr or "unknown"

-- 当前分钟窗口
local now = ngx.time() -- 获取当前时间戳(秒)
local minute = math.floor(now / 60) -- 转换为分钟窗口编号

-- Redis Key: 每个 IP 每分钟一个计数器
local key = "rate:" .. client_ip .. ":" .. minute

-- 从共享字典读取 Lua 脚本
local dict = ngx.shared.redis_scripts
local script = dict:get("rate_limit_lua")

-- 执行 Redis Lua(原子)
local ttl = 70
local cnt, err = red:eval(script, 1, key, ttl)
if not cnt then
ngx.log(ngx.ERR, "redis eval failed: ", err)
return ngx.exit(500)
end

-- 限流判断
local limit = 10
if cnt > limit then
ngx.status = 429
ngx.say("Too Many Requests, limit=", limit)
return ngx.exit(429) -- 返回 HTTP 429(Too Many Requests)。
end

-- 放回连接池
close_redis(red)

-- 正常返回
ngx.say("OK, request count=", cnt)
}
}
}
}
  • 启动

1
openresty -p `pwd` -c ./redis-nginx.conf
  • 访问

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
for i in {1..15}; do
curl http://localhost:8080/api
done
# 结果
OK, request count=1
OK, request count=2
OK, request count=3
OK, request count=4
OK, request count=5
OK, request count=6
OK, request count=7
OK, request count=8
OK, request count=9
OK, request count=10
Too Many Requests, limit=10
Too Many Requests, limit=10
Too Many Requests, limit=10
Too Many Requests, limit=10
Too Many Requests, limit=10

生产环境推荐使用 content_by_lua_file

  • 修改 redis-nginx.conf

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
# 指定 Nginx Worker 进程数量,生产环境:等于 CPU 核心数,建议配置为 auto,自动适配
worker_processes auto;
# 指定错误日志路径,所有错误都会写入该日志文件,包括 Lua ngx.log(ngx.ERR, ...)
error_log logs/error.log;
# 定义事件模型参数
events {
# 单个 Worker 最大并发连接数
worker_connections 1024;
}

http {
# Lua 模块加载路径
# 语法规则
# ?.lua 表示模块文件名占位符。
# ;; 表示 保留默认路径,否则会覆盖系统默认路径。
lua_package_path "/usr/local/openresty/lualib/?.lua;;";

# ----------------------------
# Redis 原子限流 Lua 脚本
# ----------------------------

# 创建一块 共享内存区,名称:redis_scripts,大小:1MB(Worker 之间共享内存)
# 常用于:缓存 Lua 脚本、Token、计数器、配置信息
lua_shared_dict redis_scripts 1m;

# Worker 初始化阶段加载 Lua,在 每个 Worker 启动时执行一次
# 适合:加载配置、初始化缓存、预加载脚本、启动定时器
init_worker_by_lua_block {
-- 将 Lua 脚本加载到 Nginx Worker 内存
local script = [[
-- 对指定 Key 进行自增
local cnt = redis.call("INCR", KEYS[1])
-- 如果是第一次创建 Key
if cnt == 1 then
-- 设置 Key 过期时间(秒)
redis.call("EXPIRE", KEYS[1], ARGV[1])
end
-- 返回当前计数
return cnt
]]
-- 将 Lua 脚本存入共享内存,避免每次请求拼接 Lua 脚本字符串,提升性能
local dict = ngx.shared.redis_scripts
-- Key:rate_limit_lua,Value:Lua 脚本
dict:set("rate_limit_lua", script)
}

server {
listen 8080;

location /api {
# 关键修改点:改为加载 Lua 文件
content_by_lua_file /Users/hanqf/Desktop/openresty/lua/rate_limit.lua;
}
}
}
  • rate_limit.lua

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
-- 加载 OpenResty 官方 Redis 客户端
local redis = require "resty.redis"
-- 创建 Redis 对象
local red = redis:new()
-- Redis 连接信息
local redis_ip = "127.0.0.1"
local redis_port = 6379
local redis_timeout = 500
local redis_user = "admin"
local redis_pass = "123456"

-- 关闭redis连接的工具方法,其实是放入连接池
local function close_redis(red)
local pool_max_idle_time = 10000 -- 连接的空闲时间,单位是毫秒
local pool_size = 100 --连接池大小
-- 将连接放回连接池,后续请求可复用
local ok, err = red:set_keepalive(pool_max_idle_time, pool_size)
if not ok then
-- 失败时记录错误日志
ngx.log(ngx.ERR, "放入redis连接池失败: ", err)
end
end

-- 超时(毫秒)
red:set_timeout(redis_timeout)

-- 建立 Redis 连接,若连接池有空闲连接会复用
local ok, err = red:connect(redis_ip, redis_port)
if not ok then
ngx.log(ngx.ERR, "redis connect failed: ", err)
-- 连接失败返回 500 错误码
return ngx.exit(500)
end

-- ACL 认证(仅新连接)
-- 判断是否是新连接,避免重复认证浪费性能
if red:get_reused_times() == 0 then
local ok, err = red:auth(redis_user, redis_pass)
if not ok then
ngx.log(ngx.ERR, "redis auth failed: ", err)
-- 认证失败返回 500 错误码
return ngx.exit(500)
end
end

-- 客户端 IP
local client_ip = ngx.var.remote_addr or "unknown"

-- 当前分钟窗口
local now = ngx.time() -- 获取当前时间戳(秒)
local minute = math.floor(now / 60) -- 转换为分钟窗口编号

-- Redis Key: 每个 IP 每分钟一个计数器
local key = "rate:" .. client_ip .. ":" .. minute

-- 从共享字典读取 Lua 脚本
local dict = ngx.shared.redis_scripts
local script = dict:get("rate_limit_lua")

-- 执行 Redis Lua(原子)
local ttl = 70
local cnt, err = red:eval(script, 1, key, ttl)
if not cnt then
ngx.log(ngx.ERR, "redis eval failed: ", err)
return ngx.exit(500)
end

-- 限流判断
local limit = 10
if cnt > limit then
ngx.status = 429
ngx.say("Too Many Requests, limit=", limit)
return ngx.exit(429) -- 返回 HTTP 429(Too Many Requests)。
end

-- 放回连接池
close_redis(red)

-- 正常返回
ngx.say("OK, request count=", cnt)

改用 access_by_lua_file

  • 前面的配置所有的响应都是由 lua 处理的,但是实际上,用户的请求被限流器放行后应该将请求下发到后端真实的服务,而不是通过 Lua 返回一个状态码或字符串。

  • 生产级网关设计的标准做法: ✅ 凡是“鉴权/限流/风控/灰度/路由决策”逻辑,都应该放在 access 阶段,而不是 content 阶段。

  • 修改 redis-nginx.conf

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
# 指定 Nginx Worker 进程数量,生产环境:等于 CPU 核心数,建议配置为 auto,自动适配
worker_processes auto;
# 指定错误日志路径,所有错误都会写入该日志文件,包括 Lua ngx.log(ngx.ERR, ...)
error_log logs/error.log;
# 定义事件模型参数
events {
# 单个 Worker 最大并发连接数
worker_connections 1024;
}

http {
# Lua 模块加载路径
# 语法规则
# ?.lua 表示模块文件名占位符。
# ;; 表示 保留默认路径,否则会覆盖系统默认路径。
lua_package_path "/usr/local/openresty/lualib/?.lua;;";

# ----------------------------
# Redis 原子限流 Lua 脚本
# ----------------------------

# 创建一块 共享内存区,名称:redis_scripts,大小:1MB(Worker 之间共享内存)
# 常用于:缓存 Lua 脚本、Token、计数器、配置信息
lua_shared_dict redis_scripts 1m;

# Worker 初始化阶段加载 Lua,在 每个 Worker 启动时执行一次
# 适合:加载配置、初始化缓存、预加载脚本、启动定时器
init_worker_by_lua_block {
-- 将 Lua 脚本加载到 Nginx Worker 内存
local script = [[
-- 对指定 Key 进行自增
local cnt = redis.call("INCR", KEYS[1])
-- 如果是第一次创建 Key
if cnt == 1 then
-- 设置 Key 过期时间(秒)
redis.call("EXPIRE", KEYS[1], ARGV[1])
end
-- 返回当前计数
return cnt
]]
-- 将 Lua 脚本存入共享内存,避免每次请求拼接 Lua 脚本字符串,提升性能
local dict = ngx.shared.redis_scripts
-- Key:rate_limit_lua,Value:Lua 脚本
dict:set("rate_limit_lua", script)
}

# 后端服务
upstream backend {
server 127.0.0.1:9000;
}

server {
listen 8080;

location /api {
# 限流在 access 阶段执行
access_by_lua_file /Users/hanqf/Desktop/openresty/lua/rate_limit.lua;

# 通过后转发给后端服务
proxy_pass http://backend;

proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
}
  • ✅ rate_limit.lua 修改

1
2
3
4
-- 将最后的
-- ngx.say("OK, request count=", cnt)
-- 替换为
return -- 放行:什么都不做

后记

应该如何编写滑动窗口限流脚本呢?

  • 目标:

1
限制:最近 60 秒内 ≤ 10 次请求
  • 核心思想:

    • 每次请求记录当前时间戳
    • 删除窗口外的旧记录
    • 统计窗口内请求数量
    • 超过阈值拒绝
  • Redis 数据结构使用:ZSET

1
2
# KEY:rate:IP,SCORE:时间戳,MEMBER:任意唯一值,可以依旧使用时间戳,或者 时间戳_随机数,防止重复
ZADD rate:192.168.1.10 1620000000 1620000000