背景

某些业务场景下会有对单位时间内访问频次限制的需求,但是HTTP服务是无状态的,前端客户端又不能信任,所以一般就会在服务器端将用户信息和访问信息做下关联,以此来实现访问频次限制。

通常大家都会选择Redis来作为此中间件的存储介质,毕竟快嘛。而具体的实现又有些不同, 如下:

  • 调用lua脚本,当然在任何业务代码中组合Redis命令也能实现的了,但是会导致代码重复,每一个有此需求的地方,或许都要去实现一遍相似的逻辑。所以一般都会将通用逻辑封装到lua脚本中,供Redis使用。
  • 使用Redis模块redis-cell,这个我没接触过,就不多写了,但好像redis-v4版本以上可用,所以某些生产环境下redis-v3.2.8这种的,也用不了。

今天主要想试试,利用lua脚本来实现一个访问频次限制的需求,但也只是测试环境,实际使用还是要谨慎点。

环境搭建

为了不污染本地环境,我通常会在docker下搞这些测试。

docker pull redis:latest
docker run -d -p 6379:6379 --name MY_REDIS redis
docker ps -a
# 链接测试
redis-cli
keys * # 因为是测试环境,所以敢这么写,生产环境一定要忘记这个命令

基础知识

想想本次实验的目的,基本上就可以明确离不开lua了。所以了解下语法,常规使用什么的,也是很有必要的。

  • 基本知识点 https://www.runoob.com/lua/lua-tutorial.html
  • 在线运行工具 https://c.runoob.com/compile/66

接下来,是个比较重要的知识点,那就是怎么在Redis中debug写好的lua脚本。详细内容可以点击这个链接查看:https://my.oschina.net/floor/blog/1603116

给我的感觉是 lua debugger 的好处就在于可以实时的看到代码的执行效果。 这样不至于我们瞎猜这一步,那一步的执行结果到底是什么了。调试可以大大加快我们对流程,对代码的理解速度。

异步模式

./redis-cli --ldb --eval /tmp/script.lua mykey somekey , arg1 arg2

同步模式

./redis-cli --ldb-sync-mode --eval /tmp/script.lua

使用最频繁的命令有下面这几个。

local name="mark"
local age = 25
local address="xxx县"
return name
快捷键 作用 示例
c 跳到下一个断点 c
p 打印变量 print name
s 步进调试,一步步走 s
n 执行下一行 n
w whole 显示全部代码 w
b line1 line2 lineN 打断点 b 1 3 给第一行,第三行加断点,加完可以从w来看到

实验

使用string结构

local notexists = redis.call("set", KEYS[1], 1, "NX", "EX", tonumber(ARGV[2]))
if (notexists) thenreturn 1
end
local current = tonumber(redis.call("get", KEYS[1]))
if (current == nil) thenlocal result = redis.call("incr", KEYS[1])redis.call("expire", KEYS[1], tonumber(ARGV[2]))return result
end
if (current >= tonumber(ARGV[1])) thenreturn "too many requests"
end
local result = redis.call("incr", KEYS[1])
return result

调用的时候就可以使用如下命令。

➜  lua redis-cli  --eval rate-limiting.lua keyname , 2 100
(integer) 1
➜  lua redis-cli  --eval rate-limiting.lua keyname , 2 100
(integer) 2
➜  lua redis-cli  --eval rate-limiting.lua keyname , 2 100
"too many requests"
➜  lua redis-cli  --eval rate-limiting.lua keyname , 2 100
"too many requests"
➜  lua redis-cli  --eval rate-limiting.lua keyname , 2 100
(integer) 1

期间遇到了一个问题,那就是KEYSARGV不匹配了,这两者要做好分隔。

➜  lua redis-cli --eval rate-limiting.lua keyname  2 100
(error) ERR Error running script (call to f_8e5e7b92016b2c883afbe8b69731686cc5713443): @user_script:1: @user_script: 1: Lua redis() command arguments must be strings or integers

不然Redis不知道哪几个是KEYS,什么时候开始为ARGV。

一定要注意 key 和argv 之间一定要用逗号隔开,为了不必要的问题,逗号两边加上空格,防止key部分沾染到逗号

使用zset结构

看起来可以满足使用,但是总感觉粒度有点粗。预期中应该是一个滑动窗口式 的控制模型。于是打算利用Redis现有支持的数据结构来测试下。

-- 滑动窗口式的频率限制
--[[
KEYS[1]
ARGV[1] 过期时间片段
ARGV[2] 最大频次限制
ARGV[3] 当前Unix秒数
]]--
local maxscore = tonumber(ARGV[3])
local minscore = tonumber(ARGV[3])-tonumber(ARGV[1])
-- 需要明确 zset的member应该是时间戳,score是
local row = redis.call("zrevrangebyscore", KEYS[1], maxscore, minscore, "withscores")
-- <reply> ["1232132146","8","1232132147","6","1232132144","3","1232132145","2"]
-- 做好清理工作
redis.call("expire", KEYS[1], tonumber(ARGV[1]))
local sum = tonumber(redis.call("zincrby", KEYS[1], 1, ARGV[3]))if row == nil then-- 如果没有返回结果,说明是第一次访问,返回true即可return "第一次访问,成功"..sum
end
-- row不为空,计算当前时间片段内的和,是否大于最大限制
for index=1,#row do-- 步长待优化if index%2==0 thensum = sum + tonumber(row[index])end
end-- for k,v in pairs(row) do
--     -- print(k..": "..v)
--     sum = tonumber(sum) + tonumber(k)
-- end
if sum >= tonumber(ARGV[2]) thenreturn "超过了最大限制,失败"..sum
end
return "非第一次访问,成功"..sum

对应的PHP调用就可以:

<?php
$script = <<<SCRIPT
...
SCRIPT;$redis = new Redis();
$redis->pconnect("localhost", 6379);
$ret = $redis->eval($script, ["zset", 10, 6, time()], 1);
var_dump($ret);

调试命令为:

redis-cli --ldb-sync-mode --eval /tmp/ratelimiting2.lua zset , 10 6 1232132144
➜  /tmp redis-cli --ldb-sync-mode --eval /tmp/ratelimiting2.lua zset , 10 6 1232132144
Lua debugging session started, please use:
quit    -- End the session.
restart -- Restart the script in debug mode again.
help    -- Show Lua script debugging commands.* Stopped at 8, stop reason = step over
-> 8   local maxscore = tonumber(ARGV[3])
lua debugger> n
* Stopped at 9, stop reason = step over
-> 9   local minscore = tonumber(ARGV[3])-tonumber(ARGV[1])
lua debugger> b 1110  -- TODO 有点问题,理想为按member排序,score存储访问频次信息,这里的数据结构不合要求。#11  local row = redis.call("zrevrangebyscore", KEYS[1], maxscore, minscore, "withscores")12  -- <reply> ["1232132146","8","1232132147","6","1232132144","3","1232132145","2"]
lua debugger> w1   -- 滑动窗口式的频率限制2   --[[3   KEYS[1]4   ARGV[1] 过期时间片段5   ARGV[2] 最大频次限制6   ARGV[3] 当前Unix秒数7   ]]--8   local maxscore = tonumber(ARGV[3])
-> 9   local minscore = tonumber(ARGV[3])-tonumber(ARGV[1])10  -- TODO 有点问题,理想为按member排序,score存储访问频次信息,这里的数据结构不合要求。#11  local row = redis.call("zrevrangebyscore", KEYS[1], maxscore, minscore, "withscores")12  -- <reply> ["1232132146","8","1232132147","6","1232132144","3","1232132145","2"]13  -- 做好清理工作14  redis.call("expire", KEYS[1], tonumber(ARGV[1]))15  local sum = tonumber(redis.call("zincrby", KEYS[1], 1, ARGV[3]))1617  if row == nil then18      -- 如果没有返回结果,说明是第一次访问,返回true即可19      return "第一次访问,成功"..sum20  end21  -- row不为空,计算当前时间片段内的和,是否大于最大限制22  for index=1,#row do23      -- 步长待优化24      if index%2==0 then25          sum = sum + tonumber(row[index])26      end27  end2829  -- for k,v in pairs(row) do30  --     -- print(k..": "..v)31  --     sum = tonumber(sum) + tonumber(k)32  -- end33  if sum >= tonumber(ARGV[2]) then34      return "超过了最大限制,失败"..sum35  end36  return "非第一次访问,成功"..sum
lua debugger> n
* Stopped at 11, stop reason = break point
->#11  local row = redis.call("zrevrangebyscore", KEYS[1], maxscore, minscore, "withscores")
lua debugger> n
<redis> zrevrangebyscore zset 1232132144 1232132134 withscores
<reply> []
* Stopped at 14, stop reason = step over
-> 14  redis.call("expire", KEYS[1], tonumber(ARGV[1]))
lua debugger>


最后发现这是个鸡肋功能,zrevrangebyscore不能用,人家by的是score,而实验预期是by member。所以不能使用。list结构又缺乏额外 访问时间戳以及对应频次的关系,基于此不太好实现优雅的滑动窗口式的频次限制。

总结

做完这点,有一点自己的思考,觉得多多少少还是有一点问题的。如果非得去实现滑动窗口式的频次限制,当然也是可以做到的,但是代价就是增加了lua脚本的复杂度,对于整体服务效率而言,不是一个好现象;而lua脚本又不是一无是处,某些场景下,原子性要求较高的场景,lua脚本完成的很好。所以还是看场景吧,合适的场景选择合适的方案。

关于访问频次限制的思考相关推荐

  1. MySQL 进阶 索引 -- SQL性能分析(SQL执行频率:查看当前数据库的INSERT、UPDATE、DELETE、SELECT的访问频次、慢查询日志、 profile详情、explain)

    文章目录 1. SQL性能分析 1.1 SQL执行频率(可以查看当前数据库SQL的访问频次) 1.2 慢查询日志(可以记录用时较长的SQL) 1.2.1 开启慢查询日志 1.2.2 慢查询日志测试 1 ...

  2. php接口访问次数,接口访问频次权限

    接口访问频次权限 频次限制 微博开放接口限制每段时间只能请求一定的次数.限制的单位时间有每小时.每天:限制的维度有单授权用户和单IP:部分特殊接口有单独的请求次数限制.例如:• 一个应用内单授权用户每 ...

  3. @requirespermissions注解是什么意思_如何基于spring开发自定义注解实现对接口访问频次限制?...

    做JavaWeb的开发的同学们都应该遇到过,客户要求某个接口进行频次的限制,如每秒并发10个,或者短信验证码发送场景,60秒内只允许发送一次. 通常开发的小伙伴们肯定是拿到以上需求在接口逻辑里进行实现 ...

  4. 设置访问权限_【新思考教学者思】李世松:不要对经典设置访问权限

    不要对经典设置访问权限 --<背影>备课札记 文/李世松     紫阳县举办课堂教学改革推进会,师训教研中心王主任电话通知我讲一节示范课.我知道,这既是对我的一种肯定,更是一次磨炼,因为我 ...

  5. 接口访问次数_如何基于spring开发自定义注解实现对接口访问频次限制?

    做JavaWeb的开发的同学们都应该遇到过,客户要求某个接口进行频次的限制,如每秒并发10个,或者短信验证码发送场景,60秒内只允许发送一次. 通常开发的小伙伴们肯定是拿到以上需求在接口逻辑里进行实现 ...

  6. tp5 限制访问频次

    效果 1.开启Redis 打开你的Redis软件没有的话可以在小皮环境 软件管理中安装 2 .tp5配置config.php 'cache' => [// 驱动方式'type' => 'r ...

  7. thinkphp6限制接口访问频次

    安装扩展包 composer require topthink/think-throttle 在 config/throttle.php 配置选项: // 缓存键前缀,防止键值与其他应用冲突'pref ...

  8. Java子类访问父类私有变量的思考

    示例如下: 父类User,包含私有变量name和money: 以及两个构造函数和基本的getter方法. public class User {public User() {}public User( ...

  9. Redis构建频次访问控制器(一)

    Redis频次访问控制器 设计思路 频次控制旨在控制某个用户接触到某个广告的次数,以达到提高广告性价比的目的,一般来说,随着某个用户看到同一个广告频次的逐渐上升,点击率呈逐渐下降的趋势,因此在按照CP ...

最新文章

  1. SAP UI5 /UI5/IF_UI5_REP_PERSISTENCE - why I cannot deploy app to GM6
  2. WCF开发入门的六个步骤
  3. Spring Boot笔记-利用Quartz进行定时任务,利用websocket推送到浏览器(界面为thymeleaf)
  4. 2017职称英语和计算机考试,2017年职称英语考试取消了吗
  5. SQL语句 分页实现
  6. 自定义注解和注解的相关使用
  7. CocoStudio基础教程(6)使用CocoStudio编辑帧事件并关联到程序
  8. Tushare使用分享
  9. 【读书笔记】《CSS新世界》—— 第一章 概述
  10. P2P软件UFX被指藏后门搜客户信息 融都科技否认
  11. 关于DevExpress Winform GridControl GridView 主从表(Master-Detail)导出Excel问题
  12. 2023年【安徽省安全员C证】免费试题及安徽省安全员C证证考试
  13. 多传感器融合定位十五-多传感器时空标定(综述)
  14. [Java]剑指offer51-55题_day11
  15. 05人月神话读书笔记之一
  16. TooManyCells:用于识别与可视化单细胞关系的方法
  17. web上传整个文件夹
  18. 点开/双击TIM没有反应
  19. Windows Server2016+SqlServer2016搭建AlwaysOn集群(一)
  20. IntentFilter功能简介

热门文章

  1. windows下映射网络驱动器
  2. 炫龙笔记本安装Ubantu系统
  3. 应届生的身份有什么好处
  4. [附源码]计算机毕业设计JAVA大学城二手书交易网站
  5. 手机JAVA编程技术
  6. 王者荣耀英雄简介-1
  7. error: expected declaration or statement at end of input----solved
  8. flutter 微信语言选择_Flutter 模仿微信读书效果!
  9. C++边边角角(一)
  10. 轨道交通行业网站(持续完善)