简介:本资源是一套面向Web后端开发与高性能服务架构学习者的OpenResty实战案例集,聚焦Nginx+Lua+Redis协同开发场景,适用于具备基础Linux和Web服务器知识的中级开发者,解决高并发下动态逻辑嵌入、缓存联动与轻量级业务编排等典型问题。压缩包共174个文件,含78个核心Lua脚本(实现请求拦截、会话管理、Redis交互等)、55篇Markdown技术笔记(覆盖Nginx 11个处理阶段、Location匹配规则、worker调优及常见陷阱)、12张架构示意图与配置流程图,辅以Shell部署脚本、PDF原理文档及少量PHP/HTML测试页,整体体积11.58MB,结构清晰、即拿即用。目前已有468人学习下载,内容源自一线实践,包含可直接运行的conf配置模板(如lua_request_01_location.conf)、WebSocket集成示例、微信接口模拟及Redis-Iresty封装方案,助读者快速掌握OpenResty生态下的工程化落地路径。
1. 为什么用 Lua + OpenResty + Redis 组合做高性能 Web 服务?不是为了炫技,而是解决真实瓶颈
当你在 Nginx 层需要实时校验用户权限、限流计数、动态路由或缓存穿透防护,又不想把请求打到后端应用服务器——比如 Java 或 Python 服务——那传统proxy_pass就成了性能瓶颈。这时,lua-nginx-openresty-redis不是“可选方案”,而是绕过应用层、在边缘节点完成业务逻辑的刚需路径。OpenResty 把 Lua 嵌入 Nginx 事件循环,Redis 提供毫秒级原子读写,两者结合能实现每秒数万次的令牌桶限流、会话状态同步、API 签名验证等操作,且全程不阻塞 Nginx worker 进程。它适合 API 网关、多租户 SaaS 的租户隔离、游戏登录态校验(如热更新配置下发)、以及需要低延迟响应的 IoT 设备管理接口。新手常误以为这只是“Nginx 写点 Lua 脚本”,实际难点在于:如何避免redis:connect()阻塞、怎样复用连接池、为何init_by_lua*不能访问ngx.var、以及 Redis pipeline 在access_by_lua*中的正确触发时机——这些细节直接决定服务能否扛住 5000 QPS 而不抖动。
2. 搭建最小可运行环境:从 OpenResty 安装到 Redis 连通性验证
2.1 OpenResty 安装与基础配置结构确认
OpenResty 并非 Nginx 插件,而是基于 Nginx 源码深度定制的发行版,自带lua-resty-redis、lua-resty-core等模块。不要用apt install nginx-lua-module或yum install openresty一键包——CentOS 7/8 和 Ubuntu 22.04 的官方仓库版本普遍滞后(如 OpenResty 1.19.x),而lua-resty-redis的连接池自动回收机制在 1.21.4.2+ 才稳定。推荐使用官方预编译包:
# 下载并解压(以 Linux x64 为例,替换为最新版链接) wget https://openresty.org/download/openresty-1.21.4.2.tar.gz tar -xzf openresty-1.21.4.2.tar.gz cd openresty-1.21.4.2 ./configure --prefix=/usr/local/openresty \ --with-http_ssl_module \ --with-http_realip_module \ --with-http_v2_module \ --with-stream \ --with-stream_ssl_module \ --with-stream_realip_module make && sudo make install安装后验证路径和模块加载:
/usr/local/openresty/nginx/sbin/nginx -V 2>&1 | grep -o 'lua' # 应输出 lua,表示 Lua 模块已编译进内核 ls /usr/local/openresty/lualib/resty/ | grep redis # 应看到 redis.lua,即 lua-resty-redis 模块存在注意:OpenResty 的
lualib目录是 Lua 模块默认搜索路径,/usr/local/openresty/lualib/resty/redis.lua是核心驱动,所有redis:new()实例都依赖它。若手动修改lua_package_path,必须包含该路径,否则require "resty.redis"会报module 'resty.redis' not found。
2.2 Redis 服务就绪与连接池参数设计
Redis 推荐使用 6.2+ 版本(支持 RESP3 协议和更细粒度的客户端命令统计),本地开发可用 Docker 快速启动:
docker run -d --name my-redis -p 6379:6379 -e REDIS_PASSWORD=dev123 redis:6.2-alpine # 验证连通性 redis-cli -h 127.0.0.1 -p 6379 -a dev123 ping # 返回 PONG 即成功关键不是“能连上”,而是连接池配置是否匹配 Nginx worker 数量与业务并发模型。lua-resty-redis的set_keepalive()方法将连接放回池中,但池大小由pool_size和pool_timeout控制。常见错误是设pool_size = 100却只启动 2 个 worker,导致连接争抢;或pool_timeout = 1秒太短,频繁重建连接。合理值需按公式估算:pool_size ≥ (单 worker 平均并发请求数) × (worker 数)
例如:4 核机器开 4 个 worker,每个 worker 平均处理 50 个并发 Redis 请求,则pool_size ≥ 200。pool_timeout建议设为 60 秒,避免空闲连接长期占用。
2.3 Nginx 配置文件结构与 Lua 加载时机
OpenResty 的配置分四层执行时机,必须严格对应业务逻辑位置:
init_by_lua_block:Master 进程启动时执行一次,适合初始化全局变量、加载配置文件(如 JSON)、预编译正则;init_worker_by_lua_block:每个 Worker 进程启动时执行,适合创建共享内存 zone、初始化 Redis 连接池;set_by_lua_block:在location内执行,用于设置变量(如set $user_id ...),不可调用阻塞 I/O;access_by_lua_block:认证/鉴权逻辑入口,可调用 Redis 同步操作(如查 token);content_by_lua_block:生成响应体,适合复杂业务逻辑(如拼装 JSON)。
一个典型nginx.conf片段:
# /usr/local/openresty/nginx/conf/nginx.conf worker_processes 4; events { worker_connections 1024; } http { # 全局 Lua 模块路径(可选,因 OpenResty 默认已包含) lua_package_path "/usr/local/openresty/lualib/?.lua;;"; # 初始化 Redis 连接池(每个 worker 独立池) init_worker_by_lua_block { local redis = require "resty.redis" local red = redis:new() red:set_timeouts(1000, 1000, 1000) -- connect, send, read timeout (ms) -- 将连接池存入全局 table,供后续 access_by_lua 复用 ngx.ctx.redis_pool = red } server { listen 8000; location /api/user { # 认证阶段:查 Redis 获取用户信息 access_by_lua_block { local red = ngx.ctx.redis_pool local ok, err = red:connect("127.0.0.1", 6379) if not ok then ngx.log(ngx.ERR, "failed to connect to redis: ", err) return ngx.exit(500) end -- 设置密码(若 Redis 配置了 requirepass) red:auth("dev123") -- 查询 key,此处用 user:123 模拟 local res, err = red:get("user:" .. ngx.var.arg_id) if not res then ngx.log(ngx.WARN, "redis get failed: ", err) return ngx.exit(401) end -- 将结果存入 ngx.ctx,供 content_by_lua 使用 ngx.ctx.user_data = res } # 响应生成 content_by_lua_block { ngx.say("User data: ", ngx.ctx.user_data or "not found") } } } }提示:
ngx.ctx是每个请求的上下文表,生命周期与 request 绑定,比ngx.shared.DICT更轻量,适合临时传递数据。但切记ngx.ctx不能跨location传递——access_by_lua和content_by_lua在同一location内才共享。
3. 实现高可用 Redis 操作:连接池复用、超时控制与错误降级
3.1lua-resty-redis连接池的正确打开方式
直接调用redis:new():connect()每次新建 TCP 连接,QPS 超过 1000 就会触发Too many open files错误。必须通过set_keepalive()将连接归还池中。标准流程如下:
local redis = require "resty.redis" local red = redis:new() -- 1. 设置超时(单位:毫秒),必须在 connect 前设置 red:set_timeouts(100, 100, 100) -- connect/send/read 各 100ms -- 2. 连接 Redis(失败时返回 nil + err) local ok, err = red:connect("127.0.0.1", 6379) if not ok then ngx.log(ngx.ERR, "redis connect error: ", err) return end -- 3. 认证(如果 Redis 配置了密码) ok, err = red:auth("dev123") if not ok then ngx.log(ngx.ERR, "redis auth error: ", err) return end -- 4. 执行命令(如 GET) local res, err = red:get("key:123") if not res then ngx.log(ngx.WARN, "redis get error: ", err) -- 此处可降级为本地缓存或默认值 res = "default_value" end -- 5. 关键!将连接放回池中,而非 close() -- 第一个参数:空闲连接最大存活时间(秒),第二个:连接池最大容量 local ok, err = red:set_keepalive(60000, 200) -- 60s, 最多 200 个连接 if not ok then ngx.log(ngx.ERR, "failed to set keepalive: ", err) endset_keepalive(60000, 200)中的60000是毫秒单位(即 60 秒),200是该 worker 进程内连接池的最大连接数。若池满,set_keepalive()会立即关闭连接并返回false,此时应记录日志并考虑扩容pool_size。
3.2 Redis 命令批量执行与 pipeline 优化
单次GET/SET往返耗时约 0.5~2ms,当需查 10 个 key 时,串行调用 10 次就是 5~20ms。改用 pipeline 可压缩为一次往返:
-- 错误示范:10 次独立请求 for i = 1, 10 do local res, err = red:get("user:" .. i) end -- 正确:pipeline 批量获取 local results = {} local ok, err = red:init_pipeline() if not ok then ngx.log(ngx.ERR, "pipeline init failed: ", err) return end -- 添加 10 个 GET 命令到 pipeline for i = 1, 10 do red:get("user:" .. i) end -- 执行所有命令,返回结果数组 results, err = red:commit_pipeline() if not results then ngx.log(ngx.ERR, "pipeline commit failed: ", err) return end -- results 是 table,索引 1~10 对应各 GET 结果 for i, res in ipairs(results) do ngx.log(ngx.INFO, "user:", i, " data: ", res) endcommit_pipeline()返回的是结果数组,每个元素对应 pipeline 中第 n 条命令的返回值。若某条命令出错(如 key 不存在),对应位置为nil,不会中断整个 pipeline。相比串行,pipeline 可将 10 次操作的 RTT 从 10×RTT 降至 1×RTT,实测 QPS 提升 3~5 倍。
3.3 连接失败时的优雅降级策略
生产环境 Redis 可能瞬时不可用(网络抖动、主从切换)。硬抛 500 错误会直接导致 API 不可用。应实现三级降级:
- 本地内存缓存:用
ngx.shared.DICT存最近成功结果,TTL 缩短为 10 秒; - 静态默认值:如用户信息缺失时返回
{ "id": 0, "name": "guest" }; - 异步上报:记录错误到日志,并触发告警(如调用内部 HTTP 告警接口)。
示例降级代码:
local dict = ngx.shared.my_cache local cache_key = "user:" .. ngx.var.arg_id -- 1. 先查本地共享字典 local cached = dict:get(cache_key) if cached then ngx.log(ngx.INFO, "hit local cache for ", cache_key) ngx.ctx.user_data = cached return end -- 2. 再查 Redis local red = ngx.ctx.redis_pool local res, err = red:get(cache_key) if not res then ngx.log(ngx.WARN, "redis get failed: ", err) -- 3. 降级:返回默认用户 ngx.ctx.user_data = '{"id":0,"name":"guest","role":"visitor"}' -- 4. 异步上报(非阻塞) ngx.timer.at(0, function() ngx.log(ngx.ERR, "Redis failover triggered for key: ", cache_key) end) return end -- 5. 写入本地缓存(TTL 10 秒) dict:set(cache_key, res, 10) ngx.ctx.user_data = resngx.shared.DICT是基于 slab 分配器的共享内存,线程安全且零拷贝,比ngx.var或 Lua table 更适合缓存。set(key, value, exptime)的exptime单位是秒,支持小数(如0.1表示 100ms)。
4. 调试与排错:定位 Lua 脚本阻塞、Redis 连接泄漏与 Nginx 日志分析
4.1 Lua 脚本阻塞检测:用ngx.timer.every监控执行时长
Lua 代码若含死循环或未设超时的socket:receive(),会阻塞整个 worker 进程。OpenResty 提供ngx.on_abort()和ngx.timer.every辅助诊断:
-- 在 init_worker_by_lua_block 中注册监控 init_worker_by_lua_block { local delay = 1 -- 每秒检查一次 local timer = nil local function check_long_running() -- 获取当前 worker 的活跃 timer 数量(异常值 > 100 表示可能泄漏) local active_timers = ngx.worker.count_active_timers() if active_timers > 100 then ngx.log(ngx.ERR, "too many active timers: ", active_timers) end end timer = ngx.timer.every(delay, check_long_running) }更直接的方式是开启nginx -t时的 debug 日志:
error_log /usr/local/openresty/nginx/logs/error.log debug;然后在access_by_lua_block开头加:
local start_time = ngx.now() -- ... 业务逻辑 ... local cost = ngx.now() - start_time if cost > 0.1 then -- 超过 100ms 记录警告 ngx.log(ngx.WARN, "slow lua execution: ", cost, "s for ", ngx.var.uri) end4.2 Redis 连接泄漏排查:netstat与redis-cli client list双验证
连接池未正确set_keepalive()会导致 ESTABLISHED 连接数持续增长。先查 Nginx 侧:
# 查看 nginx worker 进程打开的 socket 数量 sudo lsof -p $(pgrep -f "nginx: worker") | grep "TCP.*:6379" | wc -l # 若持续 > 200(假设 pool_size=200),说明连接未归还再查 Redis 侧:
redis-cli -h 127.0.0.1 -p 6379 -a dev123 client list | wc -l # 正常应 ≈ worker 数 × pool_size,若远大于此,确认 Lua 是否漏掉 set_keepalive关键检查点:所有red:connect()后必须有对应的red:set_keepalive(),即使connect()失败也要red:close()(避免 fd 泄漏):
local red = redis:new() local ok, err = red:connect("127.0.0.1", 6379) if not ok then red:close() -- 必须关闭未成功的连接 return end -- ... 后续操作 ... red:set_keepalive(60000, 200) -- 成功后归还4.3 Nginx 日志字段定制与 Redis 错误归因
默认log_format不包含 Lua 变量,需自定义格式捕获 Redis 状态:
log_format detailed '$remote_addr - $remote_user [$time_local] ' '"$request" $status $body_bytes_sent ' '"$http_referer" "$http_user_agent" ' 'rt=$request_time uct="$upstream_connect_time" ' 'uht="$upstream_header_time" urt="$upstream_response_time" ' 'redis_status="$redis_status" redis_cost="$redis_cost"'; server { access_log /usr/local/openresty/nginx/logs/access.log detailed; location /api/data { access_by_lua_block { local start = ngx.now() local red = ngx.ctx.redis_pool local res, err = red:get("data:" .. ngx.var.arg_id) local cost = ngx.now() - start -- 将 Redis 状态注入日志变量 ngx.var.redis_status = res and "success" or "fail" ngx.var.redis_cost = string.format("%.3f", cost) } } }重启 Nginx 后,日志中会出现redis_status="fail"和redis_cost="0.123"字段,配合grep "redis_status=\"fail\"" /path/to/access.log | awk '{print $NF}' | sort | uniq -c | sort -nr可快速定位高频失败接口。
5. 生产级进阶技巧:基于 Redis 的分布式限流与 Lua 脚本原子操作
5.1 使用 Redis EVAL 实现令牌桶限流(无第三方模块)
OpenResty 自带redis:eval()支持 Lua 脚本原子执行,规避GET + INCR的竞态问题。以下是一个每分钟最多 100 次请求的令牌桶脚本:
-- 限流脚本(保存为 /usr/local/openresty/lua/rate_limit.lua) local key = KEYS[1] -- 如 "rate:192.168.1.100" local limit = tonumber(ARGV[1]) -- 每分钟限额 local window = tonumber(ARGV[2]) -- 时间窗口秒数(60) local now = tonumber(ARGV[3]) -- 当前时间戳(秒) -- Redis 中存储 {last_time, tokens},用 HINCRBY 模拟浮点数 local last_time, tokens = unpack(redis.call("HMGET", key, "last_time", "tokens")) last_time = last_time and tonumber(last_time) or 0 tokens = tokens and tonumber(tokens) or limit -- 计算已过去时间,补充令牌 local elapsed = now - last_time local new_tokens = math.min(limit, tokens + elapsed * limit / window) -- 判断是否允许请求 if new_tokens >= 1 then -- 消费一个令牌 redis.call("HSET", key, "last_time", now, "tokens", new_tokens - 1) return 1 -- 允许 else -- 拒绝并设置过期(避免 key 永久存在) redis.call("EXPIRE", key, window) return 0 -- 拒绝 end在access_by_lua_block中调用:
local red = ngx.ctx.redis_pool local key = "rate:" .. ngx.var.remote_addr local limit = 100 local window = 60 local now = ngx.time() local allowed, err = red:eval([[ local key = KEYS[1] local limit = tonumber(ARGV[1]) local window = tonumber(ARGV[2]) local now = tonumber(ARGV[3]) local last_time, tokens = unpack(redis.call("HMGET", key, "last_time", "tokens")) last_time = last_time and tonumber(last_time) or 0 tokens = tokens and tonumber(tokens) or limit local elapsed = now - last_time local new_tokens = math.min(limit, tokens + elapsed * limit / window) if new_tokens >= 1 then redis.call("HSET", key, "last_time", now, "tokens", new_tokens - 1) return 1 else redis.call("EXPIRE", key, window) return 0 end ]], 1, key, limit, window, now) if not allowed or allowed == 0 then ngx.status = 429 ngx.header["X-RateLimit-Remaining"] = "0" ngx.say("Too Many Requests") return ngx.exit(429) end注意:
redis:eval()的KEYS参数必须是 Redis key 名(不能含表达式),ARGV传数值。脚本在 Redis 服务端原子执行,无需担心并发覆盖。
5.2 Redis 数据类型选择指南:何时用 Hash、Sorted Set 或 Bitmap
| 场景 | 推荐类型 | Lua 操作示例 | 优势 |
|---|---|---|---|
| 用户属性存储(name/age/email) | Hash | red:hset("user:123", "name", "Alice", "age", "25") | 字段级更新,节省内存 |
| 实时排行榜(按分数排序) | Sorted Set | red:zadd("leaderboard", score, "user:123") | O(log N) 插入/查询,支持范围扫描 |
| 日活用户去重(百万级) | Bitmap | red:setbit("active:20240501", user_id, 1) | 单 bit 存储,100 万用户仅占 ~125KB |
| 简单开关配置(feature flag) | String | red:set("feature:payment", "on") | 最简结构,读写最快 |
Bitmap 特别适合统计类场景:red:bitcount("active:20240501")可秒级返回当日 DAU,比SCARD集合节省 90% 内存。
5.3 OpenResty 刷新 404 问题的根因与修复
“OpenResty 刷新 404” 是高频问题,本质是location匹配失败或content_by_lua未输出内容。排查步骤:
- 确认
location正则是否贪婪:location ~ ^/api/.*会匹配/api/,但location /api/是前缀匹配,更安全; - 检查
content_by_lua_block是否有ngx.say()或ngx.print():若逻辑走到末尾无输出,Nginx 默认返回 404; - 验证
init_by_lua是否误用了ngx.exit():init_by_lua中调用ngx.exit()会导致整个进程退出; - 查看
error.log中是否有no resolver defined:若 Lua 脚本中用了http.request()但未配resolver,会静默失败。
最小修复模板:
location /api/test { # 显式指定 resolver(避免 DNS 问题) resolver 8.8.8.8 valid=30s; content_by_lua_block { ngx.say("Hello from OpenResty + Redis!") -- 必须有输出,否则 404 } }若仍 404,用curl -v http://localhost:8000/api/test查看响应头,确认Content-Length是否为 0。
本文还有配套的精品资源,点击获取