lolly/docs/nginx/26-nginx-lua-guide.md
xfy 972eab4267 refactor(docs): 重构文档目录结构,nginx 文档移至子目录
将 docs/ 根目录下的 nginx 相关文档统一移动到 docs/nginx/ 子目录,
提高文档组织性和可维护性。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-16 10:48:14 +08:00

1672 lines
41 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# NGINX Lua 模块深度指南
## 概述
NGINX Lua 模块ngx_http_lua_module是 OpenResty 平台的核心组件,它将 Lua 脚本语言嵌入 NGINX使 NGINX 具备强大的动态脚本能力。本文档深入介绍 Lua 模块的核心概念、API 和最佳实践。
---
## 1. OpenResty 简介
### 1.1 什么是 OpenResty
OpenResty 是一个基于 NGINX 的高性能 Web 平台,由 agentzh章亦春创建。它在标准 NGINX 基础上集成了:
- **LuaJIT**:高性能 Lua 解释器,执行速度接近原生代码
- **ngx_lua**NGINX Lua 嵌入模块
- **丰富的 Lua 库**Redis、MySQL、Memcached 等客户端库
- **协程调度器**:异步非阻塞 I/O 支持
### 1.2 OpenResty 与传统 NGINX 的区别
| 特性 | 标准 NGINX | OpenResty |
|------|-----------|-----------|
| 脚本能力 | 有限NJS | 强大(完整 Lua 支持) |
| 性能 | 高 | 更高LuaJIT |
| 生态系统 | 模块扩展 | 丰富的 Lua 库 |
| 学习曲线 | 平缓 | 中等 |
| 典型应用 | 反向代理 | API 网关、WAF、边缘计算 |
### 1.3 安装 OpenResty
#### 使用包管理器安装
**Ubuntu/Debian**
```bash
# 添加 OpenResty 仓库
wget -O - https://openresty.org/package/pubkey.gpg | sudo apt-key add -
sudo add-apt-repository -y "deb http://openresty.org/package/ubuntu $(lsb_release -sc) main"
# 安装
sudo apt-get update
sudo apt-get install -y openresty
```
**CentOS/RHEL**
```bash
# 添加仓库
sudo yum install -y yum-utils
sudo yum-config-manager --add-repo https://openresty.org/package/centos/openresty.repo
# 安装
sudo yum install -y openresty
```
**macOS**
```bash
brew install openresty
```
#### 从源码编译安装
```bash
# 下载源码
wget https://openresty.org/download/openresty-1.25.3.1.tar.gz
tar -xzf openresty-1.25.3.1.tar.gz
cd openresty-1.25.3.1
# 配置编译选项
./configure \
--prefix=/usr/local/openresty \
--with-http_ssl_module \
--with-http_v2_module \
--with-http_v3_module \
--with-http_realip_module \
--with-http_stub_status_module \
--with-http_sub_module \
--with-pcre-jit \
--with-luajit
# 编译安装
make -j$(nproc)
sudo make install
# 添加到环境变量
echo 'export PATH=/usr/local/openresty/bin:$PATH' >> ~/.bashrc
source ~/.bashrc
```
#### 验证安装
```bash
# 检查版本
openresty -v
# 输出nginx version: openresty/1.25.3.1
# 检查 LuaJIT
openresty -V 2>&1 | grep luajit
# 确认包含 --with-luajit
# 启动 OpenResty
sudo openresty
# 测试
curl http://localhost/
```
---
## 2. ngx_lua 核心指令
ngx_lua 提供了多个执行阶段指令,允许在不同请求处理阶段执行 Lua 代码。
### 2.1 执行阶段概览
```
请求处理流程:
┌─────────────────────────────────────────┐
│ init_by_lua │ ← NGINX 启动时
├─────────────────────────────────────────┤
│ init_worker_by_lua │ ← 每个 worker 启动时
├─────────────────────────────────────────┤
│ ssl_certificate_by_lua │ ← SSL 证书阶段(可选)
├─────────────────────────────────────────┤
│ set_by_lua │ 设置变量值 │
├─────────────────────────────────────────┤
│ rewrite_by_lua │ URL 重写 │
├─────────────────────────────────────────┤
│ access_by_lua │ 访问控制 │
├─────────────────────────────────────────┤
│ content_by_lua │ 生成响应内容 │
├─────────────────────────────────────────┤
│ header_filter_by_lua │ 处理响应头 │
├─────────────────────────────────────────┤
│ body_filter_by_lua │ 处理响应体 │
├─────────────────────────────────────────┤
│ log_by_lua │ 日志阶段 │
└─────────────────────────────────────────┘
```
### 2.2 指令详解
#### init_by_lua
在 NGINX 启动时执行,用于全局初始化。
```nginx
http {
# 加载 Lua 模块,预编译代码
init_by_lua_block {
require "cjson"
require "resty.redis"
require "resty.mysql"
-- 预编译正则表达式
local regex = [[\d+]]
local m, err = ngx.re.match("hello 123", regex, "jo")
-- 全局配置
config = {
redis_host = "127.0.0.1",
redis_port = 6379,
cache_ttl = 300
}
-- 打印启动信息
ngx.log(ngx.NOTICE, "OpenResty initialized with LuaJIT")
}
}
```
**适用场景:**
- 预加载常用模块
- 初始化全局变量/配置
- 建立数据库连接池
- 编译正则表达式
#### init_worker_by_lua
在每个 worker 进程启动时执行。
```nginx
http {
init_worker_by_lua_block {
local delay = 5 -- 5秒间隔
local handler
-- 定时任务:健康检查
handler = function(premature)
if premature then
return
end
-- 执行健康检查
local http = require "resty.http"
local httpc = http.new()
local res, err = httpc:request_uri("http://127.0.0.1:8080/health", {
method = "GET",
timeout = 2000
})
if res and res.status == 200 then
ngx.log(ngx.INFO, "Health check passed")
else
ngx.log(ngx.ERR, "Health check failed: ", err)
end
-- 重新注册定时器
local ok, err = ngx.timer.at(delay, handler)
if not ok then
ngx.log(ngx.ERR, "Failed to create timer: ", err)
end
end
-- 启动定时器
ngx.timer.at(delay, handler)
ngx.log(ngx.NOTICE, "Worker ", ngx.worker.id(), " started")
}
}
```
**适用场景:**
- 启动后台定时任务
- worker 级别的初始化
- 定时数据采集
- 缓存预热
#### set_by_lua
设置 NGINX 变量值。
```nginx
location /api {
set_by_lua_block $api_backend {
local version = ngx.var.http_x_api_version
if version == "v2" then
return "backend_v2"
else
return "backend_v1"
end
}
proxy_pass http://$api_backend;
}
```
**限制:**
- 不能使用阻塞 I/O
- 不能使用 `ngx.sleep`
- 不能使用 cosocket
#### rewrite_by_lua
在 rewrite 阶段执行,用于 URL 重写和重定向。
```nginx
location / {
rewrite_by_lua_block {
local uri = ngx.var.uri
local args = ngx.var.args
-- 统一处理尾部斜杠
if uri ~= "/" and uri:sub(-1) == "/" then
uri = uri:sub(1, -2)
end
-- URL 兼容处理
if uri:match("^/old%-api/") then
local new_uri = uri:gsub("^/old%-api", "/api/v1")
return ngx.redirect(new_uri .. (args and "?" .. args or ""), 301)
end
-- 设置内部变量
ngx.var.target_service = "user_service"
}
proxy_pass http://backend;
}
```
#### access_by_lua
在 access 阶段执行,用于访问控制和认证。
```nginx
location /admin {
access_by_lua_block {
local token = ngx.var.http_authorization
if not token then
ngx.header["WWW-Authenticate"] = "Bearer"
return ngx.exit(ngx.HTTP_UNAUTHORIZED)
end
-- JWT 验证
local jwt = require "resty.jwt"
local jwt_obj = jwt:verify("secret_key", token:gsub("Bearer ", ""))
if not jwt_obj.verified then
ngx.log(ngx.ERR, "JWT verification failed: ", jwt_obj.reason)
return ngx.exit(ngx.HTTP_UNAUTHORIZED)
end
-- 设置用户信息到变量
ngx.var.user_id = jwt_obj.payload.sub
ngx.var.user_role = jwt_obj.payload.role
}
proxy_pass http://admin_backend;
}
```
#### content_by_lua
生成响应内容的核心指令。
```nginx
location /api/status {
content_by_lua_block {
local cjson = require "cjson"
local status = {
nginx_version = ngx.var.nginx_version,
lua_version = _VERSION,
worker_id = ngx.worker.id(),
worker_count = ngx.worker.count(),
time = ngx.time(),
connections = ngx.var.connections_active
}
ngx.header["Content-Type"] = "application/json"
ngx.say(cjson.encode(status))
}
}
```
#### header_filter_by_lua
修改响应头。
```nginx
location / {
proxy_pass http://backend;
header_filter_by_lua_block {
-- 添加安全头部
ngx.header["X-Frame-Options"] = "SAMEORIGIN"
ngx.header["X-XSS-Protection"] = "1; mode=block"
ngx.header["X-Content-Type-Options"] = "nosniff"
-- 移除敏感头部
ngx.header["Server"] = nil
ngx.header["X-Powered-By"] = nil
-- 根据条件设置缓存
if ngx.var.uri:match("^/api/") then
ngx.header["Cache-Control"] = "no-store, no-cache, must-revalidate"
end
}
}
```
#### body_filter_by_lua
修改响应体(基于流式处理)。
```nginx
location / {
proxy_pass http://backend;
body_filter_by_lua_block {
local chunk = ngx.arg[1]
local eof = ngx.arg[2]
-- 累积响应体
if ngx.ctx.body then
ngx.ctx.body = ngx.ctx.body .. chunk
else
ngx.ctx.body = chunk
end
-- 最后一块数据处理
if eof then
local body = ngx.ctx.body
-- 敏感信息脱敏
body = body:gsub("\"phone\":\"(%d%d%d)%d%d%d%d(\d%d%d%d)\"",
"\"phone\":\"%1****%2\"")
body = body:gsub("\"email\":\"([^@]+)@[^\"]+\"",
"\"email\":\"%1@***\"")
ngx.arg[1] = body
else
-- 非最后一块,输出空并标记不完成
ngx.arg[1] = nil
ngx.arg[2] = false
end
}
}
```
#### log_by_lua
日志阶段处理。
```nginx
http {
log_by_lua_block {
local uri = ngx.var.uri
local status = ngx.var.status
local request_time = ngx.var.request_time
-- 慢请求记录
if tonumber(request_time) > 1.0 then
ngx.log(ngx.WARN, "Slow request: ", uri,
" status=", status,
" time=", request_time)
end
-- 统计信息发送到监控
if status >= 500 then
local statsd = require "resty.statsd"
statsd.increment("nginx.error." .. status)
end
}
}
```
### 2.3 指令上下文支持
| 指令 | http | server | location | upstream |
|------|------|--------|----------|----------|
| `init_by_lua` | ✓ | ✗ | ✗ | ✗ |
| `init_worker_by_lua` | ✓ | ✗ | ✗ | ✗ |
| `set_by_lua` | ✗ | ✓ | ✓ | ✗ |
| `rewrite_by_lua` | ✓ | ✓ | ✓ | ✗ |
| `access_by_lua` | ✓ | ✓ | ✓ | ✗ |
| `content_by_lua` | ✗ | ✗ | ✓ | ✗ |
| `header_filter_by_lua` | ✓ | ✓ | ✓ | ✗ |
| `body_filter_by_lua` | ✓ | ✓ | ✓ | ✗ |
| `log_by_lua` | ✓ | ✓ | ✓ | ✗ |
| `balancer_by_lua` | ✗ | ✗ | ✗ | ✓ |
---
## 3. Lua 共享字典ngx.shared.DICT
共享字典是在所有 worker 进程间共享的内存缓存。
### 3.1 定义共享字典
```nginx
http {
# 语法lua_shared_dict <name> <size>
lua_shared_dict cache 10m; # 通用缓存
lua_shared_dict sessions 5m; # 会话存储
lua_shared_dict rate_limit 1m; # 限流计数
lua_shared_dict locks 1m; # 锁存储
}
```
**内存估算:**
- 每个 key-value 对约占用 60-80 字节(小数据)
- 1MB 可存储约 12,000-16,000 个简单键值对
### 3.2 基本操作
```lua
-- 获取字典实例
local cache = ngx.shared.cache
-- 存储数据(支持过期时间)
cache:set("key", "value", 300) -- 300秒后过期
cache:set("key", "value", 0) -- 永不过期
-- 带过期时间的存储
cache:set("session:123", user_data, 1800) -- 30分钟会话
-- 获取数据
local value, flags = cache:get("key")
if value then
ngx.say("Value: ", value)
else
ngx.say("Cache miss")
end
-- 删除数据
cache:delete("key")
-- 原子递增(用于计数器)
local new_val, err = cache:incr("counter", 1, 0) -- 从0开始每次+1
-- 批量获取
cache:set("user:1", "Alice")
cache:set("user:2", "Bob")
cache:set("user:3", "Charlie")
local keys = {"user:1", "user:2", "user:3"}
local values = cache:get(keys)
```
### 3.3 高级操作
```lua
local cache = ngx.shared.cache
-- 安全添加(仅当 key 不存在时设置)
local success, err, forcible = cache:add("key", "value", 300)
if not success then
ngx.log(ngx.ERR, "Failed to add: ", err)
end
-- 安全替换(仅当 key 存在时设置)
local success = cache:replace("key", "new_value")
-- 原子操作:如果不存在则设置
local ok = cache:add("lock:process", "1", 10)
if ok then
-- 获取到锁
-- 执行操作
cache:delete("lock:process")
end
-- 获取过期时间
local ttl, err = cache:ttl("key")
if ttl then
ngx.say("TTL: ", ttl, " seconds")
end
-- 获取信息
local info = cache:get_keys(0) -- 0 表示获取所有 keys
ngx.say("Keys count: ", #info)
-- 清空字典(慎用)
cache:flush_all()
-- 过期数据清理
cache:flush_expired(100) -- 清理最多100个过期条目
```
### 3.4 应用场景
#### 分布式限流
```nginx
http {
lua_shared_dict rate_limit 10m;
server {
location /api {
access_by_lua_block {
local limit = ngx.shared.rate_limit
local key = "rate:" .. ngx.var.binary_remote_addr
-- 令牌桶算法实现
local now = ngx.time()
local rate = 10 -- 每秒10个请求
local burst = 20 -- 突发容量
local last = limit:get(key .. ":last")
local tokens = limit:get(key .. ":tokens")
if not last then
tokens = burst
else
local elapsed = now - last
tokens = math.min(burst, tokens + elapsed * rate)
end
if tokens < 1 then
return ngx.exit(ngx.HTTP_TOO_MANY_REQUESTS)
end
tokens = tokens - 1
limit:set(key .. ":tokens", tokens)
limit:set(key .. ":last", now)
}
proxy_pass http://api_backend;
}
}
}
```
#### 会话存储
```nginx
http {
lua_shared_dict sessions 20m;
server {
location /login {
content_by_lua_block {
local cjson = require "cjson"
local sessions = ngx.shared.sessions
-- 验证用户名密码
local username = ngx.var.arg_username
local password = ngx.var.arg_password
if not authenticate(username, password) then
return ngx.exit(ngx.HTTP_UNAUTHORIZED)
end
-- 创建会话
local session_id = ngx.md5(ngx.time() .. ngx.var.remote_addr)
local session_data = {
username = username,
login_time = ngx.time(),
ip = ngx.var.remote_addr
}
sessions:set("session:" .. session_id,
cjson.encode(session_data), 3600)
-- 设置 Cookie
ngx.header["Set-Cookie"] = "session=" .. session_id ..
"; Path=/; HttpOnly; Secure"
ngx.say("Login successful")
}
}
location /profile {
access_by_lua_block {
local cjson = require "cjson"
local sessions = ngx.shared.sessions
-- 获取 session
local cookie = ngx.var.cookie_session
if not cookie then
return ngx.exit(ngx.HTTP_UNAUTHORIZED)
end
local data = sessions:get("session:" .. cookie)
if not data then
return ngx.exit(ngx.HTTP_UNAUTHORIZED)
end
-- 解析会话数据
local session = cjson.decode(data)
ngx.var.user_name = session.username
}
proxy_pass http://backend;
}
}
}
```
#### 缓存穿透防护
```lua
-- 缓存穿透防护(防止缓存击穿和雪崩)
local function get_with_lock(cache, key, ttl, fetch_func)
-- 1. 尝试从缓存获取
local value = cache:get(key)
if value then
return value
end
local lock_key = "lock:" .. key
local lock_ttl = 10 -- 锁超时时间
-- 2. 尝试获取锁
local ok = cache:add(lock_key, "1", lock_ttl)
if not ok then
-- 3. 未获取到锁,等待后重试
ngx.sleep(0.1)
return cache:get(key)
end
-- 4. 获取到锁,从数据源加载
local ok2, result = pcall(fetch_func)
if ok2 and result then
cache:set(key, result, ttl)
end
-- 5. 释放锁
cache:delete(lock_key)
return result
end
-- 使用示例
local value = get_with_lock(ngx.shared.cache, "user:123", 300, function()
-- 从数据库获取
return fetch_from_db("user", 123)
end)
```
---
## 4. Cosocket API非阻塞网络 I/O
Cosocket 是 OpenResty 提供的非阻塞网络 I/O 接口,支持 TCP、UDP 和 Unix Domain Socket。
### 4.1 TCP Cosocket
```lua
-- 创建 TCP socket
local sock = ngx.socket.tcp()
-- 设置超时
sock:settimeout(5000) -- 5秒超时
-- 连接服务器
local ok, err = sock:connect("127.0.0.1", 6379)
if not ok then
ngx.log(ngx.ERR, "Failed to connect: ", err)
return
end
-- 发送数据
local bytes, err = sock:send("PING\r\n")
if not bytes then
ngx.log(ngx.ERR, "Failed to send: ", err)
return
end
-- 接收数据
local line, err = sock:receive("*l") -- 接收一行
if not line then
ngx.log(ngx.ERR, "Failed to receive: ", err)
return
end
ngx.say("Response: ", line)
-- 关闭连接
sock:close()
```
### 4.2 高级用法
```lua
local sock = ngx.socket.tcp()
-- 连接池复用
sock:setkeepalive(60000, 100) -- 60秒超时最多100个连接
-- 指定模式接收
local data, err = sock:receive(1024) -- 接收最多1024字节
local data, err = sock:receive("*a") -- 接收所有数据
local data, err = sock:receiveuntil("\r\n") -- 接收直到指定分隔符
-- 批量发送
local ok, err = sock:send({
"GET / HTTP/1.1\r\n",
"Host: example.com\r\n",
"Connection: close\r\n",
"\r\n"
})
```
### 4.3 UDP Cosocket
```lua
local sock = ngx.socket.udp()
-- 设置超时
sock:settimeout(2000)
-- 设置目标
local ok, err = sock:setpeername("127.0.0.1", 53)
if not ok then
ngx.log(ngx.ERR, "Failed to set peer: ", err)
return
end
-- 发送 DNS 查询
local query = build_dns_query("example.com")
local ok, err = sock:send(query)
-- 接收响应
local data, err = sock:receive(512)
if data then
local result = parse_dns_response(data)
ngx.say("IP: ", result)
end
sock:close()
```
### 4.4 异步 HTTP 请求
使用 `resty.http` 库进行异步 HTTP 请求:
```bash
# 安装 lua-resty-http
luarocks install lua-resty-http
```
```lua
local http = require "resty.http"
-- 简单 GET 请求
local httpc = http.new()
local res, err = httpc:request_uri("http://api.example.com/data", {
method = "GET",
headers = {
["Accept"] = "application/json"
}
})
if res then
ngx.status = res.status
ngx.say(res.body)
else
ngx.status = 502
ngx.say("Request failed: ", err)
end
```
#### 高级 HTTP 请求
```lua
local http = require "resty.http"
local httpc = http.new()
-- 配置超时
httpc:set_timeout(5000)
-- 建立连接(连接复用)
local ok, err = httpc:connect("api.example.com", 443)
if not ok then
return ngx.exit(ngx.HTTP_BAD_GATEWAY)
end
-- SSL 握手
local session, err = httpc:ssl_handshake(false, "api.example.com", false)
-- 发送请求
local res, err = httpc:request({
method = "POST",
path = "/v1/users",
headers = {
["Content-Type"] = "application/json",
["Authorization"] = "Bearer " .. token
},
body = [[{"name":"John","email":"john@example.com"}]]
})
if not res then
ngx.log(ngx.ERR, "Request failed: ", err)
return ngx.exit(ngx.HTTP_BAD_GATEWAY)
end
-- 流式读取响应
local reader = res.body_reader
repeat
local chunk, err = reader(8192)
if err then
ngx.log(ngx.ERR, "Read error: ", err)
break
end
if chunk then
ngx.print(chunk)
end
until not chunk
-- 保持连接复用
local ok, err = httpc:set_keepalive(60000, 100)
```
---
## 5. 与 Redis/MySQL 集成
### 5.1 Redis 集成
```bash
# 安装 lua-resty-redisOpenResty 已内置)
```
#### 基础操作
```lua
local redis = require "resty.redis"
-- 创建连接
local red = redis:new()
red:set_timeout(1000) -- 1秒超时
-- 连接
local ok, err = red:connect("127.0.0.1", 6379)
if not ok then
ngx.log(ngx.ERR, "Failed to connect: ", err)
return ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR)
end
-- 基本操作
red:set("key", "value")
red:set("key", "value", 300) -- 带过期时间
local res, err = red:get("key")
if res == ngx.null then
ngx.say("Key not found")
else
ngx.say("Value: ", res)
end
-- 列表操作
red:lpush("queue", "task1")
red:lpush("queue", "task2")
local task = red:rpop("queue")
-- 哈希操作
red:hset("user:1001", "name", "Alice")
red:hset("user:1001", "age", "30")
local user = red:hgetall("user:1001")
-- 事务
red:multi()
red:incr("counter")
red:lpush("log", "new entry")
local res, err = red:exec()
-- 连接池
local ok, err = red:set_keepalive(60000, 100)
```
#### Redis 连接池封装
```lua
-- redis_pool.lua
local redis = require "resty.redis"
local _M = {}
function _M.get_connection()
local red = redis:new()
red:set_timeout(1000)
-- 使用 Unix socket性能更好
local ok, err = red:connect("unix:/var/run/redis/redis.sock")
if not ok then
-- 回退到 TCP
ok, err = red:connect("127.0.0.1", 6379)
if not ok then
return nil, err
end
end
-- 认证(如有密码)
-- local res, err = red:auth("password")
return red, nil
end
function _M.return_connection(red)
if not red then
return
end
local ok, err = red:set_keepalive(60000, 100)
if not ok then
red:close()
end
end
return _M
```
#### 缓存模式封装
```lua
-- cache.lua
local redis_pool = require "redis_pool"
local cjson = require "cjson"
local _M = {}
function _M.get(key, ttl, fetch_func)
local red, err = redis_pool.get_connection()
if not red then
-- Redis 不可用,直接获取数据
return fetch_func()
end
-- 尝试从 Redis 获取
local data, err = red:get("cache:" .. key)
if data and data ~= ngx.null then
redis_pool.return_connection(red)
return cjson.decode(data)
end
-- 从数据源获取
local result = fetch_func()
if result then
-- 异步写入 Redis
red:set("cache:" .. key, cjson.encode(result), "EX", ttl)
end
redis_pool.return_connection(red)
return result
end
return _M
```
### 5.2 MySQL 集成
```bash
# 安装 lua-resty-mysqlOpenResty 已内置)
```
#### 基础操作
```lua
local mysql = require "resty.mysql"
-- 创建连接
local db, err = mysql:new()
if not db then
ngx.log(ngx.ERR, "Failed to create mysql: ", err)
return
end
db:set_timeout(1000)
-- 连接数据库
local ok, err, errcode, sqlstate = db:connect({
host = "127.0.0.1",
port = 3306,
database = "test",
user = "root",
password = "password",
charset = "utf8mb4",
max_packet_size = 1024 * 1024,
pool = "mysqlpool" -- 连接池名称
})
if not ok then
ngx.log(ngx.ERR, "Failed to connect: ", err)
return
end
-- 查询
local res, err, errcode, sqlstate = db:query("SELECT * FROM users WHERE id = 1")
if not res then
ngx.log(ngx.ERR, "Query failed: ", err)
return
end
-- 处理结果
for i, row in ipairs(res) do
ngx.say("User: ", row.name, ", Email: ", row.email)
end
-- 插入/更新
local res, err = db:query([[
INSERT INTO users (name, email)
VALUES ('John', 'john@example.com')
]])
if res then
ngx.say("Inserted ID: ", res.insert_id)
end
-- 事务
local res, err = db:query("START TRANSACTION")
local res1, err1 = db:query("UPDATE accounts SET balance = balance - 100 WHERE id = 1")
local res2, err2 = db:query("UPDATE accounts SET balance = balance + 100 WHERE id = 2")
if res1 and res2 then
db:query("COMMIT")
else
db:query("ROLLBACK")
end
-- 放回连接池
local ok, err = db:set_keepalive(60000, 100)
```
#### MySQL 连接池封装
```lua
-- mysql_pool.lua
local mysql = require "resty.mysql"
local cjson = require "cjson"
local _M = {
config = {
host = "127.0.0.1",
port = 3306,
database = "app",
user = "app_user",
password = "app_pass",
charset = "utf8mb4",
max_packet_size = 1024 * 1024,
pool_size = 50
}
}
function _M.query(sql)
local db, err = mysql:new()
if not db then
return nil, err
end
db:set_timeout(3000)
local ok, err = db:connect({
host = _M.config.host,
port = _M.config.port,
database = _M.config.database,
user = _M.config.user,
password = _M.config.password,
charset = _M.config.charset,
max_packet_size = _M.config.max_packet_size,
pool = "mysqlpool"
})
if not ok then
return nil, err
end
local res, err = db:query(sql)
db:set_keepalive(60000, _M.config.pool_size)
if not res then
return nil, err
end
return res, nil
end
-- 参数化查询(防 SQL 注入)
function _M.escape(str)
return ngx.quote_sql_str(str)
end
return _M
```
### 5.3 综合示例:用户信息查询
```nginx
location /api/user {
content_by_lua_block {
local cjson = require "cjson"
local user_id = ngx.var.arg_id
if not user_id or not user_id:match("^%d+$") then
ngx.status = 400
ngx.say("Invalid user ID")
return
end
-- 1. 尝试从 Redis 获取
local redis = require "resty.redis"
local red = redis:new()
red:set_timeout(1000)
local ok, err = red:connect("127.0.0.1", 6379)
if ok then
local data = red:get("user:" .. user_id)
if data and data ~= ngx.null then
ngx.header["Content-Type"] = "application/json"
ngx.header["X-Cache"] = "HIT"
ngx.say(data)
red:set_keepalive(60000, 100)
return
end
red:set_keepalive(60000, 100)
end
-- 2. MySQL 查询
local mysql = require "resty.mysql"
local db = mysql:new()
db:set_timeout(2000)
local ok, err = db:connect({
host = "127.0.0.1",
port = 3306,
database = "users",
user = "readonly",
password = "readonly",
charset = "utf8mb4"
})
if not ok then
ngx.status = 500
ngx.say("Database error")
return
end
local sql = "SELECT id, name, email, created_at FROM users WHERE id = " .. user_id
local res, err = db:query(sql)
db:set_keepalive(60000, 50)
if not res or #res == 0 then
ngx.status = 404
ngx.say("User not found")
return
end
local user = res[1]
local response = cjson.encode(user)
-- 3. 异步写入 Redis不阻塞响应
local ok, err = red:connect("127.0.0.1", 6379)
if ok then
red:set("user:" .. user_id, response, "EX", 300) -- 缓存5分钟
red:set_keepalive(60000, 100)
end
ngx.header["Content-Type"] = "application/json"
ngx.header["X-Cache"] = "MISS"
ngx.say(response)
}
}
```
---
## 6. 性能优化技巧
### 6.1 LuaJIT 优化
```lua
-- 1. 使用局部变量缓存全局变量
local ngx = ngx
local cjson = require "cjson"
local http = require "resty.http"
-- 2. 避免在循环中创建函数
local function process_item(item)
return item * 2
end
for i = 1, 1000 do
process_item(i) -- 比内联函数更高效
end
-- 3. 使用 table.new 预分配OpenResty 扩展)
local new_tab = require "table.new"
local t = new_tab(100, 0) -- 预分配100个数组元素
-- 4. 字符串拼接使用 table.concat
local parts = {}
for i = 1, 100 do
parts[i] = "item" .. i
end
local result = table.concat(parts, ",")
-- 5. 使用 ngx.re 而不是 Lua 正则
-- 高效
local m, err = ngx.re.match(str, [[\d+]], "jo")
-- 低效
local m = str:match("%d+")
-- 6. 避免使用 pairs/ipairs 进行数值索引遍历
-- 高效
for i = 1, #arr do
local v = arr[i]
end
-- 低效
for i, v in ipairs(arr) do
end
```
### 6.2 连接池优化
```lua
-- 合理设置连接池大小
local pool_size = 100
local keepalive_timeout = 60000 -- 60秒
-- Redis
red:set_keepalive(keepalive_timeout, pool_size)
-- MySQL
db:set_keepalive(keepalive_timeout, pool_size)
-- HTTP
httpc:set_keepalive(keepalive_timeout, pool_size)
```
### 6.3 缓存策略
| 策略 | 适用场景 | 实现方式 |
|------|---------|---------|
| **本地缓存** | 热点数据、配置信息 | `ngx.shared.DICT` |
| **Redis 缓存** | 分布式缓存、会话 | `resty.redis` |
| **多级缓存** | 高并发读取 | L1本地+ L2Redis |
| **缓存预热** | 系统启动时 | `init_worker_by_lua` |
| **缓存穿透防护** | 防止缓存击穿 | 互斥锁 + 空值缓存 |
### 6.4 Worker 间通信
```lua
-- 使用共享字典实现 worker 间通信
http {
lua_shared_dict ipc 1m;
init_worker_by_lua_block {
local ipc = ngx.shared.ipc
-- 订阅消息
local check_message
check_message = function(premature)
if premature then return end
-- 获取消息
local msg = ipc:get("broadcast")
if msg then
-- 处理消息
ngx.log(ngx.INFO, "Worker ", ngx.worker.id(),
" received: ", msg)
ipc:delete("broadcast")
end
ngx.timer.at(0.1, check_message)
end
ngx.timer.at(0.1, check_message)
}
}
```
### 6.5 内存管理
```lua
-- 1. 及时释放大对象
local large_data = fetch_large_data()
-- 处理数据
process(large_data)
large_data = nil -- 显式释放引用
-- 2. 使用弱引用表(缓存场景)
local weak_cache = setmetatable({}, {
__mode = "v"
})
-- 3. 避免闭包捕获大对象
local function create_handler(config)
-- 只捕获需要的字段
local timeout = config.timeout
return function()
-- 使用 timeout不持有整个 config
end
end
-- 4. 控制字符串创建
-- 使用 string.sub 而不是正则提取
local prefix = str:sub(1, 10)
```
---
## 7. 完整配置示例
### 7.1 API 网关配置
```nginx
user openresty;
worker_processes auto;
error_log /var/log/openresty/error.log warn;
pid /run/openresty.pid;
events {
worker_connections 4096;
use epoll;
multi_accept on;
}
http {
include /usr/local/openresty/nginx/conf/mime.types;
default_type application/octet-stream;
# Lua 共享字典
lua_shared_dict cache 50m;
lua_shared_dict rate_limit 10m;
lua_shared_dict sessions 20m;
lua_shared_dict locks 5m;
# Lua 库路径
lua_package_path "/usr/local/openresty/site/lualib/?.lua;;";
lua_package_cpath "/usr/local/openresty/site/lualib/?.so;;";
# 全局初始化
init_by_lua_block {
require "cjson"
require "resty.redis"
require "resty.mysql"
-- 全局配置
CONFIG = {
redis = { host = "127.0.0.1", port = 6379 },
mysql = {
host = "127.0.0.1", port = 3306,
database = "api_db",
user = "api_user", password = "api_pass"
},
jwt_secret = "your-secret-key",
rate_limit = { rps = 100, burst = 200 }
}
}
# Worker 初始化
init_worker_by_lua_block {
-- 定期清理过期缓存
local flush_expired
flush_expired = function(premature)
if premature then return end
ngx.shared.cache:flush_expired(100)
ngx.timer.at(30, flush_expired)
end
ngx.timer.at(30, flush_expired)
}
# 日志格式
log_format api_log '$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"'
' cache=$upstream_http_x_cache';
access_log /var/log/openresty/access.log api_log;
# 性能优化
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
types_hash_max_size 2048;
# 限流区域
limit_req_zone $binary_remote_addr zone=ip:10m rate=10r/s;
# 上游服务器
upstream api_backend {
server 127.0.0.1:8080 weight=5;
server 127.0.0.1:8081 weight=5;
keepalive 100;
}
# 主服务器
server {
listen 80;
server_name api.example.com;
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl http2;
server_name api.example.com;
# SSL 配置
ssl_certificate /etc/ssl/certs/api.crt;
ssl_certificate_key /etc/ssl/private/api.key;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
ssl_prefer_server_ciphers on;
# 安全头部
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
# 全局访问控制
location / {
# IP 白名单检查
access_by_lua_block {
local whitelist = { ["10.0.0.0/24"] = true }
-- 实现 IP 检查逻辑
}
proxy_pass http://api_backend;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
# 健康检查
location /health {
access_log off;
content_by_lua_block {
local cjson = require "cjson"
local health = {
status = "healthy",
time = ngx.time(),
worker = ngx.worker.id()
}
ngx.header["Content-Type"] = "application/json"
ngx.say(cjson.encode(health))
}
}
# API 路由
location /api/v1/ {
# 限流
limit_req zone=ip burst=20 nodelay;
access_by_lua_block {
-- JWT 认证
local token = ngx.var.http_authorization
if not token then
return ngx.exit(ngx.HTTP_UNAUTHORIZED)
end
-- 验证 JWT简化示例
-- 实际使用 resty.jwt
}
header_filter_by_lua_block {
ngx.header["X-API-Version"] = "v1"
}
proxy_pass http://api_backend;
}
# 登录接口
location /api/auth/login {
limit_req zone=ip burst=5 nodelay;
content_by_lua_block {
local cjson = require "cjson"
ngx.req.read_body()
local data = ngx.req.get_body_data()
if not data then
ngx.status = 400
ngx.say(cjson.encode({ error = "No body" }))
return
end
local args = cjson.decode(data)
-- 验证用户名密码
-- 生成 JWT
ngx.header["Content-Type"] = "application/json"
ngx.say(cjson.encode({
token = "jwt_token_here",
expires_in = 3600
}))
}
}
# 静态资源
location /static/ {
alias /var/www/static/;
expires 30d;
add_header Cache-Control "public, immutable";
}
}
}
```
### 7.2 WAFWeb 应用防火墙)配置
```nginx
http {
lua_shared_dict waf_rules 10m;
lua_shared_dict waf_block 50m;
init_by_lua_block {
-- 加载 WAF 规则
local cjson = require "cjson"
local rules = {
{
id = "1001",
name = "SQL Injection",
pattern = [[(?:union|select|insert|update|delete|drop|create)\s+]],
severity = "high",
action = "block"
},
{
id = "1002",
name = "XSS Attack",
pattern = [[<script[^>]*>[\s\S]*?</script>]],
severity = "high",
action = "block"
},
{
id = "1003",
name = "Path Traversal",
pattern = [[\.\./\.\.]],
severity = "medium",
action = "block"
}
}
-- 存储到共享字典
local waf_rules = ngx.shared.waf_rules
for _, rule in ipairs(rules) do
waf_rules:set(rule.id, cjson.encode(rule))
end
}
server {
listen 80;
server_name protected.example.com;
location / {
access_by_lua_block {
local cjson = require "cjson"
local waf_rules = ngx.shared.waf_rules
local waf_block = ngx.shared.waf_block
local ip = ngx.var.remote_addr
-- 检查 IP 是否被封锁
if waf_block:get("block:" .. ip) then
return ngx.exit(ngx.HTTP_FORBIDDEN)
end
-- 获取所有规则
local rules = waf_rules:get_keys(0)
local matched = false
local matched_rule = nil
-- 检查请求
local check_string = ngx.var.request_uri .. " " ..
(ngx.var.http_user_agent or "") .. " " ..
(ngx.var.http_cookie or "")
for _, rule_id in ipairs(rules) do
local rule_data = waf_rules:get(rule_id)
if rule_data then
local rule = cjson.decode(rule_data)
local m, err = ngx.re.match(check_string, rule.pattern, "ijo")
if m then
matched = true
matched_rule = rule
break
end
end
end
if matched then
-- 记录攻击
ngx.log(ngx.ERR, "WAF blocked request from ", ip,
" Rule: ", matched_rule.id, " - ", matched_rule.name)
-- 增加计数
local count, err = waf_block:incr("count:" .. ip, 1, 0)
if count and count > 100 then
-- 封锁 IP
waf_block:set("block:" .. ip, "1", 3600)
ngx.log(ngx.ERR, "IP blocked: ", ip)
end
return ngx.exit(ngx.HTTP_FORBIDDEN)
end
}
proxy_pass http://backend;
}
}
}
```
---
## 8. 参考文档
- [OpenResty 官方文档](https://openresty.org/en/)
- [LuaJIT 文档](http://luajit.org/)
- [lua-nginx-module](https://github.com/openresty/lua-nginx-module)
- [lua-resty-core](https://github.com/openresty/lua-resty-core)
- [lua-resty-redis](https://github.com/openresty/lua-resty-redis)
- [lua-resty-mysql](https://github.com/openresty/lua-resty-mysql)
- [lua-resty-http](https://github.com/ledgetech/lua-resty-http)
- [lua-resty-jwt](https://github.com/cdbattags/lua-resty-jwt)
- [Lua 5.1 参考手册](https://www.lua.org/manual/5.1/)