nginx+lua+redis防刷,lua代码

来源:互联网 发布:缩鼻翼手术多少钱知乎 编辑:程序博客网 时间:2024/05/12 23:41
问题:

当机器过多时,在每台机器的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




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

原创粉丝点击