使用 redis 实现滑动窗口,我们会基于这个场景,建立一个 Starter,在这之前,我们需要先。理解这个场景。
关键字:滑动窗口、流式计算、lua脚本、redis、zset、starter
概要:本文封装 redis 的API,实现简易滑动窗口,分别从业务背景、窗口理解、redis 的 zset 结构,lua 脚本,注意事项,不足等进行讲解
一、业务背景
规则预警,在特定时间触发规则达到 n 次后发出告警信息,例如:5 分钟之内失败 2 次,当满足条件后会发一条通知告警;数值可以根据实际情况动态配置。
下图是动态展示滑动窗口的示意图,按照黄色线固定窗口进行移动,窗口内会出现各种数值点,对窗口数字进行统计:
借助 redis 的 zset 有序集合能力,其中 score 字段要求有序,因此使用时间戳做 score,这样既保证顺序也能根据时间窗口计算窗口内的个数,通过计算时间窗口内的个数再与业务做判断;另外为了保障原子能力,使用lua脚本
二、redis版功能实现
通过 Lua 脚本实现 CAS(check-and-set)命令。
关于窗口在业务上的诉求,我分了三种场景,分别如下所示:
2.1 场景一、统计时间窗口内是否达到预定阈值,返回true和false, 并且达到阈值后清除
描述:1. 添加计数,2.将时间窗口外的数据移除;3.统计当前窗口的个数;4.判断是否超过阈值,5.超过清理并返回,否则返回false
lua
redis.pcall('zadd', KEYS[1], ARGV[1], ARGV[1]);
redis.pcall('zremrangebyscore', KEYS[1], 0, ARGV[2]);
redis.pcall("expire", KEYS[1], ARGV[3]);
if tonumber(redis.pcall('zcard',KEYS[1])) >= tonumber(ARGV[4])
redis.pcall('zremrangebyscore', KEYS[1], 0, ARGV[1]);
then return true end;
return false;
注意:不要使用下面这种方式。 集群方式下,不支持local变量,另外尽量少用变量,减少lua脚本占用过多内存
lua
local key = KEYS[1];
local current_time = ARGV[1];
local pre_time = ARGV[2];
local expire_second = ARGV[3];
local threshold = ARGV[4];
redis.pcall("zadd", key, current_time, current_time);
redis.pcall("zremrangebyscore", key, 0, pre_time);;
local count = redis.pcall("zcard",key);
redis.pcall("expire", key, expire_second);
if tonumber(count) >= tonumber(threshold) then
redis.pcall("zremrangebyscore", key, 0, current_time);
return true end;
return false;
2.2 场景二、统计时间窗口内是否达到预定阈值,返回true和false, 满足true的时候不做清理
lua
redis.pcall('zadd', KEYS[1], ARGV[1], ARGV[1]);
redis.pcall('zremrangebyscore', KEYS[1], 0, ARGV[2]);
redis.pcall("expire", KEYS[1], ARGV[3]);
if tonumber(redis.pcall('zcard',KEYS[1])) >= tonumber(ARGV[4])
then return true end;
return false;
2.3 场景三、统计时间窗口内的个数
只统计个数,不做其他的
lua
redis.pcall('zadd', KEYS[1], ARGV[1], ARGV[1]);
redis.pcall('zremrangebyscore', KEYS[1], 0, ARGV[2]);
redis.pcall("expire", KEYS[1], ARGV[3]);
return redis.pcall("zcard",KEYS[1]);
当然在实际落地的过程,会遇到一些其他问题,比如使用lua限制,分布式限制等
有了上面的三个场景后,接下来我们开始实战一个 Starter
三、Starter 实现
3.1 自定义一个 Starter 需要的流程(关键步骤)
- 选择一个合理的业务场景。比如我选择了 滑动窗口这个场景。
- 创建新的Maven项目,并引入依赖,通常命名需要遵循Spring Boot的命名规范,通常是-spring-boot-starter
- 代码实现,以及其他类的引入
- 编写自动配置类。 xxxAutoConfiguration
- 编写 spring.factories 文件, 在src/main/resources/META-INF/spring.factories中注册自定义的自动配置类
- 打包并发布到仓库,并在其他项目测试
3.2 本 Starter 的项目工程结构
本文的源码地址:gitee.com/uzongn/uzon...
四、代码实现
4.1 lua 脚本
本文给出三个 lua 脚本,分别应对三个场景。
lua 是一种非常简单的脚本语言,如果想了解更多,可以在菜鸟教程中学习,非常轻量:Lua 教程 | 菜鸟教程
4.2 核心逻辑
com.uzong.sliding.window.calculate.CalculateCore
细节描述:
CalculateCore
的创建,需要交给 xxxAutoConfiguration 类。不可在CalculateCore
上添加 @Resource, @Service 等类。所有 Starter 类的创建都尽量交给 xxxAutoConfiguration,用来控制类的加载。 这是一种规范- 此处依赖 RedisTemplate,DefaultRedisScript、RedisSerializer 等。用例处理接口调用、序列化等。
- 调用的是 redisTemplate.execute方法,参数中包含了执行脚本、序列化、业务参数、过期时间等等。最核心也是最基础的接口。用于执行Redis脚本
4.3 对外的 service api
用于上层可以直接使用的 api,目前只提供了3个。对于 api 尽量包含详细的说明。以及注意实现
其实现类,则主要依赖 CalculateCore 类,就不过多介绍
4.4 配置类 SlidingWindowAutoConfiguration
注意细节:
- 通过 ConditionalOnClass,否则无法被创建
- 通过构造方式在此自动装配类中创建 SlidingWindowServiceImpl 等对象。方便管理所有相关的 bean 对象
4.5 spring.factories
前缀都是 org.springframework.boot.autoconfigure.EnableAutoConfiguration
4.6 打包并发布到仓库,并在其他项目测试
先发布到本地 Maven 仓库,测试没问题,再发布到公司的私服。需要注意版本管理。
到这里,Starter 实战结束了。更多细节可以参考本项目。
执行测试
https
http://localhost:8080/api/sl/calculateCount?bizCodeKey=001&windowSeconds=10
http://localhost:8080/api/sl/clearOnCondition?bizCodeKey=001&windowSeconds=3&threshold=5
http://localhost:8080/api/sl/keepCalculate?bizCodeKey=001&windowSeconds=3&threshold=5