OpenResty封装Redis工具

一、前言:为什么需要封装 Redis 工具?

在 OpenResty 中直接使用 lua-resty-redis 虽然可行,但存在严重问题:

  • 重复代码 :每次都要写 connectset_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 表结构

八、结语

感谢您的阅读!如果你有任何疑问或想要分享的经验,请在评论区留言交流!

相关推荐
小小小米粒3 小时前
redis命令集合
数据库·redis·缓存
上海合宙LuatOS3 小时前
LuatOS扩展库API——【libfota2】远程升级
网络·物联网·junit·luatos
旷世奇才李先生3 小时前
Redis高级实战:分布式锁、缓存穿透与集群部署(附实战案例)
redis·分布式·缓存
uElY ITER7 小时前
基于Spring Boot 3 + Spring Security6 + JWT + Redis实现登录、token身份认证
spring boot·redis·spring
java干货9 小时前
如果光缆被挖断导致 Redis 出现两个 Master,怎么防止数据丢失?
数据库·redis·缓存
郝开9 小时前
Docker Compose 本地环境搭建:redis
redis·docker·容器
人道领域10 小时前
【黑马点评日记】高并发秒杀:库存超卖与锁机制解析
java·开发语言·redis·spring·intellij-idea
qq_2837200510 小时前
Python3 模块精讲:Redis 第三方库从入门到精通全攻略
redis·缓存
tonydf11 小时前
一次由组件并发引发的类“缓存击穿”问题排查与修复
redis·后端·架构
爱喝雪碧的可乐12 小时前
【Redis 毁灭计划】7 大高危操作打崩线上服务!从缓存雪崩到数据库宕机,90% 程序员都踩过的坑
开发语言·网络·redis·php