当前位置:网站首页>涨薪5K必学高并发核心编程,限流原理与实战,分布式计数器限流
涨薪5K必学高并发核心编程,限流原理与实战,分布式计数器限流
2022-08-03 19:43:00 【知食份子.】
分布式计数器限流
分布式计算器限流是使用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 objectend--方法:判断是否能通过流量控制--返回值为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();endreturn _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 = {} };--取得用户的iplocal 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 keylocal 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(),"<br>");end--回收redis连接limiter:close();--如果没有流量控制,就终止nginx的处理流程if not passed then errorOut.resp_msg = "抱歉,被限流了"; ngx.say(cjson.encode(errorOut)); ngx.exit(ngx.HTTP_UNAUTHORIZED);endreturn ;
以上代码位于练习工程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,"<br>"); 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秒endif 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命令如下:
[[email protected] ~]#cd /work/develop/LuaDemoProject/src/luaScript/module/ratelimit/[[email protected] 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()--取得用户的iplocal 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 keylocal 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(),"<br>");end--回收redis连接limiter:close();如果没有流量控制就终止的处理流程--如果没有流量控制,就终止Nginx的处理流程if not passed then errorOut.resp_msg = "抱歉,被限流了"; ngx.say(cjson.encode(errorOut)); ngx.exit(ngx.HTTP_UNAUTHORIZED);endreturn ;
以上代码位于练习工程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,"<br>"); 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编码执行。
边栏推荐
猜你喜欢
【夜莺监控方案】08-监控msyql集群(prometheuse+n9e+mysqld_exporter)
花 30 美金请 AI 画家弄了个 logo,网友:画得非常好,下次别画了!
开发即时通讯到底需要什么样的技术,需要多久的时间
NNLM、RNNLM等语言模型 实现 下一单词预测(next-word prediction)
LeetCode 952. Calculate Maximum Component Size by Common Factor
Anaconda 虚拟环境迁移
机器学习中专业术语的个人理解与总结(纯小白)
告诉你0基础怎么学好游戏建模?
JMeter笔记5 |Badboy使用和录制
演讲议题及嘉宾重磅揭晓,TDengine 开发者大会推动数据技术“破局”
随机推荐
if/else或switch替换为Enum
机器学习中专业术语的个人理解与总结(纯小白)
阿洛的反思
面试突击:什么是粘包和半包?怎么解决?
力扣刷题之数组序号计算(每日一题7/28)
边缘盒子+时序数据库,美的数字化平台 iBuilding 背后的技术选型
开源教育论坛| ChinaOSC
【leetcode】剑指 Offer II 008. 和大于等于 target 的最短子数组(滑动窗口,双指针)
Introduction to Cosine Distance
Unity获取canvas 下ui 在屏幕中的实际坐标
Use ControlTemplate or Style from resource file in WPF .cs and find the control
Force is brushed buckle problem for the sum of two Numbers
CS免杀姿势
盘点在线帮助中心对企业能够起到的作用
ECCV 2022 Oral | 满分论文!视频实例分割新SOTA: IDOL
The addition and subtraction of the score of the force deduction brush question (a daily question 7/27)
DeepMCP网络详解
LeetCode 622. Designing Circular Queues
线上一次JVM FullGC搞得整晚都没睡,彻底崩溃
余弦距离介绍