Openresty+Lua+Redis实现高性能缓存

一、背景

当我们的程序需要提供较高的并发访问时,往往需要在程序中引入缓存技术,通常都是使用Redis作为缓存,但是要再更进一步提升性能的话,就需要尽可能的减少请求的链路长度,比如可以将访问Redis缓存从Tomcat服务器提前Nginx

原本访问缓存逻辑

User---> Nginx -> Tomcat -> Redis 

User---> Nginx -> Redis 

二、介绍

1 OpenResty 介绍

OpenResty® 是一个基于 Nginx 与 Lua 的高性能 Web 平台,其内部集成了大量精良的 Lua 库、第三方模块以及大多数的依赖项。用于方便地搭建能够处理超高并发、扩展性极高的动态 Web 应用、Web 服务和动态网关。

官网: OpenResty® - 开源官方站

2 Lua 介绍

Lua 是一个小巧的脚本语言。它是巴西里约热内卢天主教大学(Pontifical Catholic University of Rio de Janeiro)里的一个由Roberto Ierusalimschy、Waldemar Celes 和 Luiz Henrique de Figueiredo三人所组成的研究小组于1993年开发的。 其设计目的是为了通过灵活嵌入应用程序中从而为应用程序提供灵活的扩展和定制功能。Lua由标准C编写而成,几乎在所有操作系统和平台上都可以编译,运行。Lua并没有提供强大的库,这是由它的定位决定的。所以Lua不适合作为开发独立应用程序的语言。Lua 有一个同时进行的JIT项目,提供在特定平台上的即时编译功能。

推荐教程: Lua 教程 | 菜鸟教程

三、软件安装

1 OpenResty 安装

下载最新版本

上传到虚拟机的/usr/local 目录下,之后解压

这里前提是需要安装c语言编译器和Nginx依赖包(如已经安装过了跳过下面3个命令),否则下面的安装会报错的

yum install -y gcc

yum install -y pcre pcre-devel

yum install -y zlib zlib-devel

yum install -y openssl openssl-devel

进入到解压后的文件夹  openresty-1.25.3.1 中执行

./configure --prefix=/usr/local/openresty

正常的话,出现下面的画面说明执行成功了

然后执行make && make install 

make 

make install 

执行完成后,可以看到在/usr/local 目录下多了一个openresty 目录

2 目录介绍

  1. bin目录:执行文件目录。
  2. lualib目录:这个目录存放的是OpenResty中使用的Lua库,主要分为ngx和resty两个子目录。
  3. nginx目录:这个目录存放的是OpenResty的nginx配置和可执行文件。
  4. luajit目录:luajit目录是LuaJIT的安装根目录,用于提供LuaJIT的运行环境和相关资源。

3 启动Nginx

 nginx/sbin/nginx -c /usr/local/openresty/nginx/conf/nginx.conf

4 访问Nginx

在浏览器中输入虚拟机的地址http://192.168.31.115/

四、Openresty中初试Lua

1 编辑nginx.conf

在server{}中插入下面代码

location /lua {

                default_type text/html;
                content_by_lua '
                        ngx.say("<p>hello,world</p>")
                ';
        }

2 重启一下Nginx

nginx/sbin/nginx -s stop

nginx/sbin/nginx -c /usr/local/openresty/nginx/conf/nginx.conf

3 访问浏览器

在浏览器中输入虚拟机的IP地址+lua

http://192.168.31.115/lua

正常的话应该可以看到下面的画面

 4 通过lua文件的方式

进入到Nginx目录,创建lua文件夹,并新建一个hello.lua文件

cd nginx

mkdir lua

vim lua/hello.lua

ngx.say("<p>hello,hello,hello</p>")

修改nginx.conf 文件

location /lua {

                default_type text/html;
                content_by_lua_file lua/hello.lua;
        }

重启Nginx,再次刷新网站

5 Openresty连接Redis

参考官网文档:GitHub - openresty/lua-resty-redis: Lua redis client driver for the ngx_lua based on the cosocket API

在/usr/local/openresty/nginx/lua目录下,编辑一个redis.lua 文件,内容如下:

local redis = require "resty.redis"
local red = redis:new()

red:set_timeouts(1000, 1000, 1000) -- 1 sec

local ok, err = red:connect("192.168.31.114", 6579)
if not ok then
   ngx.say("failed to connect: ", err)
   return
end

local res, err = red:auth("123456")
if not res then
   ngx.say("failed to authenticate: ", err)
   return
end

ok, err = red:set("dog", "an animal")
if not ok then
    ngx.say("failed to set dog: ", err)
    return
end

ngx.say("set result: ", ok)

local res, err = red:get("dog")
if not res then
    ngx.say("failed to get dog: ", err)
    return
end

if res == ngx.null then
    ngx.say("dog not found.")
    return
end

ngx.say("dog: ", res)

再修改nginx.conf 

location /lua {

                default_type text/html;
                content_by_lua_file lua/redis.lua;
        }

访问浏览器

到这里,我们已经成功使用Nginx通过lua脚本访问到了Redis,但是这种写法仍然有一个巨大的问题,就是每次请求都会重新连接Redis,性能非常低下,我们测试一下这样写的接口性能

6 解决Redis重复连接问题

再修改nginx.conf 

require("my/cache").go()

五、真实案例

1 案例背景

应用程序中有一个接口/goods-center/getGoodsDetails 希望通过nginx先查询Redis缓存,缓存中没有就去应用服务中查询,然后把查询到结果缓存到Redis中

2 Nginx获取请求的参数

编辑conf/nginx.conf 中的server,添加下面配置

        location /goods-center/getGoodsDetails {
                default_type application/json;
                content_by_lua_file lua/item.lua;
        }

然后在/usr/local/openresty/nginx/lua 目录中创建item.lua,添加下面lua代码

local args = ngx.req.get_uri_args()

ngx.say(args["goodsId"])

重新加载nginx配置

sbin/nginx -s reload

浏览器演示

3 转发请求到后端服务

定义一些工具类,方便后续写代码时调用,在/usr/local/openresty/lualib/mylua/common.lua

在common.lua中添加http get工具方法

local function http_get(path,params)
	local resp = ngx.location.capture(path,{
		method = ngx.HTTP_GET,
		args = params
	})
	if not resp then
		ngx.log(ngx.ERR)
		ngx.exit(404)
	end
	return resp.body
end

local _M = {
	http_get = http_get
}

return _M

编辑/usr/local/openresty/nginx/lua/item.lua 文件

-- 导入common包
local common = require('mylua.common')
local http_get = common.http_get

local args = ngx.req.get_uri_args()

-- 查询商品信息
local itemJson = http_get("/goods-center/getGoodsDetails",args)

ngx.say(itemJson)

修改/usr/local/openresty/nginx/conf/nginx.conf 添加下面代码

    	location /nginx-cache/goods-center/getGoodsDetails {
    		default_type application/json;
    		content_by_lua_file lua/item.lua;
    	}

        location ~ ^/goods-center/ {
            proxy_pass http://192.168.31.112:9527;
        }

解释一下上面代码,将/nginx-cache/goods-center/getGoodsDetails 请求通过lua脚本处理,转发到/goods-center/getGoodsDetails ,再通过~ ^/goods-center/  反向代理到应用服务器上http://192.168.31.112:9527;

演示:

4 优先查询Redis (官方并发会报错)

官网文档:GitHub - openresty/lua-resty-redis: Lua redis client driver for the ngx_lua based on the cosocket API

这应该是我到目前为止最想吐槽的开源软件了,按照官网的文档操作简直就是个玩具,无法商用

只要超过1个线程去压测就会报bad request 错误,这个在官网的局限性一栏中有提到,但是不明白为什么不解决,这个问题不解决,就无法商用,而且每次请求都会创建连接,性能巨差,关键这个问题在网上都很少有人提出过这个问题,包括一些教学视频,都是点到为止,根本没有测试过并发场景能不能用,我只要一并发测试就GG,怎么改都不行,翻了很多文档,都没有解决方案,如果有人有方案可以在评论区分享一下,互相学习

GitHub - openresty/lua-resty-redis: Lua redis client driver for the ngx_lua based on the cosocket API

先看代码

/usr/local/openresty/lualib/mylua/common.lua

local redis = require('resty.redis')
local red = redis:new()
red:set_timeouts(1000,1000,1000)

local function get_from_redis(key)
	ngx.log(ngx.INFO,"redis init start .............")

	local ok,err = red:connect("192.168.31.114",6579)
	-- 连接失败
	if not ok then
		ngx.log(ngx.ERR,"connect redis error",err)
		return nil
	end
	
	-- 认证失败
	local res, err = red:auth("123456")
	if not res then
	   ngx.say(ngx.ERR,"failed to authenticate: ", err)
	   return nil
	end

	local resp,err = red:get(key)
	if not resp then
		ngx.log(ngx.ERR,"get from redis error ",err," key: ",key)
		return nil
	end
	-- 数据为空
	if resp == ngx.null then
		ngx.log(ngx.ERR,"this key is nil, key: ",key)
		return nil
	end
	 -- 设置连接超时时间和连接池大小
    red:set_keepalive(600000, 100)
	return resp

end

local function http_get(path,params)
	local resp = ngx.location.capture(path,{
		method = ngx.HTTP_GET,
		args = params
	})
	if not resp then
		ngx.log(ngx.ERR)
		ngx.exit(404)
	end
	return resp.body
end

local _M = {
	http_get = http_get,
	get_from_redis = get_from_redis
}

return _M

/usr/local/openresty/nginx/lua/item.lua

-- 导入common包
local _M = {}

common = require('mylua.common')
http_get = common.http_get
get_from_redis = common.get_from_redis

function _M.get_data()
	local args = ngx.req.get_uri_args()

	-- 先查询Redis
	local itemJson = get_from_redis("goods-center:goodsInfo:" .. args["goodsId"])
	ngx.log(ngx.INFO,"get from redis itemJson,  ",itemJson)
	if itemJson == nil then
		-- redis 没有,则查询服务器信息
		itemJson = http_get("/goods-center/getGoodsDetails",args)
	end

	ngx.say(itemJson)
end

return _M

修改/usr/local/openresty/nginx/conf/nginx.conf 添加下面代码

    server {
    	location /nginx-cache/goods-center/getGoodsDetails {
    		default_type application/json;
    		content_by_lua_block {
                require("lua/item").get_data()
            }
    	}

        location ~ ^/goods-center/ {
            proxy_pass http://192.168.31.112:9527;
        }

单线程测试(没有报错,且有500多的吞吐量)

2个线程测试,有40%+ 的错误率,报错详情截图给了,线程越多报错越多

2024/02/04 21:56:06 [error] 21662#0: *390686 lua entry thread aborted: runtime error: /usr/local/openresty/lualib/resty/redis.lua:166: bad request
stack traceback:
coroutine 0:
	[C]: in function 'connect'
	/usr/local/openresty/lualib/resty/redis.lua:166: in function 'connect'
	/usr/local/openresty/lualib/mylua/common.lua:9: in function 'get_from_redis'
	./lua/item.lua:12: in function 'get_data'
	content_by_lua(nginx.conf:49):2: in main chunk, client: 192.168.31.32, server: localhost, request: "GET /nginx-cache/goods-center/getGoodsDetails?goodsId=10000 HTTP/1.1", host: "192.168.31.115"

5 使用ngx.shared.redis_pool连接池(并发不会报错)

/usr/local/openresty/lualib/mylua/common.lua

-- 引入 lua-resty-redis 模块
local redis = require "resty.redis"

-- 获取 OpenResty 全局字典对象(连接池)
local redis_pool = ngx.shared.redis_pool

-- Redis 连接池的最大连接数
local max_connections = 100

-- Redis 服务器地址和端口
local redis_host = "192.168.31.114"
local redis_port = 6579

-- 获取 Redis 连接
local function get_redis_connection()
    local red = redis_pool:get(redis_host)
    
    if not red then
    	ngx.log(ngx.ERR, "create new : ", err)
        -- 创建一个新的 Redis 连接
        red = redis:new()

        -- 设置连接超时时间
        red:set_timeout(1000,1000,1000)
    
        -- 连接 Redis 服务器
        local ok, err = red:connect(redis_host, redis_port)
        if not ok then
            ngx.log(ngx.ERR, "Failed to connect to Redis: ", err)
            return nil, err
        end

        local res, err = red:auth("123456")
		if not res then
		   ngx.say(ngx.ERR,"failed to authenticate: ", err)
		   return nil
		end

        -- 将连接放入连接池
        redis_pool:set(redis_host, red, 600)
    end

    return red
end

local function get_from_redis(key)
	local redis_conn, err = get_redis_connection()
	if not redis_conn then
	    ngx.log(ngx.ERR, "Failed to get Redis connection: ", err)
	    ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR)
	end
	-- 获取失败
	local resp,err = redis_conn:get(key)
	if not resp then
		ngx.log(ngx.ERR,"get from redis error ",err," key: ",key)
		return nil
	end
	-- 数据为空
	if resp == ngx.null then
		ngx.log(ngx.ERR,"this key is nil, key: ",key)
		return nil
	end
	 -- 设置连接超时时间
    redis_conn:set_keepalive(600000, max_connections)
	return resp
end

local function http_get(path,params)
	local resp = ngx.location.capture(path,{
		method = ngx.HTTP_GET,
		args = params
	})
	if not resp then
		ngx.log(ngx.ERR)
		ngx.exit(404)
	end
	return resp.body
end

local _M = {
	http_get = http_get,
	get_from_redis = get_from_redis
}

return _M

/usr/local/openresty/nginx/lua/item.lua

-- 导入common包
local _M = {}

common = require('mylua.common')
http_get = common.http_get
get_from_redis = common.get_from_redis

function _M.get_data()
	local args = ngx.req.get_uri_args()

	-- 先查询Redis
	local itemJson = get_from_redis("goods-center:goodsInfo:" .. args["goodsId"])
	ngx.log(ngx.INFO,"get from redis itemJson,  ",itemJson)
	if itemJson == nil then
		-- redis 没有,则查询服务器信息
		itemJson = http_get("/goods-center/getGoodsDetails",args)
	end

	ngx.say(itemJson)
end

return _M

修改/usr/local/openresty/nginx/conf/nginx.conf 添加下面代码

    lua_shared_dict redis_pool 100m;

    server {
    	location /nginx-cache/goods-center/getGoodsDetails {
    		default_type application/json;
    		content_by_lua_block {
                require("lua/item").get_data()
            }
    	}

        location ~ ^/goods-center/ {
            proxy_pass http://192.168.31.112:9527;
        }

6 压测详情

1 命中缓存压测

命中缓存的情况吞吐量1400多,并且采用本章第5小结这种线程池的方式连接不会报错,基本上可以商用了,唯一缺点是并发量没有达到预期,通过排查原因发现,大量的连接池并未真正生效,仍然有大量的创建连接,可能这是影响性能的主要因素,如果有同学解决了这个问题可以在评论区分享一下

2 未命中缓存压测

未命中缓存,会请求后端Tomcat服务器,Tomcat服务器会查询MySQL,这边的吞吐量测试数据为313,也不怎么高,排查了一下原因,仍然是Redis一直在不停的创建连接,这个问题目前还没有找到解决方案

六、总结

通过Nginx+lua 的方式,在Nginx这层就去查询Redis缓存,看起来的确是个非常棒的方案,但是缺点是操作起来特别麻烦,需要开发人员了解Nginx + Lua  还要了解Openresty 如何集成Nginx + Lua  + Redis,还要掌握在这种方式下,能够使用好Redis的连接池。最关键的是目前这种技术的文档并不完善,代码在某些地方不是特别的成熟,网上能找到的资料都很少而且都比较皮毛,不够深入,然而开发人员却需要深入地了解他们,才能比较好的驾驭这种方式,这次探究仍然有一个遗留问题就是Openresty + Lua + Redis 连接池的方式,连接池看起来有时候会不生效,也不是每次都不生效,有一定的概率,从而导致性能并不高,这个需要后面再研究解决它

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/371668.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

4. 树(二叉树、二叉查找树/二叉排序树/二叉搜索树、平衡二叉树、平衡二叉B树/红黑树)

树 1. 二叉树1.1 概述1.2 特点1.3 二叉树遍历方式1.3.1 前序遍历(先序遍历)1.3.2 中序遍历1.3.3 后序遍历1.3.4 层序遍历 2. 二叉查找树&#xff08;二叉排序树、二叉搜索树&#xff09;2.1 概述2.2 特点 3. 平衡二叉树3.1 概述3.2 特点3.3 旋转3.3.1 左旋3.3.2 右旋 3.4 平衡二…

Quartus IP 之mif与hex文件创建与使用

一、mif与hex概述 ROM IP的数据需要满足断电不丢失的要求&#xff0c;ROM IP数据的文件格式一般有三种文件格式&#xff1a;.mif、.hex、.coe&#xff0c;Xilinx与Intel Altera支持的ROM IP数据文件格式如下&#xff1a; Xilinx与Altera支持的ROM文件格式 Alterahex、mifAM&am…

JS第二天、原型、原型链、正则

☆☆☆☆ 什么是原型&#xff1f; 构造函数的prototype 就是原型 专门保存所有子对象共有属性和方法的对象一个对象的原型就是它的构造函数的prototype属性的值。prototype是哪来的&#xff1f;所有的函数都有一个prototype属性当函数被创建的时候&#xff0c;prototype属性…

项目02《游戏-08-开发》Unity3D

基于 项目02《游戏-07-开发》Unity3D &#xff0c; 本次任务做物品相互与详情的功能&#xff0c; 首先要做 点击相应&#xff0c; 接下来用接口实现点击相应事件&#xff0c;具体到代码中&#xff0c;我们找到需要响应鼠标事件的对象&#xff0c; 双击PackageCell…

食堂预约系统

文章目录 前言​部分沟通内容技术点小程序功能部分代码段功能图 商家管理系统功能说明功能图登录页面商家管理页面 食品管理订单管理其他功能 结束语 前言​ 最近&#xff0c;接了个小项目——学校食堂预约取餐系统。 具体需求如下图&#xff1a; 部分沟通内容 技术点 系统…

C++:模板初阶

泛型编程 泛型编程&#xff1a;编写与类型无关的通用代码&#xff0c;是代码复用的一种手段。模板是泛型编程的基础。 函数模板 函数模板代表了一个函数家族&#xff0c;该函数模板与类型无关&#xff0c;在使用时被参数化&#xff0c;根据实参类型产生函数的特定类型版本。…

百面嵌入式专栏(面试题)网络编程面试题

沉淀、分享、成长,让自己和他人都能有所收获!😄 📢本篇我们将介绍网络编程面试题 。 1、什么是IO多路复用 I/O多路复用的本质是使用select,poll或者epoll函数,挂起进程,当一个或者多个I/O事件发生之后,将控制返回给用户进程。以服务器编程为例,传统的多进程(多线程…

antv/x6 边添加鼠标悬浮高亮和删除功能

antv/x6 边添加鼠标悬浮高亮和删除功能 效果添加悬浮效果和删除工具取消悬浮效果边删除后的回调函数 效果 添加悬浮效果和删除工具 this.graph.on(edge:mouseenter, ({ cell }) > {let cellId cell.store.data.source.celllet sourceCell _this.graph.getCellById(cellId…

绝地求生:盘点游戏内七款真人脸模,你最喜欢哪款?

从27.1版本更新后&#xff0c;游戏内上线了荣都地图代言人吴彦祖和李政宰的真人脸模&#xff0c;从此闲游盒的各位盒友灵魂搭配的资源库里又多了两位英俊脸庞&#xff0c;那么今天闲游盒来盘点一下游戏内上线的七款真人脸模&#xff0c;看看大家更喜欢哪款呢? 吴彦祖和李政宰 …

CSS-IN-JS

CSS-IN-JS 为什么会有CSS-IN-JS CSS-IN-JS是web项目中将CSS代码捆绑在JavaScript代码中的解决方案。 这种方案旨在解决CSS的局限性&#xff0c;例如缺乏动态功能&#xff0c;作用域和可移植性。 CSS-IN-JS介绍 1&#xff1a;CSS-IN-JS方案的优点&#xff1a; 让css代码拥…

【MySQL】DQL的总结和案例学习

&#x1f308;个人主页: Aileen_0v0 &#x1f525;热门专栏: 华为鸿蒙系统学习|计算机网络|数据结构与算法 ​&#x1f4ab;个人格言:“没有罗马,那就自己创造罗马~” #mermaid-svg-VWRkWqFrRMi4uLRa {font-family:"trebuchet ms",verdana,arial,sans-serif;font-siz…

bert分类模型使用

使用 bert-bert-chinese 预训练模型去做分类任务&#xff0c;这里找了新闻分类数据&#xff0c;数据有 20w&#xff0c;来自https://github.com/649453932/Bert-Chinese-Text-Classification-Pytorch/tree/master/THUCNews 数据 20w &#xff0c;18w 训练数据&#xff0c;1w 验…

挑战!贪吃蛇小游戏的实现(1)

引言 相信大家都玩过贪吃蛇这个游戏&#xff01; 玩家控制一个不断移动的蛇形角色&#xff0c;在一个封闭空间内移动。随着时间推进&#xff0c;这个蛇形角色会逐渐增长&#xff0c;通常是通过吞食屏幕上出现的物品&#xff08;如点或者其他标志&#xff09;来实现。每当贪吃…

JQuery动态插入Bootstrap模态框(Modal)

这里所说的动态插入&#xff0c;是指用JS的append()方式追加元素内容&#xff0c;而不是静态写在HTML里面。 为什么会用到这种方式呢&#xff1f;比如登录框。有些网站在大部分页面都有登录按钮&#xff0c;如果是用Bootstrap的模态框调用的话&#xff0c;常规方式都是写在HTM…

目标检测及相关算法介绍

文章目录 目标检测介绍目标检测算法分类目标检测算法模型组成经典目标检测论文 目标检测介绍 目标检测是计算机视觉领域中的一项重要任务&#xff0c;旨在识别图像或视频中的特定对象的位置并将其与不同类别中的对象进行分类。与图像分类任务不同&#xff0c;目标检测不仅需要…

vue全家桶之状态管理Pinia

一、Pinia和Vuex的对比 1.什么是Pinia呢&#xff1f; Pinia&#xff08;发音为/piːnjʌ/&#xff0c;如英语中的“peenya”&#xff09;是最接近pia&#xff08;西班牙语中的菠萝&#xff09;的词&#xff1b; Pinia开始于大概2019年&#xff0c;最初是作为一个实验为Vue重新…

详解C++类和对象(上)

文章目录 写在前面1. 类的定义2. 类的访问限定符及封装2.1 类的访问限定符2.2 封装 3. 类的作用域4. 类的实例化5 类的对象大小的计算6. 类成员函数的this指针 写在前面 类和对象这一章节&#xff0c;分为上、中、下三篇文章进行拆分介绍的&#xff0c;本篇文章介绍了类和对象…

LabVIEW与EtherCAT实现风洞安全联锁及状态监测

LabVIEW与EtherCAT实现风洞安全联锁及状态监测 在现代风洞试验中&#xff0c;安全联锁与状态监测系统发挥着至关重要的作用&#xff0c;确保了试验过程的安全性与高效性。介绍了一套基于EtherCAT总线技术和LabVIEW软件开发的风洞安全联锁及状态监测系统。该系统通过实时、可靠…

C++后端开发之Sylar学习二:配置VSCode远程连接Ubuntu开发

C后端开发之Sylar学习二&#xff1a;配置VSCode远程连接Ubuntu开发 没错&#xff0c;我不能像大佬那样直接在Ubuntu上面用Vim手搓代码&#xff0c;只能在本地配置一下VSCode远程连接Ubuntu进行开发咯&#xff01; 本篇主要是讲解了VSCode如何配置ssh连接Ubuntu&#xff0c;还有…

蓝桥杯每日一题-----数位dp练习

题目 链接 参考代码 写了两个&#xff0c;一个是很久以前写的&#xff0c;一个是最近刚写的&#xff0c;很久以前写的时候还不会数位dp所以写了比较详细的注释&#xff0c;这两个代码主要是设置了不同的记忆数组&#xff0c;通过这两个代码可以理解记忆数组设置的灵活性。 im…