原子性与齿轮

使用RedisGears改进原子性和性能#

什么是齿轮?#

再装备是一个动态的服务器端数据处理引擎,其中“服务器”部分是Redis本身。RedisGears作为Redis模块分发。您可以使用Docker官方图像启动预先配置Gears的Redis实例:

docker run-p 6379:6379 redislabs/redisgears:最新版本

或者像我经常做的那样,用“redismod”包括Gears和所有其他Redis, Inc.支持的模块:

Docker运行-p 6379:6379 redislabs/redismod

RedisGears是为了在Redis内部提供一个数据处理引擎而构建的,它具有比简单的Lua服务器端脚本更正式的语义。可以把它看作是Redis的一个更灵活的map-reduce引擎。它支持支持事务、批处理和事件驱动的Redis数据处理。Gears允许您本地化计算和数据,并提供内置的协调器,以促进在集群环境中处理分布式数据。

在RedisGears中,主要的处理单元是RedisGears函数,它(目前)可以用Python编写(正在开发更多的语言)。这些函数在它们自己的线程上运行,与Redis的主线程分开,可以在响应键空间事件或外部命令时执行。这些函数被“注册”(部署)到Gears引擎,并且有一个关联的名称和一个注册Id。

在注册过程中,我们为函数选择一个特定的读取器,它定义了函数如何获取初始数据:

  • 键盘读取器:Redis键和值。
  • 键控读取器:复述,钥匙。
  • 流阅读器: Redis Stream消息。
  • PythonReader:任意的Python生成器。
  • 碎片阅读器:碎片ID。
  • 命令阅读器:来自应用程序客户端的命令参数。

限速RedisGears功能#

根据读取器的类型,Gear Functions可以作为批处理作业立即运行,也可以通过注册它来自动触发各种类型的事件,以事件驱动的方式运行。

Python函数利率上限接受3个参数:

  • 关键:支持给定用户计数器的Redis键。
  • 最大请求:用户的请求配额。
  • 到期:未来设置计数器TTL的秒数。
def 利率上限 ( 关键 , 最大请求数 , 到期 ) :
请求 = 执行 ( “得到” , 关键 )
请求 = int ( 请求 ) 如果 请求 其他的 - 1.
最大请求数 = int ( 最大请求数 )
到期 = int ( 到期 )
如果 ( 请求 == - 1. ) ( 请求 < 最大请求数 ) :
具有 原子 ( ) :
执行 ( “增量” , 关键 )
执行 ( “到期” , 关键 , 到期 )
回来 错误的
其他的 :
回来 符合事实的
#函数注册
国标 = 国标 ( “CommandReader” )
国标 地图 ( λ x : 利率上限 ( x [ 1. ] , x [ 2. ] , x [ 3. ] ) )
国标 登记 ( 触发 = “RateLimiter” )

将脚本置于src/main/resources/scripts.现在,让我们将其细分:

这个利率上限作用#

与我们在上一次实施中所做的类似,我们:

  1. 检索已传递的请求的当前数量关键通过执行-对得到命令。
  2. 将结果转换为int如果没有找到,默认为-1
  3. 最大请求数到期int
  4. 如果没有超过配额,则执行增量/到期事务中的命令(与原子():)返回错误的(无速率限制-允许请求)
  5. 否则,请返回符合事实的(拒绝请求)

函数注册#

  1. 在脚本的底部,在#函数注册节中,实例化GearsBuilder(国标)使用命令阅读器读者。这个GearsBuilder在参数、转换、触发器等中“构建”函数的上下文。
  2. 我们使用地图方法执行记录到的参数的一对一映射利率上限函数通过映射器函数回调。
  3. 现在我们可以调用登记操作将函数注册为事件处理程序。在我们的例子中,事件是触发器“RateLimiter”

SpringBoot中的齿轮#

为了在我们的SpringBoot应用程序中使用我们的RedisGear函数,我们需要做一些事情:

  1. 将该功能部署到Redis服务器
  2. 执行这个函数,对每个请求都得到一个是/否的答复

生菜国防部#

LettuceMod是一个基于生菜的Redis模块的Java客户端吗朱利安·鲁奥.它支持以下模块在独立或集群配置:

  • 再装备
  • Redisson
  • 再研究
  • 再贴现时间序列

要使用LettuceMod,我们将依赖项添加到Maven POM中,如下所示:

< 附属国 >
< groupId > com.redis groupId >
< 人工的 > 春莴苣 人工的 >
< 版本 > 1.7.0 版本 >
附属国 >

在SpringBoot中访问Gears命令#

要访问任何letucemod支持的模块,我们将注入一个StatefulRedisModulesConnection在我们FixedWindowRateLimiterApplication分类如下:

@ autowired
StatefulRedisModulesConnection < 一串 , 一串 > 连接 ;

添加匹配的导入语句:

进口 通用域名格式 复述, lettucemod api StatefulRedisModulesConnection ;

登记齿轮功能#

我们将首先编写一个函数来确定该函数是否带有触发器限速器已被注册。需要一个列表登记s并深入挖掘以提取触发参数使用Java Streams API:

私有的 可选择的 < 一串 > 获取装配工的齿轮注册信息 ( 列表 < 登记 > 注册 , 一串 触发 ) {
回来 注册 流动 ( ) 过滤器 ( R -> R getData ( ) getArgs ( ) 得到 ( “触发器” ) 等于 ( 触发 ) ) findFirst ( ) 地图 ( 登记 : : getId ) ;
}

@PostConstruct带注释的方法loadGearsScript方法:

  1. 的实例再分配指令从先前注入的StatefulRedisModulesConnection
  2. 函数获取当前注册的Gears函数dumpregistrations方法
  3. 我们把注册名单交给我们的获取装配工的齿轮注册信息
  4. 如果我们没有找到注册,我们继续注册函数:
    • 将函数从类路径加载到一串命名py
    • 使用pyexecute方法通过py脚本负载
@PostConstruct
公共 无效的 loadGearsScript ( ) 投掷 IOException {
一串 py = StreamUtils copyToString ( 刚出现的 类路径资源 ( “脚本/ rateLimiter.py” ) getInputStream ( ) ,
字符集 默认字符集 ( ) ) ;
再分配指令 < 一串 , 一串 > 齿轮 = 连接 同步 ( ) ;
列表 < 登记 > 注册 = 齿轮 dumpregistrations ( ) ;
可选择的 < 一串 > maybeRegistrationId = 获取装配工的齿轮注册信息 ( 注册 , “费率限制器” ) ;
如果 ( maybeRegistrationId 栈空 ( ) ) {
尝试 {
ExecutionResults = 齿轮 pyexecute ( py ) ;
如果 ( 伊索克 ( ) ) {
日志记录器 信息 ( “RateLimitor.py已注册” ) ;
} 其他的 如果 ( 伊瑟罗 ( ) ) {
日志记录器 错误 ( 一串 格式 ( “无法注册RateLimiter.py->%s” , 阵列 托斯特林 ( 获取错误 ( ) 托雷 ( ) ) ) ) ;
}
} ( RedisCommandExecutionException异常 rcee ) {
日志记录器 错误 ( 一串 格式 ( “无法注册RateLimiter.py->%s” , rcee 获取消息 ( ) ) ) ;
}
} 其他的 {
日志记录器 信息 ( “RateLimitor.py已注册” ) ;
}
}

修改过滤器以使用Gears功能#

接下来,我们将修改过滤器以包括StatefulRedisModulesConnection以及配额;我们需要传递给函数的值:

RateLimiterHandlerFilterFunction 实现了 HandlerFilterFunction < ServerResponse , ServerResponse > {
私有的 StatefulRedisModulesConnection < 一串 , 一串 > 连接 ;
私有的 maxRequestPerMinute ;
公共 RateLimiterHandlerFilterFunction ( StatefulRedisModulesConnection < 一串 , 一串 > 连接 ,
maxRequestPerMinute ) {
连接 = 连接 ;
maxRequestPerMinute = maxRequestPerMinute ;
}

现在我们可以修改过滤器方法来使用该函数。Gears函数通过触发正确的事件来调用限速器传递函数所需的参数;的关键,配额和未来的TTL秒数。

正如我们之前所做的,如果函数返回错误的我们让请求通过,否则我们将返回HTTP 429:

@凌驾
公共 莫诺 < ServerResponse > 过滤器 ( ServerRequest 请求 , HandlerFunction < ServerResponse > 下一个 ) {
int currentMinute = 本地时间 现在 ( ) 一分钟 ( ) ;
一串 关键 = 一串 格式 ( “rl_ % s: % s” , requestAddress ( 请求 remoteAddress ( ) ) , currentMinute ) ;
再分配指令 < 一串 , 一串 > 齿轮 = 连接 同步 ( ) ;
列表 < 对象 > 结果 = 齿轮 触发 ( “费率限制器” , 关键 , 托斯特林 ( maxRequestPerMinute ) , “59” ) ;
如果 ( ! 结果 栈空 ( ) && ! 布尔 parseBoolean ( ( 一串 ) 结果 得到 ( 0 ) ) ) {
回来 下一个 手柄 ( 请求 ) ;
} 其他的 {
回来 ServerResponse 地位 ( 请求太多 ) 构建 ( ) ;
}
}

卷曲测试#

我们再次使用curl loop测试限制器:

For n in {1..22};echo $(curl -s -w ":: HTTP %{http_code}, %{size_download} bytes, %{time_total} s" -X GET http://localhost:8080/api/ping);睡眠0.5;完成

您应该看到第21个请求被拒绝:

对于 N 在里面 { 1. .. 22 } ; 回声 $( 旋度 -西南 ":: HTTP %{http_code}, %{size_download} bytes, %{time_total} s" - x得到http://localhost: 8080 / api /平 ) ; 睡觉 0.5 ; 完成
乒乓球:HTTP: 200 , 4. 字节, 0.064786 s
乒乓球:HTTP: 200 , 4. 字节, 0.009926 s
乒乓球:HTTP: 200 , 4. 字节, 0.009546 s
乒乓球:HTTP: 200 , 4. 字节, 0.010189 s
乒乓球:HTTP: 200 , 4. 字节, 0.009399 s
乒乓球:HTTP: 200 , 4. 字节, 0.009210 s
乒乓球:HTTP: 200 , 4. 字节, 0.008333 s
乒乓球:HTTP: 200 , 4. 字节, 0.008009 s
乒乓球:HTTP: 200 , 4. 字节, 0.008919 s
乒乓球:HTTP: 200 , 4. 字节, 0.009271 s
乒乓球:HTTP: 200 , 4. 字节, 0.007515 s
乒乓球:HTTP: 200 , 4. 字节, 0.007057 s
乒乓球:HTTP: 200 , 4. 字节, 0.008373 s
乒乓球:HTTP: 200 , 4. 字节, 0.007573 s
乒乓球:HTTP: 200 , 4. 字节, 0.008209 s
乒乓球:HTTP: 200 , 4. 字节, 0.009080 s
乒乓球:HTTP: 200 , 4. 字节, 0.007595 s
乒乓球:HTTP: 200 , 4. 字节, 0.007955 s
乒乓球:HTTP: 200 , 4. 字节, 0.007693 s
乒乓球:HTTP: 200 , 4. 字节, 0.008743 s
::HTTP 429 , 0 字节, 0.007226 s
::HTTP 429 , 0 字节, 0.007388 s

如果我们在监控模式下运行Redis,我们应该看到Lua调用RG。触发在这下面,你应该看到呼叫得到,增量到期对于允许的请求:

1631249244.006212 [ 0 172.17 .0.1:56036 ] “RG.触发器” “费率限制器” “rl_localhost: 47” “20” “59”
1631249244.006995 [ 0 : 0 ] “得到” “rl_localhost: 47”
1631249244.007182 [ 0 : 0 ] “增加” “rl_localhost: 47”
1631249244.007269 [ 0 : 0 ] “到期” “rl_localhost: 47” “59”

对于价格限制的请求,你应该只看到电话得到:

1631249244.538478[0 172.17.0.1:56036]“RG.触发器”“速率限制器”“rl_localhost:47”“20”“59”
1631249244.538809[0?:0]“获取”“rl_localhost:47”

此实现的完整代码位于该分支之下带齿轮

Baidu