nginx+lua+redis防刷,lua代码

问题:

当机器过多时,在每台机器的nginx上用nginx自带防刷模块,往往限制太松。


思路:

多台机器,通过nginx-lua模块,连接redis,以ip(记得nginx安装real-ip模块,取到x-forword-for字段对应的真实用户ip)来更新访问次数,并根据redis设置的阀值进行比较,决定是否限流,不考虑并发更新丢值情况,因为访问次数足够时,总能到达阀值。


优化:

1、redis发送请求,需要查访问次数和阀值,发两次请求浪费资源,可进行合并。
通过eval指令,执行redis-lua脚本,一次性返回两个字段。

2、每次发送redis执行的lua脚本,传输字符较多,浪费带宽。
通过evalsha执行脚本对应的校验码。若redis执行过一个lua脚本后,会记录脚本,并生成对应的校验码,可通过evalsha指令,参数为对应的校验码,即可执行脚本。

3、(不采用)建立nginx到redis的连接池,防止每次都建立链接。但考虑到以下两点,所以放弃使用,如有错误和方案,麻烦指正。
(1)、连接池不应该很大,所以在并发量很大的时候,大多数请求还是要从新建立连接。
(2)、要维护连接池,保证同一时刻没有两个请求共用一个链接,造成连接关闭的异常。没有一个变量,用来标识调用不同的连接。

lua代码(可直接使用,亲测可用):

首先需要在nginx上输入下面的配置:
lua_shared_dict ngx_shared_redis 1m;
建立一个从nginx启动后便存在的共享变量,1m大小(可以小点),名字叫redis。用于存储生成的redis-lua叫的校验码,用ngx.shared.redis:get("redis")方式来获取。

--关闭连接
local function close_redis(red)
    if not red then
        return
    end
    red:close()
end
--主要处理函数
local function get_limit()
    --加载模块
    local red = require("resty.redis"):new()
    red:set_timeout(100)
    local ok, err = red:connect("127.0.0.1", 6379)
    if not ok then
        ngx.log(ngx.ERR, "redis_connect:"..err)
        return close_redis(red)
    end
    --根据ip作为key,来防刷
    local key = ngx.var.remote_addr
    --判断之前是否已经执行过redis-lua脚本,且有对应的校验码值用于调用
    if not ngx.shared.redis:get("redis") then
        --若之前未执行过,第一次执行
        local script = table.concat({
            --手动拼接为一行,两个参数,第一个为key即ip,第二个为限制阀值key
            "local val = redis.call('get',KEYS[1]) ",
            "if val then ",
                "val = redis.call('incr',KEYS[1]) ",
                "return {val,redis.call('get', KEYS[2])} ",
            "end ",
            "redis.call('set',KEYS[1],1) ",
            "redis.call('expire',KEYS[1],60) ",
            "return {0,nil}"
        })
        --在redis加载对应的lua脚本
        local sha1, err = red:script("load", script)
        if not sha1 then
            ngx.log(ngx.ERR, "load_script:"..err)
            return close_redis(red)
        end
        --拿到生成的校验码,更新对应的字段
        ngx.shared.redis:set("redis", sha1)
    end
    --根据校验码,执行脚本,cart_limit为限制阀值key
    local resp, err = red:evalsha(ngx.shared.redis:get("redis"), 2, key, "cart_limit");
    if not resp then
        --若执行失败,一种情况为,redis清空了脚本缓存,此情况下,退出并删除存储的校验码,等下次执行,再更新
        ngx.log(ngx.ERR, "not_resp:"..err)
        ngx.shared.redis:delete("redis")
        return close_redis(red)
    end
    --限制值若redis没有,默认为1200,若有则判断是否大于120
    local limit = 1200
    if resp[2] then
        local new_limit = tonumber(resp[2])
        limit = new_limit > 120 and new_limit or 1200
    end
    --若次数超过限制,则拦截
    if resp[1] > limit then
        ngx.exit(ngx.HTTP_FORBIDDEN)
    end
    close_redis(red)
end
--用pcall,调用脚本,类似try-catch,如有错误,打印脚本
if not pcall(get_limit) then
    ngx.log(ngx.ERR, "lua error")
end




以上若有错误,欢迎指正,谢谢!

你可能感兴趣的:(nginx+lua+redis防刷,lua代码)