OpenResty -- Nginx + Lua 访问 Redis
摘要
- 本文介绍如何通过 OpenResty 实现 Nginx + Lua 访问 Redis
- 本文基于
redis-7.4.7 - Redis官网:https://redis.io/
- OpenResty官网:https://openresty.org/
- Lua语法参考
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
-
MacOS 版本:
15.7.3 -
通过 Homebrew 安装
1 | brew tap openresty/brew |
-
安装时报如下错误,提示找不到
GeoIP
GeoIP 是一个 IP 地址 → 地理位置映射库和模块,核心作用是:根据客户端 IP,解析出国家、城市、经纬度、运营商等地理信息。
1 | checking for GeoIP library ... not found |
-
解决办法:这是因为 Homebrew 官方仓库已经不再支持 GeoIP,而是使用 GeoIP2 替代,但 Openresty 暂不支持 GeoIP2,所以这里只能选择不启用 GeoIP 模块。
1 | # 编辑 openresty 的安装脚本 |
-
OpenResty 命令说明
1 | openresty -h |
| 参数 | 完整写法 | 中文含义 | 典型使用场景 | 示例 |
|---|---|---|---|---|
-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 | # 安装目录 |
Linux
1 | sudo yum install -y yum-utils |
-
安装后的目录
1 | # 安装目录 |
一个简单的示例
-
vim test-nginx.conf
1 | worker_processes 1; |
-
实际响应效果,浏览器收到:
1 | HTTP/1.1 200 OK |
-
启动
1 | openresty -p `pwd` -c ./test-nginx.conf |
-
访问
1 | curl http://127.0.0.1:8080 |
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 | server { |
如何获取客户端请求数据?
-
OpenResty 把 HTTP 请求映射为 Lua API,主要分为 5 类
✅ 1. 获取 URL / Query 参数(GET)
-
示例请求
1 | GET /api?user=tom&age=18 |
-
Lua 获取方式
1 | local args = ngx.req.get_uri_args() |
✅ 2. 获取 POST 表单参数(application/x-www-form-urlencoded)
-
示例请求
1 | # 示例请求 |
-
Lua 获取方式
1 | -- 必须先读取 Body |
✅ 3. 获取 JSON Body(application/json)
-
示例请求
1 | # 示例请求 |
-
Lua 获取方式
1 | -- 必须先读取 Body |
✅ 4. 获取 HTTP Headers
1 | -- 获取所有 Header |
✅ 5. 获取请求方法、URI、路径等
1 | -- 比如请求:curl http://127.0.0.1:8080/api\?name\=zhangsan\&age\=20 |
总结
| 数据类型 | 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 | 维度:客户端 IP |
-
Redis Key 设计
1 | rate:{client_ip}:{minute_timestamp} |
-
vim redis-nginx.conf
1 | # 指定 Nginx Worker 进程数量,生产环境:等于 CPU 核心数,建议配置为 auto,自动适配 |
-
启动
1 | openresty -p `pwd` -c ./redis-nginx.conf |
-
访问
1 | for i in {1..15}; do |
生产环境推荐使用 content_by_lua_file
-
修改
redis-nginx.conf
1 | # 指定 Nginx Worker 进程数量,生产环境:等于 CPU 核心数,建议配置为 auto,自动适配 |
-
rate_limit.lua
1 | -- 加载 OpenResty 官方 Redis 客户端 |
改用 access_by_lua_file
-
前面的配置所有的响应都是由 lua 处理的,但是实际上,用户的请求被限流器放行后应该将请求下发到后端真实的服务,而不是通过 Lua 返回一个状态码或字符串。
-
生产级网关设计的标准做法: ✅ 凡是“鉴权/限流/风控/灰度/路由决策”逻辑,都应该放在 access 阶段,而不是 content 阶段。
-
修改
redis-nginx.conf
1 | # 指定 Nginx Worker 进程数量,生产环境:等于 CPU 核心数,建议配置为 auto,自动适配 |
-
✅ rate_limit.lua 修改
1 | -- 将最后的 |
后记
应该如何编写滑动窗口限流脚本呢?
-
目标:
1 | 限制:最近 60 秒内 ≤ 10 次请求 |
-
核心思想:
- 每次请求记录当前时间戳
- 删除窗口外的旧记录
- 统计窗口内请求数量
- 超过阈值拒绝
-
Redis 数据结构使用:
ZSET
1 | # KEY:rate:IP,SCORE:时间戳,MEMBER:任意唯一值,可以依旧使用时间戳,或者 时间戳_随机数,防止重复 |
-
具体的lua实现方法在 Redis 命令详解:Scripting / Functions 命令 中有详细介绍。