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

2022-10-28 11:46:18 浏览数 (1)

分布式计数器限流

分布式计算器限流是使用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的存储访问,具体的代码如下:

代码语言:javascript复制
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) > ; --限流的次数 --被限流 if limited then redis:incrValue(self.key); return false; end if not current or current == ngx.null then redis:setValue(self.key, ); redis:expire(self.key, ); --限流的时间范围 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 ;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的格式如下:

代码语言:javascript复制
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脚本进行请求限流的配置代码如下:

代码语言:javascript复制
location = /access/demo/nginx/lua { set $count ; 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次之后就会被限流。在浏览器中输入如下测试地址:

代码语言:javascript复制
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的代码如下:

代码语言:javascript复制
---此脚本的环境:redis内部,不是运行在Nginx内部--返回表示被限流,返回其他表示统计的次数local cacheKey = KEYS[]local data = redis.call("incr", cacheKey)local count=tonumber(data)--首次访问,设置过期时间if count ==  then redis.call("expire", cacheKey, ) --设置超时时间秒endif count >  then --设置超过的限制为人表示需要限流 return ; --表示需要限流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命令如下:

代码语言:javascript复制
[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限流脚本的代码如下:

代码语言:javascript复制
---此脚本的环境:nginx内部local RedisKeyRateLimiter = require("luaScript.module.ratelimit.RedisKeyRateLimiter");--定义出错的JSON输出对象local errorOut = { resp_code = -, 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脚本进行请求限流的配置如下:

代码语言:javascript复制
 location = /access/demo/evalsha/lua { set $count ; 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。

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

代码语言:javascript复制
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. 感谢大家的支持!

0 人点赞