Redis 接口的二次封装
先看一下官方的调用示例代码:
local redis = require("resty.redis")local red = redis:new()red:set_timeout(1000) -- 1 seclocal ok, err = red:connect("127.0.0.1", 6379)if not ok thenngx.say("failed to connect: ", err)returnendok, err = red:set("dog", "an animal")if not ok thenngx.say("failed to set dog: ", err)returnendngx.say("set result: ", ok)-- put it into the connection pool of size 100,-- with 10 seconds max idle timelocal ok, err = red:set_keepalive(10000, 100)if not ok thenngx.say("failed to set keepalive: ", err)returnend
这是一个标准的 Redis 接口调用,如果你的代码中 Redis 被调用频率不高,那么这段代码不会有任何问题。
但如果你的项目重度依赖 Redis,工程中有大量的程序在重复 创建连接—>数据操作—>关闭连接(或放到连接池) 这条完整的链路。甚至在调用完毕还要考虑不同的 return 情况做不同处理,你很快就会发现代码有大量的重复。
Lua 是不支持面向对象的。很多人用尽各种招数使用元表来模拟。可是,Lua 的发明者似乎不想看到这样的情形,因为他们把取长度的 __len 方法以及析构函数 __gc 留给了 C API,纯 Lua 只能望洋兴叹。
我们期望的代码应该是这样的:
local red = redis:new()local ok, err = red:set("dog", "an animal")if not ok thenngx.say("failed to set dog: ", err)returnendngx.say("set result: ", ok)local res, err = red:get("dog")if not res thenngx.say("failed to get dog: ", err)returnendif res == ngx.null thenngx.say("dog not found.")returnendngx.say("dog: ", res)
期望它自身具备以下几个特征:
new、connect函数合体,使用时只负责申请,尽量少关心什么时候具体连接、释放;- 默认 Redis 数据库连接地址,但是允许自定义;
- 每次 Redis 使用完毕,自动释放 Redis 连接到连接池供其他请求复用;
- 要支持 Redis 的重要优化手段 pipeline;
不卖关子,只要干货,我们最后是这样干的,可以这里看到 gist代码
-- file name: resty/redis_iresty.lualocal redis_c = require("resty.redis")local ok, new_tab = pcall(require, "table.new")if not ok or type(new_tab) ~= "function" thennew_tab = function (narr, nrec) return {} endendlocal _M = new_tab(0, 155)_M._VERSION = '0.01'local commands = {"append", "auth", "bgrewriteaof","bgsave", "bitcount", "bitop","blpop", "brpop","brpoplpush", "client", "config","dbsize","debug", "decr", "decrby","del", "discard", "dump","echo","eval", "exec", "exists","expire", "expireat", "flushall","flushdb", "get", "getbit","getrange", "getset", "hdel","hexists", "hget", "hgetall","hincrby", "hincrbyfloat", "hkeys","hlen","hmget", "hmset", "hscan","hset","hsetnx", "hvals", "incr","incrby", "incrbyfloat", "info","keys","lastsave", "lindex", "linsert","llen", "lpop", "lpush","lpushx", "lrange", "lrem","lset", "ltrim", "mget","migrate","monitor", "move", "mset","msetnx", "multi", "object","persist", "pexpire", "pexpireat","ping", "psetex", "psubscribe","pttl","publish", --[[ "punsubscribe", ]] "pubsub","quit","randomkey", "rename", "renamenx","restore","rpop", "rpoplpush", "rpush","rpushx", "sadd", "save","scan", "scard", "script","sdiff", "sdiffstore","select", "set", "setbit","setex", "setnx", "setrange","shutdown", "sinter", "sinterstore","sismember", "slaveof", "slowlog","smembers", "smove", "sort","spop", "srandmember", "srem","sscan","strlen", --[[ "subscribe", ]] "sunion","sunionstore", "sync", "time","ttl","type", --[[ "unsubscribe", ]] "unwatch","watch", "zadd", "zcard","zcount", "zincrby", "zinterstore","zrange", "zrangebyscore", "zrank","zrem", "zremrangebyrank", "zremrangebyscore","zrevrange", "zrevrangebyscore", "zrevrank","zscan","zscore", "zunionstore", "evalsha"}local mt = { __index = _M }local function is_redis_null( res )if type(res) == "table" thenfor k,v in pairs(res) doif v ~= ngx.null thenreturn falseendendreturn trueelseif res == ngx.null thenreturn trueelseif res == nil thenreturn trueendreturn falseend-- change connect address as you needfunction _M.connect_mod( self, redis )redis:set_timeout(self.timeout)return redis:connect("127.0.0.1", 6379)endfunction _M.set_keepalive_mod( redis )-- put it into the connection pool of size 100, with 60 seconds max idle timereturn redis:set_keepalive(60000, 1000)endfunction _M.init_pipeline( self )self._reqs = {}endfunction _M.commit_pipeline( self )local reqs = self._reqsif nil == reqs or 0 == #reqs thenreturn {}, "no pipeline"elseself._reqs = nilendlocal redis, err = redis_c:new()if not redis thenreturn nil, errendlocal ok, err = self:connect_mod(redis)if not ok thenreturn {}, errendredis:init_pipeline()for _, vals in ipairs(reqs) dolocal fun = redis[vals[1]]table.remove(vals , 1)fun(redis, unpack(vals))endlocal results, err = redis:commit_pipeline()if not results or err thenreturn {}, errendif is_redis_null(results) thenresults = {}ngx.log(ngx.WARN, "is null")end-- table.remove (results , 1)self.set_keepalive_mod(redis)for i,value in ipairs(results) doif is_redis_null(value) thenresults[i] = nilendendreturn results, errendfunction _M.subscribe( self, channel )local redis, err = redis_c:new()if not redis thenreturn nil, errendlocal ok, err = self:connect_mod(redis)if not ok or err thenreturn nil, errendlocal res, err = redis:subscribe(channel)if not res thenreturn nil, errendres, err = redis:read_reply()if not res thenreturn nil, errendredis:unsubscribe(channel)self.set_keepalive_mod(redis)return res, errendlocal function do_command(self, cmd, ... )if self._reqs thentable.insert(self._reqs, {cmd, ...})returnendlocal redis, err = redis_c:new()if not redis thenreturn nil, errendlocal ok, err = self:connect_mod(redis)if not ok or err thenreturn nil, errendlocal fun = redis[cmd]local result, err = fun(redis, ...)if not result or err then-- ngx.log(ngx.ERR, "pipeline result:", result, " err:", err)return nil, errendif is_redis_null(result) thenresult = nilendself.set_keepalive_mod(redis)return result, errendfor i = 1, #commands dolocal cmd = commands[i]_M[cmd] =function (self, ...)return do_command(self, cmd, ...)endendfunction _M.new(self, opts)opts = opts or {}local timeout = (opts.timeout and opts.timeout * 1000) or 1000local db_index= opts.db_index or 0return setmetatable({timeout = timeout,db_index = db_index,_reqs = nil }, mt)endreturn _M
调用示例代码:
local redis = require("resty.redis_iresty")
local red = redis:new()
local 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)
在最终的示例代码中看到,所有的连接创建、连接销毁、以及连接池部分,都被完美隐藏了,我们只需要业关注务就可以了。妈妈再也不用担心我把 Redis 搞垮了。
