一、前言:为什么需要封装 Redis 工具?
在 OpenResty 中直接使用 lua-resty-redis 虽然可行,但存在严重问题:
- ❌ 重复代码 :每次都要写
connect、set_keepalive - ❌ 连接泄漏 :忘记
set_keepalive导致连接耗尽 - ❌ 错误处理分散 :每个地方都要判断
ngx.null - ❌ 配置硬编码:IP、端口、超时散落在各处
解决方案:封装一个统一的 Redis 工具模块!
本文将带你从零实现一个生产级 Redis 工具类 ,支持:
✅ 自动连接池复用
✅ 统一超时与错误处理
✅ 支持 Pipeline 批量操作
✅ 配置集中管理
二、设计目标
我们的 redis_util.lua 模块应提供如下接口:
Lua
local redis_util = require "lib.redis_util"
-- 基础操作
local val, err = redis_util.get("user:1001")
local ok, err = redis_util.set("config:version", "v2", 3600)
-- 批量操作
local results, err = redis_util.pipeline({
{"get", "user:1001"},
{"hget", "order:2001", "status"},
{"expire", "session:abc", 1800}
})
三、完整工具类实现
创建文件 /usr/local/openresty/lualib/lib/redis_util.lua:
Lua
-- lib/redis_util.lua
-- 生产级 Redis 工具类封装
local redis = require "resty.redis"
local ngx = ngx
local _M = {}
_M._VERSION = '1.0'
-- ======================
-- 配置区(建议从共享内存或配置中心读取)
-- ======================
local config = {
host = "127.0.0.1",
port = 6379,
password = nil, -- 如需密码认证
connect_timeout = 1000, -- 连接超时(毫秒)
read_timeout = 1000, -- 读超时
send_timeout = 1000, -- 写超时
pool_size = 100, -- 连接池大小
pool_idle_timeout = 10000 -- 连接空闲超时(毫秒)
}
-- ======================
-- 内部函数:获取 Redis 连接
-- ======================
local function get_redis_connection()
local red = redis:new()
-- 设置超时(单位:秒)
red:set_timeouts(
config.connect_timeout / 1000,
config.read_timeout / 1000,
config.send_timeout / 1000
)
-- 连接 Redis
local ok, err = red:connect(config.host, config.port)
if not ok then
return nil, "connect failed: " .. tostring(err)
end
-- 密码认证(如配置)
if config.password then
local auth_ok, auth_err = red:auth(config.password)
if not auth_ok then
red:close()
return nil, "auth failed: " .. tostring(auth_err)
end
end
return red, nil
end
-- ======================
-- 内部函数:释放连接到池
-- ======================
local function release_connection(red)
local ok, err = red:set_keepalive(config.pool_idle_timeout, config.pool_size)
if not ok then
ngx.log(ngx.WARN, "Failed to set keepalive: ", err)
red:close() -- 释放失败则关闭
end
end
-- ======================
-- 公共方法:GET
-- ======================
function _M.get(key)
if not key then
return nil, "key is required"
end
local red, err = get_redis_connection()
if not red then
return nil, err
end
local res, err = red:get(key)
if err then
red:close()
return nil, "get failed: " .. tostring(err)
end
release_connection(red)
-- 注意:Redis 的 nil 返回 ngx.null
if res == ngx.null then
return nil, "not found"
end
return res, nil
end
-- ======================
-- 公共方法:SET
-- ======================
function _M.set(key, value, exptime)
if not key or not value then
return nil, "key and value are required"
end
local red, err = get_redis_connection()
if not red then
return nil, err
end
local ok, err
if exptime then
ok, err = red:set(key, value, "EX", exptime)
else
ok, err = red:set(key, value)
end
if not ok then
red:close()
return nil, "set failed: " .. tostring(err)
end
release_connection(red)
return true, nil
end
-- ======================
-- 公共方法:DEL
-- ======================
function _M.delete(key)
if not key then
return nil, "key is required"
end
local red, err = get_redis_connection()
if not red then
return nil, err
end
local res, err = red:del(key)
if err then
red:close()
return nil, "del failed: " .. tostring(err)
end
release_connection(red)
return res, nil -- 返回删除的 key 数量
end
-- ======================
-- 高级方法:Pipeline 批量操作
-- commands: 表,每个元素是 {command, arg1, arg2, ...}
-- ======================
function _M.pipeline(commands)
if not commands or #commands == 0 then
return nil, "commands is required"
end
local red, err = get_redis_connection()
if not red then
return nil, err
end
-- 初始化 pipeline
red:init_pipeline()
-- 添加所有命令
for _, cmd in ipairs(commands) do
local method = cmd[1]
local args = {unpack(cmd, 2)}
local func = red[method]
if not func then
red:close()
return nil, "unsupported command: " .. tostring(method)
end
func(red, unpack(args))
end
-- 执行 pipeline
local results, err = red:commit_pipeline()
if err then
red:close()
return nil, "pipeline failed: " .. tostring(err)
end
release_connection(red)
return results, nil
end
return _M
四、使用示例
4.1 基础场景:Token 验证
Lua
location /api/secure {
access_by_lua_block {
local redis_util = require "lib.redis_util"
local token = ngx.var.http_authorization
if not token then
ngx.exit(401)
end
local user_id, err = redis_util.get("auth:token:" .. token)
if err then
ngx.log(ngx.WARN, "Redis error: ", err)
ngx.exit(500) -- 或降级到 DB
end
if not user_id then
ngx.status = 401
ngx.say('{"error":"invalid token"}')
return ngx.exit(401)
end
ngx.req.set_header("X-User-ID", user_id)
}
proxy_pass http://backend;
}
4.2 高级场景:批量查询用户资料
Lua
local user_ids = {"1001", "1002", "1003"}
local commands = {}
for _, id in ipairs(user_ids) do
table.insert(commands, {"get", "user:profile:" .. id})
end
local profiles, err = redis_util.pipeline(commands)
if err then
ngx.log(ngx.ERR, "Batch get failed: ", err)
-- 降级处理
else
-- 处理 profiles 结果数组
end
五、生产环境最佳实践
5.1 配置外部化
避免硬编码,从共享内存读取配置:
Lua
-- 在 init_worker 阶段加载配置到 shared dict
local config_dict = ngx.shared.config
local host = config_dict:get("redis_host") or "127.0.0.1"
5.2 监控与日志
- 记录 Redis 错误日志
- 上报缓存命中率指标
Lua
if not user_id then
ngx.shared.stats:incr("cache_miss", 1)
else
ngx.shared.stats:incr("cache_hit", 1)
end
5.3 故障降级
Lua
local user_id, err = redis_util.get(key)
if err then
ngx.log(ngx.WARN, "Redis unavailable, fallback to DB")
-- 走后端查询
end
六、性能与连接管理
| 操作 | 未封装 | 封装后 |
|---|---|---|
| 连接复用 | 需手动调用 set_keepalive |
自动管理 |
| QPS(单 worker) | ~5k | ~20k+ |
| 内存占用 | 连接泄漏风险高 | 稳定可控 |
📊 压测建议:
- 单 worker 连接池大小 ≤ 100
- 超时设置 ≤ 1s
七、常见问题排查
| 问题 | 原因 | 解决 |
|---|---|---|
connection refused |
Redis 未启动 | 检查服务状态 |
大量 TIME_WAIT |
未调用 set_keepalive |
使用封装工具类 |
| 中文乱码 | 编码不一致 | 确保 UTF-8 |
| Pipeline 返回 nil | 命令格式错误 | 检查 commands 表结构 |
八、结语
感谢您的阅读!如果你有任何疑问或想要分享的经验,请在评论区留言交流!