OpenResty实现Redis查询

一、前言:为什么要在 OpenResty 中查 Redis?

在高并发场景下,传统架构面临巨大挑战:

  • ❌ 所有请求穿透到后端 → 数据库压力剧增
  • ❌ 通用逻辑(鉴权、限流)重复开发
  • ❌ 响应延迟高(网络往返 + 后端处理)

OpenResty = Nginx + LuaJIT ,让你在网关层直接操作 Redis ,实现:

毫秒级响应 (缓存命中时无需调用后端)

降低 70%+ 后端负载

统一业务逻辑入口

本文将手把手教你从零实现 OpenResty 查询 Redis 缓存 ,并提供生产级封装方案


二、核心依赖:lua-resty-redis

OpenResty 官方提供高性能 Redis 客户端 `lua-resty-redis`,特点:

  • ✅ 基于 cosocket,非阻塞 I/O
  • ✅ 支持 连接池复用set_keepalive
  • ✅ 自动重连机制
  • ✅ 支持 Pipeline 批量操作

💡 确认已安装(官方 OpenResty 默认包含):

bash 复制代码
openresty -V 2>&1 | grep -o 'lua-resty-redis'

三、基础实现:单次 Redis 查询

场景:用户 Token 验证

Lua 复制代码
location /api/secure {
    access_by_lua_block {
        -- 1. 引入 Redis 模块
        local redis = require "resty.redis"
        local red = redis:new()

        -- 2. 设置超时(单位:毫秒)
        red:set_timeout(1000)

        -- 3. 连接 Redis
        local ok, err = red:connect("127.0.0.1", 6379)
        if not ok then
            ngx.log(ngx.ERR, "Redis connect failed: ", err)
            ngx.exit(500)
        end

        -- 4. 查询 Token
        local token = ngx.var.http_authorization
        local user_id = red:get("auth:token:" .. token)

        -- 5. 处理结果(注意:Redis nil 返回 ngx.null)
        if not user_id or user_id == ngx.null then
            ngx.status = 401
            ngx.say('{"error":"invalid token"}')
            return ngx.exit(401)
        end

        -- 6. 透传用户 ID 给后端
        ngx.req.set_header("X-User-ID", user_id)

        -- 7. 关键:放入连接池复用(避免连接泄漏!)
        local ok, err = red:set_keepalive(10000, 100)
        if not ok then
            ngx.log(ngx.WARN, "Failed to set keepalive: ", err)
        end
    }

    proxy_pass http://backend;
}

🔑 关键点

  • 必须调用 set_keepalive 实现连接复用
  • 使用 user_id == ngx.null 判断 Redis 返回 nil

四、生产级封装:Redis 工具类

避免重复代码,创建 /usr/local/openresty/lualib/lib/redis_util.lua

Lua 复制代码
-- lib/redis_util.lua
local redis = require "resty.redis"
local ngx = ngx

local _M = {}

-- 配置(建议从共享内存读取)
local config = {
    host = "127.0.0.1",
    port = 6379,
    timeout = 1000,
    pool_size = 100,
    pool_idle_timeout = 10000
}

-- 获取 Redis 连接
local function get_connection()
    local red = redis:new()
    red:set_timeout(config.timeout)
    
    local ok, err = red:connect(config.host, config.port)
    if not ok then
        return nil, "connect failed: " .. tostring(err)
    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, "set_keepalive failed: ", err)
        red:close()
    end
end

-- GET 方法
function _M.get(key)
    local red, err = get_connection()
    if not red then return nil, err end
    
    local res, err = red:get(key)
    release_connection(red)
    
    if err then return nil, err end
    if res == ngx.null then return nil, "not found" end
    
    return res, nil
end

-- SET 方法
function _M.set(key, value, exptime)
    local red, err = get_connection()
    if not red then return nil, err end
    
    local ok, err = exptime and red:set(key, value, "EX", exptime) or red:set(key, value)
    release_connection(red)
    
    if not ok then return nil, err end
    return true, nil
end

return _M

五、使用封装工具:简洁 & 安全

Lua 复制代码
location /api/user-profile {
    content_by_lua_block {
        local redis_util = require "lib.redis_util"
        local cjson = require "cjson.safe"

        local user_id = ngx.var.arg_userId
        if not user_id then
            ngx.exit(400)
        end

        -- 1. 先查缓存
        local cache_key = "user:profile:" .. user_id
        local cached, err = redis_util.get(cache_key)

        if cached then
            ngx.say(cached)
            return
        end

        -- 2. 缓存未命中:回源查询后端
        local res = ngx.location.capture("/internal/user/" .. user_id)
        if res.status ~= 200 then
            ngx.status = res.status
            ngx.say(res.body)
            return
        end

        -- 3. 写入缓存(TTL=300秒)
        redis_util.set(cache_key, res.body, 300)

        ngx.print(res.body)
    }
}

优势

  • 代码简洁,逻辑清晰
  • 自动连接管理
  • 统一错误处理

六、高级技巧:Pipeline 批量查询

场景:一次获取多个用户资料

Lua 复制代码
-- 在 redis_util.lua 中添加 pipeline 方法
function _M.pipeline(commands)
    local red, err = get_connection()
    if not red then return nil, err end

    red:init_pipeline()
    for _, cmd in ipairs(commands) do
        red[cmd[1]](red, unpack(cmd, 2))
    end

    local results, err = red:commit_pipeline()
    release_connection(red)
    
    if err then return nil, err end
    return results, nil
end

使用示例

Lua 复制代码
local commands = {
    {"get", "user:profile:1001"},
    {"get", "user:profile:1002"},
    {"expire", "session:abc", 1800}
}
local results, err = redis_util.pipeline(commands)

📊 性能提升

  • 单条查询:10ms × 100 = 1000ms
  • Pipeline:~15ms(网络往返仅 1 次)

七、生产环境最佳实践

7.1 连接池配置(nginx.conf)

Lua 复制代码
http {
    lua_socket_connect_timeout 1s;
    lua_socket_send_timeout 1s;
    lua_socket_read_timeout 1s;
    lua_socket_pool_size 100;  # 每个 worker 的连接池大小
}

7.2 故障降级

Lua 复制代码
local profile, err = redis_util.get(key)
if err then
    ngx.log(ngx.WARN, "Redis error, fallback to backend: ", err)
    -- 直接走后端,不中断服务
end

7.3 监控指标

  • 记录缓存命中率:

    Lua 复制代码
    if cached then
        ngx.shared.stats:incr("cache_hit", 1)
    else
        ngx.shared.stats:incr("cache_miss", 1)
    end

八、常见问题排查

问题 原因 解决
connection refused Redis 未启动或防火墙 检查 telnet 127.0.0.1 6379
timeout 网络慢或 Redis 阻塞 优化 Lua 脚本,设置合理超时
连接泄漏 未调用 set_keepalive 使用封装工具类强制管理
中文乱码 编码不一致 确保 Redis 和 OpenResty 均使用 UTF-8

九、结语

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

相关推荐
别抢我的锅包肉3 小时前
【MySQL】第四节 - 多表查询、多表关系全解析
数据库·mysql·datagrip
Database_Cool_3 小时前
OpenClaw-Observability:基于 DuckDB 构建 OpenClaw 的全链路可观测体系
数据库·阿里云·ai
刘~浪地球3 小时前
Redis 从入门到精通(五):哈希操作详解
数据库·redis·哈希算法
zzh0814 小时前
MySQL高可用集群笔记
数据库·笔记·mysql
Shely20174 小时前
MySQL数据表管理
数据库·mysql
爬山算法4 小时前
MongoDB(80)如何在MongoDB中使用多文档事务?
数据库·python·mongodb
APguantou5 小时前
NCRE-三级数据库技术-第2章-需求分析
数据库·需求分析
寂夜了无痕5 小时前
MySQL 主从延迟全链路根因诊断与破局法则
数据库·mysql·mysql主从延迟
爱丽_5 小时前
分页为什么越翻越慢:offset 陷阱、seek 分页与索引排序优化
数据库·mysql