涨薪5K必学高并发核心编程,限流原理与实战,分布式计数器限流

愿天堂没有BUG

共 6849字,需浏览 14分钟

 · 2022-03-02


分布式计数器限流

分布式计算器限流是使用Redis存储限流关键字key的统计计数。

这里介绍两种限流的实现方案:Nginx Lua分布式计数器限流和RedisLua分布式计数器限流。

实战:Nginx Lua分布式计数器限流

本小节以对用户IP计数器限流为例实现单IP在一定时间周期(如10秒)内只能访问一定次数(如10次)的限流功能。由于使用到Redis存储分布式访问计数,通过Nginx Lua编程完成全部功能,因此这里将这种类型的限流称为Nginx Lua分布式计数器限流。

本小节的Nginx Lua分布式计数器限流案例架构如图9-3所示。

图9-3 Nginx Lua分布式计数器限流架构

首先介绍限流计数器脚本RedisKeyRateLimiter.lua,该脚本负责完成访问计数和限流的结果判断,其中涉及Redis的存储访问,具体的代码如下:

local redisExecutor = require("luaScript.redis.RedisOperator");
--一个统一的模块对象
local _Module = {}
_Module.__index = _Module
--方法:创建一个新的实例
function _Module.new(self, key)
local object = { red = nil } setmetatable(object, self)
--创建自定义的redis操作对象
local red = redisExecutor:new();
red:open();
object.red = red;
object.key = "count_rate_limit:" .. key;
return object
end
--方法:判断是否能通过流量控制
--返回值为true表示通过流量控制,返回值为false表示被限制
function _Module.acquire(self)
local redis = self.red;
local current = redis:getValue(self.key);
--判断是否大于限制次数
local limited = current and current ~= ngx.null and tonumber(current) > 10; --限流的次数
--被限流
if limited then
redis:incrValue(self.key);
return false;
end
if not current or current == ngx.null then
redis:setValue(self.key, 1);
redis:expire(self.key, 10); --限流的时间范围
else
redis:incrValue(self.key);
end
return true;
end
--方法:取得访问次数,供演示使用
function _Module.getCount(self)
local current = self.red:getValue(self.key);
if current and current ~= ngx.null then
return tonumber(current);
end
return 0;
end
--方法:归还redis连接
function _Module.close(self)
self.red:close();
end
return _Module
以上代码位于练习工程LuaDemoProject的
src/luaScript/module/ratelimit/文件夹下,文件名称为
RedisKeyRateLimiter.lua。
然后介绍access_auth_nginx限流脚本,该脚本使用前面定义的
RedisKeyRateLimiter.lua通用访问计算器脚本,完成针对同一个IP的限流操
作,具体的代码如下:
---此脚本的环境:nginx内部
---启动调试
--local mobdebug = require("luaScript.initial.mobdebug");
--mobdebug.start();
--导入自定义的计数器模块
local RedisKeyRateLimiter = require("luaScript.module.ratelimit.RedisKeyRateLimiter");
定义出错的
输出对象--定义出错的JSON输出对象
local errorOut = { resp_code = -1, resp_msg = "限流出错", datas = {} };
--取得用户的ip
local shortKey = ngx.var.remote_addr;
--没有限流关键字段,提示错误
if not shortKey or shortKey == ngx.null then
errorOut.resp_msg = "shortKey不能为空"
ngx.say(cjson.encode(errorOut));
return ;
end
--拼接计数的redis key
local key = "ip:" .. shortKey;
local limiter = RedisKeyRateLimiter:new(key);
local passed = limiter:acquire();
--如果通过流量控制
if passed then
ngx.var.count = limiter:getCount();
--注意,在这里直接输出会导致content阶段的指令被跳过
--ngx.say( "目前的访问总数:",limiter:getCount(),"
");

end
--回收redis连接
limiter:close();
--如果没有流量控制,就终止nginx的处理流程
if not passed then
errorOut.resp_msg = "抱歉,被限流了";
ngx.say(cjson.encode(errorOut));
ngx.exit(ngx.HTTP_UNAUTHORIZED);
end
return ;

以上代码位于练习工程LuaDemoProject的
src/luaScript/module/ratelimit/文件夹下,文件名称为access_auth_nginx.lua。access_auth_nginx.lua在拼接计数器的key时使用了Nginx的内置变量$remote_addr获取客户端的IP地址,最终在Redis存储访问计数的key的格式如下:

count_rate_limit:ip:192.168.233.1

这里的192.168.233.1为笔者本地的测试IP,存储在Redis中针对此IP的限流计数结果如图9-4所示。

图9-4 存储在Redis中针对此IP的限流计数结果

在Nginx的access请求处理阶段,使用access_auth_nginx.lua脚本进行请求限流的配置代码如下:

location = /access/demo/nginx/lua {
set $count 0;
access_by_lua_file luaScript/module/ratelimit/access_auth_nginx.lua;
content_by_lua_block {
ngx.say( "目前的访问总数:",ngx.var.count,"
"
);
ngx.say("hello world!");
}
}

以上配置位于练习工程LuaDemoProject的src/conf/nginxratelimit.conf文件中,在使之生效之前,需要在openresty-start.sh脚本中换上该配置文件,然后重启Nginx。

接下来,开始限流自验证。

上面的代码中,由于RedisKeyRateLimiter所设置的限流规则为单IP在10秒内限制访问10次,所以,在验证的时候,在浏览器中刷新10次之后就会被限流。在浏览器中输入如下测试地址:

http://nginx.server/access/demo/nginx/lua?seckillGoodId=1

10秒内连续刷新,第6次的输出如图9-5所示。

图9-5 自验证时第6次刷新的输出

10秒之内连续刷新,发现第10次之后请求被限流了,说明Lua限流脚本工作是正常的,被限流后的输出如图9-6所示。

图9-6 自验证时刷新10次之后的输出

以上代码有两点缺陷:

(1)数据一致性问题:计数器的读取和自增由两次Redis远程操作完成,如果存在多个网关同时进行限流,就可能会出现数据一致性问题。

(2)性能问题:同一次限流操作需要多次访问Redis,存在多次网络传输,大大降低了限流的性能。

实战:Redis Lua分布式计数器限流

大家知道,Redis允许将Lua脚本加载到Redis服务器中执行,可以调用大部分Redis命令,并且Redis保证了脚本的原子性。由于既使用Redis存储分布式访问计数,又通过Redis执行限流计数器的Lua脚本,因此这里将这种类型的限流称为RedisLua分布式计数器限流。

本小节的Redis Lua分布式计数器限流案例的架构如图9-7所示。

图9-7 Redis Lua分布式计数器限流架构

首先来看限流的计数器脚本redis_rate_limiter.lua,该脚本负责完成访问计数和限流结果的判断,其中会涉及Redis计数的存储访问。需要注意的是,该脚本将在Redis中加载和执行。

计数器脚本redis_rate_limiter.lua的代码如下:

---此脚本的环境:redis内部,不是运行在Nginx内部
--返回0表示被限流,返回其他表示统计的次数
local cacheKey = KEYS[1]
local data = redis.call("incr", cacheKey)
local count=tonumber(data)
--首次访问,设置过期时间
if count == 1 then
redis.call("expire", cacheKey, 10) --设置超时时间10秒
end
if count > 10 then --设置超过的限制为10人
表示需要限流 return 0; --0表示需要限流
end
--redis.debug(redis.call("get", cacheKey))
return count;

以上代码位于练习工程LuaDemoProject的
src/luaScript/module/ratelimit/文件夹下,文件名为redis_rate_limiter.lua。在调用该脚本之前,首先要将其加载到Redis,并且获取其加载之后的sha1编码,以供Nginx上的限流脚本access_auth_evalsha.lua使用。

将redis_rate_limiter.lua加载到Redis的Linux Shell命令如下:

[root@localhost ~]#cd /work/develop/LuaDemoProject/src/luaScript/module/ratelimit/
[root@localhost ratelimit]#/usr/local/redis/bin/redis-cli script load "$(cat redis_rate_limiter.lua)"
"2c95b6bc3be1aa662cfee3bdbd6f00e8115ac657"

然后来看access_auth_evalsha.lua限流脚本,该脚本使用Redis的evalsha操作指令,远程访问加载在Redis上的redis_rate_limiter.lua访问计算器脚本,完成针对同一个IP的限流操作。

access_auth_evalsha.lua限流脚本的代码如下:

---此脚本的环境:nginx内部
local RedisKeyRateLimiter = require("luaScript.module.ratelimit.RedisKeyRateLimiter");
--定义出错的JSON输出对象
local errorOut = { resp_code = -1, resp_msg = "限流出错", datas = {} };
--读取get参数
local args = ngx.req.get_uri_args()
--取得用户的ip
local shortKey = ngx.var.remote_addr;
--没有限流关键字段,提示错误
if not shortKey or shortKey == ngx.null then
errorOut.resp_msg = "shortKey不能为空"
ngx.say(cjson.encode(errorOut));
return ;
end
--拼接计数的redis key
local key = "count_rate_limit:ip:" .. shortKey;
local limiter = RedisKeyRateLimiter:new(key);
local passed = limiter:acquire();
--如果通过流量控制
if passed then
ngx.var.count = limiter:getCount();
--注意,在这里直接输出会导致content阶段的指令被跳过
--ngx.say( "目前的访问总数:",limiter:getCount(),"
");

end
--回收redis连接
limiter:close();
如果没有流量控制
就终止
的处理流程--如果没有流量控制,就终止Nginx的处理流程
if not passed then
errorOut.resp_msg = "抱歉,被限流了";
ngx.say(cjson.encode(errorOut));
ngx.exit(ngx.HTTP_UNAUTHORIZED);
end
return ;

以上代码位于练习工程LuaDemoProject的
src/luaScript/module/ratelimit/文件夹下,文件名为access_auth_evalsha.lua。在Nginx的access请求处理阶段,使用access_auth_evalsha.lua脚本进行请求限流的配置如下:

 location = /access/demo/evalsha/lua {
set $count 0;
access_by_lua_file luaScript/module/ratelimit/access_auth_evalsha.lua;
content_by_lua_block {
ngx.say( "目前的访问总数:",ngx.var.count,"
"
);
ngx.say("hello world!");
}
}

以上配置位于练习工程LuaDemoProject的
src/conf/nginx-ratelimit.conf文件中,在使之生效之前需要在openresty-start.sh脚本中换上该配置文件,然后重启Nginx。

接下来开始限流自验证。在浏览器中访问以下地址:

http://nginx.server/access/demo/evalsha/lua

10秒之内连续刷新,发现第10次之后请求被限流了,说明Redis内部的Lua限流脚本工作是正常的,被限流后的输出如图9-8所示。

图9-8 自验证时刷新10次之后的输出

通过将Lua脚本加载到Redis执行有以下优势:

(1)减少网络开销:不使用Lua的代码需要向Redis发送多次请求,而脚本只需一次即可,减少网络传输。

(2)原子操作:Redis将整个脚本作为一个原子执行,无须担心并发,也就无须事务。

(3)复用:只要Redis不重启,脚本加载之后会一直缓存在Redis中,其他客户端可以通过sha1编码执行。

本文给大家讲解的内容是高并发核心编程,限流原理与实战,分布式计数器限流

  1. 下篇文章给大家讲解的是高并发核心编程,限流原理与实战,Nginx漏桶限流详解;

  2. 觉得文章不错的朋友可以转发此文关注小编;

  3. 感谢大家的支持!


本文就是愿天堂没有BUG给大家分享的内容,大家有收获的话可以分享下,想学习更多的话可以到微信公众号里找我,我等你哦。

浏览 19
点赞
评论
收藏
分享

手机扫一扫分享

举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

举报