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 表结构

八、结语

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

相关推荐
ofoxcoding2 小时前
Redis 缓存穿透怎么解决?3 种方案实测 + 踩坑全记录(2026)
数据库·redis·缓存·ai
二宝1523 小时前
互联网大厂Java面试实战演练:谢飞机的三轮提问与深入解析
java·spring boot·redis·微服务·面试·kafka·oauth2
bLEd RING12 小时前
Redis 设置密码无效问题解决
数据库·redis·缓存
刘~浪地球14 小时前
Redis 从入门到精通(一):简介、安装与配置
数据库·redis·缓存
小红的布丁15 小时前
Redisson 分布式锁实现:可重入与看门狗
redis
刘~浪地球17 小时前
Redis 从入门到精通(二):数据类型详解
数据库·redis·缓存
s1mple“”18 小时前
大厂Java面试实录:从Spring Boot到AI技术的电商场景深度解析
spring boot·redis·微服务·kafka·向量数据库·java面试·ai技术
会飞的大可19 小时前
Redis 竞品与替代方案选型可行性分析报告
数据库·redis·缓存
smachao20 小时前
Redis Desktop Manager(Redis可视化工具)安装及使用详细教程
redis·git·bootstrap