本节介绍如何使用开源的lua-resty-redis模块在Lua脚本中连接和访问Redis,该模块的官方网址为
https://github.com/openresty/lua-resty-redis。
实战案例运行准备:本节涉及的配置文件为源码工程的nginxredis-demo.conf文件。在运行本节实例前需要修改启动脚本openresty-start.bat(或openresty-start.sh)中的PROJECT_CONF变量的值,将其改为nginx-redis-demo.conf,然后重启OpenRestry。
使用Lua模块lua-resty-redis之前需要在官方网站下载resty/redis.lua库文件,然后将该库文件加入项目工程所在的Lua外部库路径。lua-resty-redis官方已经申明,大部分的Redis操作命令都实现了同名的Lua API方法。
下面是一个简单的使用Lua模块lua-resty-redis操作Redis的实
例,代码如下:
local redis = require "resty.redis"
local config = require("luaScript.module.config.redis-config");
---启动调试
local mobdebug = require("luaScript.initial.mobdebug");
mobdebug.start();
--设置超时时长
local red = redis:new()
--设置超时时长,单位为ms
red:set_timeouts(config.timeout, config.timeout, config.timeout)
--连接服务器
local ok, err = red:connect(config.host_name, config.port)
if not ok then
ngx.say("failed to connect: ", err)
return
end
--设置值
ok, err = red:set("dog", "an animal")
if not ok then
ngx.say("failed to set dog: ", err, "
")
return
else
ngx.say("set dog: ok", "
")
end
--取值
local res, err = red:get("dog")
--判空演示
if not res or res == ngx.null then
ngx.say("failed to get dog: ", err, "
")
return
else
ngx.say("get dog: ok", "
", res, "
")
end
--批量操作,减少网络IO次数
red:init_pipeline()
red:set("cat", "cat 1")
red:set("horse", "horse 1")
red:get("cat")red:get("horse")
red:get("dog")
local results, err = red:commit_pipeline()
if not results then
ngx.say("failed to commit the pipelined requests: ", err)
return
end
for i, res in ipairs(results) do
if type(res) == "table" then
if res[1] == false then
ngx.say("failed to run command ", i, ": ", res[2], "
")
else
--处理表容器
ngx.say("succeed to run command ", i, ": ", res[i], "
")
end
else
--处理变量
ngx.say("succeed to run command ", i, ": ", res, "
")
end
end
--简单的关闭连接
local ok, err = red:close()
if not ok then
ngx.say("failed to close: ", err)
return
else
ngx.say("succeed to close redis")
end
以上Lua脚本处于工程目录下的
luaScript/redis/RedisDemo.lua文件中,完成了如下Redis的操作:
(1)连接Redis服务器。
(2)根据key设置缓存值。
(3)根据key获取缓存值。
(4)批量Redis操作。
(5)简单地关闭Redis连接。
在nginx-redis-demo.conf配置文件中编写一个location配置块来使用该脚本,具体代码如下:
简单操作演示 #redis CRUD简单操作演示
location /redis_demo {
content_by_lua_file luaScript/redis/RedisDemo.lua;
}
修改nginx-redis-demo.conf文件后需要重启OpenRestry,然后可以使用浏览器访问其地址/redis_demo,具体的访问结果如图8-21所示。
图8-21 Redis CRUD简单操作演示的访问结
RedisDemo.lua通过require导入了redis-config.lua配置文件,该文件定义了整个项目都需要使用的全局Redis配置信息,其代码如下:
--定义一个统一的redis配置模块
--统一的模块对象
local _Module = {
--redis服务器的地址
host_name = "192.168.233.128";
--redis服务器的端口
port = "6379";
服务器的数据库 --redis服务器的数据库
db = "0";
--redis服务器的密码
password = '123456';
--连接超时时长
timeout = 20000;
--线程池的连接数量
pool_size = 100;
--最大的空闲时间,单位:毫秒
pool_max_idle_time = 10000;
}
return _Module
通过Lua操作Redis会涉及获取连接、操作数据、连接回收等基础性工作,这里建议将这些基础性工作封装到一个Redis操作的基础类,主要的代码如下:
local redis = require "resty.redis"
local basic = require("luaScript.module.common.basic");
local config = require("luaScript.module.config.redis-config");
--连接池大小
local pool_size = config.pool_size;
--最大的空闲时间,单位:毫秒
local pool_max_idle_time = config.pool_max_idle_time;
--一个统一的模块对象
local _Module = {}
_Module.__index = _Module
--类的方法new
function _Module.new(self)
local object = { red = nil }
setmetatable(object, self)
return object
end
--获取redis连接
function _Module.open(self)
local red = redis:new()
--设置超时的时间为 2 sec,connect_timeout, send_timeout, read_timeout
red:set_timeout(config.timeout, config.timeout, config.timeout);
local ok, err = red:connect( config.host_name, config.port)
if not ok then
basic.error("连接redis服务器失败: ", err)
return false;
end
if config.password then
red:auth(config.password)
end
if config.db then
red:select(config.db)
end
basic.log("连接redis服务器成功")
self.red = red;
return true;
end
--缓存值
function _Module.setValue(self, key, value)
ok, err = self.red:set(key, value)
if not ok then
basic.error("redis缓存设置失败")
return false;
end basic.log("set result ok")
return true;
end
--值递增
function _Module.incrValue(self, key)
ok, err = self.red:incr(key)
if not ok then
basic.error("redis缓存递增失败 ")
return false;
end
basic.log("incr ok")
return true;
end
--过期
function _Module.expire(self, key, seconds)
ok, err = self.red:expire(key, seconds)
if not ok then
basic.error("redis设置过期失败 ")
return false;
end
return true;
end
--获取值
function _Module.getValue(self, key)
local resp, err = self.red:get(key)
if not resp then
basic.error("redis缓存读取失败 ")
return nil;
end
return resp;
end
--省略封装的其他Redis操作方法
--将连接还给连接池
function _Module.close(self)
if not self.red then
return
end
local ok, err = self.red:set_keepalive(pool_max_idle_time, pool_size)
if not ok then
basic.error("redis set_keepalive执行失败 ")
end
basic.log("redis连接释放成功")
end
return _Module
此基础操作类的名称为RedisOperator,需要使用时通过require("
luaScript.redis.RedisOperator")导入即可。
在示例代码RedisDemo.lua脚本中,每一次客户端请求,luaresty-redis模块都会创建一个新的Redis连接。在生产环境中,每一次请求都开启一个服务器新连接会导致以下问题:
(1)连接资源被快速消耗。
(2)网络一旦抖动,会有大量TIME_WAIT连接产生,需要定期重启服务程序或机器。
(3)服务器工作不稳定,QPS忽高忽低。
(4)性能普遍上不去。
为什么会出现这些性能问题呢?因为每一次传输数据,我们需要完成创建连接、收发数据、拆除连接3个基本步骤,在低并发场景下每次请求完整走完这3步基本上不会有什么问题。然而,一旦挪到高并发应用场景,性能问题就出现了。
性能优化的第一件事情就是把短连接改成长连接,可以减少大量创建连接、拆除连接的时间。从性能上来说肯定要比短连接好很多,但还是有比较大的浪费。
性能优化的第二件事情就是使用连接池。通过一个连接池pool将所有长连接缓存和管理起来,谁需要使用,就从这里取走,干完活立马放回来。
实际上,大家在开发过程中用到的连接池是非常多的,比如HTTP连接池、数据库连接池、消息推送连接池等。实际上,几乎所有点到点之间的连接资源复用都需要通过连接池完成。在OpenResty中,lua-resty-redis模块管理了一个连接池,并且定义了set_keepalive方法完成连接的回收和复用。set_keepalive方法的语法如下:
ok, err = red:set_keepalive(max_idle_timeout, pool_size)
该方法将当前的Redis连接立即放入连接池。其中,max_idle_timeout参数指定连接在池中的最大空闲超时时长(以毫秒为单位);pool_size参数指定每个Nginx工作进程的连接池的最大连接数。如果入池成功,就返回1;如果入池出现错误,就返回nil,并返回错误描述字符串。
下面看一个连接回收的示例,具体的代码如下:
location /pool_demo {
content_by_lua_block {
local redis = require "resty.redis"
local config = require("luaScript.module.config.redis-config");
--连接池大小
local pool_size = config.pool_size;
--最大的空闲时间,单位:毫秒
local pool_max_idle_time = config.pool_max_idle_time;
local red = redis:new()
local ok, err = red:connect(config.host_name, config.port)
if not ok then
ngx.say("failed to connect: ", err)
return
else
ngx.say("succeed to connect redis", "
")
end
red: auth(config.password)
--red: set_keepalive(pool_max_idle_time, pool_size) --① 坑
ok, err = red:set("dog", "an animal")
if not ok then
--red: set_keepalive(pool_max_idle_time, pool_size) --② 坑
return
end
--③ 正确回收
red: set_keepalive(pool_max_idle_time, pool_size)
ngx.say("succeed to collect redis connection", "
")
}
}
以上代码中,有3个需要注意的地方,具体介绍如下:
①坑:只有数据传输完毕、Redis连接使用完成之后,才能调用set_keepalive方法将连接放到池子里,set_keepalive方法会立即将red连接对象转换到closed状态,后面的Redis调用将出错。
②坑:如果设置错误,那么red连接对象不一定可用,不能把可用性存疑的连接放回池子里,如果另一个请求从连接池获取到一个不能用的连接,就会直接报错。
③正确回收:此处的set_keepalive方法调用是正确的。
以上代码处于nginx-redis-demo.conf文件中,修改后需要重启OpenRestry,然后可以使用浏览器访问其地址/pool_demo,具体的访问结果如图8-22所示。
图8-22 Redis连接池演示实例的执行结果
set_keepalive方法完成连接回收之后,下一次通过red:
connect(...)获取连接时,connect方法在创建新连接前会在连接池中查找空闲连接,只有查找失败才会真正创建新连接。
总之,作为一个专业的服务端开发工程师,大家必须要对连接池有较深理解,其实不论是Redis连接池、HTTP连接池,还是数据库连接池,甚至是线程池,其原理都是差不多的。