• -------------------------------------------------------------
  • ====================================

lua开发–lua模块和redis

nginx_lua dewbay 6年前 (2019-04-12) 2474次浏览 已收录 0个评论 扫描二维码

lua 模块开发

在实际开发中,不可能把所有代码写到一个大而全的lua文件中,需要进行分模块开发;而且模块化是高性能 Lua 应用的关键。使用 require 第一次导入模块后,所有 Nginx 进程全局共享模块的数据和代码,每个 Worker 进程需要时会得到此模块的一个副本(Copy-On-Write),即模块可以认为是每 Worker 进程共享而不是每 Nginx Server 共享;另外注意之前我们使用 init_by_lua中初始化的全局变量是每请求复制一个;如果想在多个 Worker 进程间共享数据可以使用 ngx.shared.DICT 或如 Redis 之类的存储。

在/usr/example/lualib 中已经提供了大量第三方开发库如 cjson、redis客户端、mysql 客户端:

cjson.so

resty/

   aes.lua

   core.lua

   dns/

   lock.lua

   lrucache/

   lrucache.lua

   md5.lua

   memcached.lua

   mysql.lua

   random.lua

   redis.lua

   ……

需要注意在使用前需要将库在 nginx.conf 中导入:

   #lua 模块路径,其中”;;”表示默认搜索路径,默认到/usr/servers/nginx 下找  
    lua_package_path "/usr/example/lualib/?.lua;;";  #lua 模块  
    lua_package_cpath "/usr/example/lualib/?.so;;";  #c 模块   

使用方式是在 lua 中通过如下方式引入

    local cjson = require(“cjson”)  
    local redis = require(“resty.redis”)   

接下来我们来开发一个简单的lua 模块

vim /usr/example/lualib/module1.lua  
    local count = 0  
    local function hello()  
       count = count + 1  
       ngx.say("count : ", count)  
    end  

    local _M = {  
       hello = hello  
    }  

    return _M  

开发时将所有数据做成局部变量/局部函数;通过 _M 导出要暴露的函数,实现模块化封装。

接下来创建 test_module_1.lua.

vim /usr/example/lua/test_module_1.lua  
  local module1 = require("module1")  

    module1.hello()  

使用 local var = require(“模块名”),该模块会到 lua_package_path 和 lua_package_cpath 声明的的位置查找我们的模块,对于多级目录的使用 require(“目录 1.目录 2.模块名”)加载。

example.conf 配置

 location /lua_module_1 {  
        default_type 'text/html';  
        lua_code_cache on;  
        content_by_lua_file /usr/example/lua/test_module_1.lua;  
    }  

访问如http://192.168.1.2/lua_module_1进行测试,会得到类似如下的数据,count 会递增

count : 1

count :2

……

count :N

此时可能发现 count 一直递增,假设我们的 worker_processes 2,我们可以通过 kill -9 nginx worker process 杀死其中一个 Worker 进程得到 count 数据变化。

假设我们创建了 vim /usr/example/lualib/test/module2.lua 模块,可以通过 local module2 = require(“test.module2”)加载模块

基本的模块开发就完成了,如果是只读数据可以通过模块中声明 local 变量存储;如果想在每 Worker 进程共享,请考虑竞争;如果要在多个 Worker 进程间共享请考虑使用 ngx.shared.DICT 或如 Redis 存储。


常见的 lua 开发库

对于开发来说需要有好的生态开发库来辅助我们快速开发,而 Lua 中也有大多数我们需要的第三方开发库如 Redis、Memcached、Mysql、Http 客户端、JSON、模板引擎等。

一些常见的 Lua 库可以在 github 上搜索,https://github.com/search?utf8=%E2%9C%93&q=lua+resty

Redis 客户端

lua-resty-redis是为基于 cosocket API 的 ngx_lua 提供的 Lua redis客户端,通过它可以完成 Redis 的操作。默认安装 OpenResty 时已经自带了该模块,使用文档可参考https://github.com/openresty/lua-resty-redis

在测试之前请启动 Redis 实例:

nohup /usr/servers/redis-2.8.19/src/redis-server  /usr/servers/redis-2.8.19/redis_6660.conf &

基本操作

编辑 test_redis_baisc.lua

    local function close_redis(red)  
        if not red then  
            return  
        end  
        local ok, err = red:close()  
        if not ok then  
            ngx.say("close redis error : ", err)  
        end  
    end  

    local redis = require("resty.redis")  

    --创建实例  
    local red = redis:new()  
    --设置超时(毫秒)  
    red:set_timeout(1000)  
    --建立连接  
    local ip = "127.0.0.1"  
    local port = 6660  
    local ok, err = red:connect(ip, port)  
    if not ok then  
        ngx.say("connect to redis error : ", err)  
        return close_redis(red)  
    end  
    --调用 API 进行处理  
    ok, err = red:set("msg", "hello world")  
    if not ok then  
        ngx.say("set msg error : ", err)  
        return close_redis(red)  
    end  

    --调用 API 获取数据  
    local resp, err = red:get("msg")  
    if not resp then  
        ngx.say("get msg error : ", err)  
        return close_reedis(red)  
    end  
    --得到的数据为空处理  
    if resp == ngx.null then  
        resp = ''  --比如默认值  
    end  
    ngx.say("msg : ", resp)  

    close_redis(red)  

基本逻辑很简单,要注意此处判断是否为 nil,需要跟 ngx.null 比较。

example.conf 配置文件

     location /lua_redis_basic {  
        default_type 'text/html';  
        lua_code_cache on;  
        content_by_lua_file /usr/example/lua/test_redis_basic.lua;  
    }  

3、访问如http://192.168.1.2/lua_redis_basic进行测试,正常情况得到如下信息

msg : hello world

连接池

建立 TCP 连接需要三次握手而释放 TCP 连接需要四次握手,而这些往返时延仅需要一次,以后应该复用 TCP 连接,此时就可以考虑使用连接池,即连接池可以复用连接。

我们只需要将之前的 close_redis 函数改造为如下即可:

    local function close_redis(red)  
        if not red then  
            return  
        end  
        --释放连接(连接池实现)  
        local pool_max_idle_time = 10000 --毫秒  
        local pool_size = 100 --连接池大小  
        local ok, err = red:set_keepalive(pool_max_idle_time, pool_size)  
        if not ok then  
            ngx.say("set keepalive error : ", err)  
        end  
    end  

即设置空闲连接超时时间防止连接一直占用不释放;设置连接池大小来复用连接。

此处假设调用 red:set_keepalive(),连接池大小通过 nginx.conf 中 http 部分的如下指令定义:

#默认连接池大小,默认 30

lua_socket_pool_size 30;

#默认超时时间,默认 60s

lua_socket_keepalive_timeout 60s;

注意:

1、连接池是每 Worker 进程的,而不是每 Server 的;

2、当连接超过最大连接池大小时,会按照 LRU 算法回收空闲连接为新连接使用;

3、连接池中的空闲连接出现异常时会自动被移除;

4、连接池是通过 ip 和 port 标识的,即相同的 ip 和 port 会使用同一个连接池(即使是不同类型的客户端如 Redis、Memcached);

5、连接池第一次 set_keepalive 时连接池大小就确定下了,不会再变更;

5、cosocket 的连接池http://wiki.nginx.org/HttpLuaModule#tcpsock:setkeepalive

pipeline

pipeline 即管道,可以理解为把多个命令打包然后一起发送;MTU(Maxitum Transmission Unit 最大传输单元)为二层包大小,一般为 1500 字节;而 MSS(Maximum Segment Size 最大报文分段大小)为四层包大小,其一般是 1500-20(IP 报头)-20(TCP 报头)=1460 字节;因此假设我们执行的多个 Redis 命令能在一个报文中传输的话,可以减少网络往返来提高速度。因此可以根据实际情况来选择走 pipeline 模式将多个命令打包到一个报文发送然后接受响应,而 Redis 协议也能很简单的识别和解决粘包。

1、修改之前的代码片段

    red:init_pipeline()  
    red:set("msg1", "hello1")  
    red:set("msg2", "hello2")  
    red:get("msg1")  
    red:get("msg2")  
    local respTable, err = red:commit_pipeline()  

    --得到的数据为空处理  
    if respTable == ngx.null then  
        respTable = {}  --比如默认值  
    end  

    --结果是按照执行顺序返回的一个 table  
    for i, v in ipairs(respTable) do  
       ngx.say("msg : ", v, "<br/>")  
    end  

通过 init_pipeline()初始化,然后通过 commit_pipieline()打包提交 init_pipeline()之后的 Redis 命令;返回结果是一个 lua table,可以通过 ipairs 循环获取结果;

2、配置相应 location,测试得到的结果

msg : OK 
msg : OK 
msg : hello1 
msg : hello2

3、Redis Lua 脚本

利用 Redis 单线程特性,可以通过在 Redis 中执行 Lua 脚本实现一些原子操作。如之前的 red:get(“msg”)可以通过如下两种方式实现:

1、直接 eval:

local resp, err = red:eval("return redis.call('get', KEYS[1])", 1, "msg"); 

2、script load 然后 evalsha SHA1 校验和,这样可以节省脚本本身的服务器带宽:

local sha1, err = red:script("load",  "return redis.call('get', KEYS[1])");  
if not sha1 then  
   ngx.say("load script error : ", err)  
   return close_redis(red)  
end  
ngx.say("sha1 : ", sha1, "<br/>")  
local resp, err = red:evalsha(sha1, 1, "msg"); 

首先通过 script load 导入脚本并得到一个 sha1 校验和(仅需第一次导入即可),然后通过 evalsha 执行 sha1 校验和即可,这样如果脚本很长通过这种方式可以减少带宽的消耗。

此处仅介绍了最简单的 redis lua 脚本,更复杂的请参考官方文档学习使用。

另外 Redis 集群分片算法该客户端没有提供需要自己实现,当然可以考虑直接使用类似于 Twemproxy 这种中间件实现。

Memcached 客户端使用方式和本文类似,本文就不介绍了。

转载 http://jinnianshilongnian.iteye.com/blog/2187328


露水湾 , 版权所有丨如未注明 , 均为原创丨本网站采用BY-NC-SA协议进行授权
转载请注明原文链接:lua开发–lua模块和redis
喜欢 (0)
[]
分享 (0)
关于作者:
发表我的评论
取消评论

表情 贴图 加粗 删除线 居中 斜体 签到

Hi,您需要填写昵称和邮箱!

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址