第十节 SpringBoot Starter 实战之 redis 滑动窗口

使用 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 需要的流程(关键步骤)

  1. 选择一个合理的业务场景。比如我选择了 滑动窗口这个场景。
  2. 创建新的Maven项目,并引入依赖,通常命名需要遵循Spring Boot的命名规范,通常是-spring-boot-starter
  3. 代码实现,以及其他类的引入
  4. 编写自动配置类。 xxxAutoConfiguration
  5. 编写 spring.factories 文件, 在src/main/resources/META-INF/spring.factories中注册自定义的自动配置类
  6. 打包并发布到仓库,并在其他项目测试

3.2 本 Starter 的项目工程结构

本文的源码地址:gitee.com/uzongn/uzon...

四、代码实现

4.1 lua 脚本

本文给出三个 lua 脚本,分别应对三个场景。

lua 是一种非常简单的脚本语言,如果想了解更多,可以在菜鸟教程中学习,非常轻量:Lua 教程 | 菜鸟教程

4.2 核心逻辑

com.uzong.sliding.window.calculate.CalculateCore

细节描述:

  1. CalculateCore 的创建,需要交给 xxxAutoConfiguration 类。不可在 CalculateCore 上添加 @Resource, @Service 等类。所有 Starter 类的创建都尽量交给 xxxAutoConfiguration,用来控制类的加载。 这是一种规范
  2. 此处依赖 RedisTemplate,DefaultRedisScript、RedisSerializer 等。用例处理接口调用、序列化等。
  3. 调用的是 redisTemplate.execute方法,参数中包含了执行脚本、序列化、业务参数、过期时间等等。最核心也是最基础的接口。用于执行Redis脚本

4.3 对外的 service api

用于上层可以直接使用的 api,目前只提供了3个。对于 api 尽量包含详细的说明。以及注意实现

其实现类,则主要依赖 CalculateCore 类,就不过多介绍

4.4 配置类 SlidingWindowAutoConfiguration

注意细节:

  1. 通过 ConditionalOnClass,否则无法被创建
  2. 通过构造方式在此自动装配类中创建 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
相关推荐
许野平25 分钟前
Rust: 利用 chrono 库实现日期和字符串互相转换
开发语言·后端·rust·字符串·转换·日期·chrono
duration~40 分钟前
Maven随笔
java·maven
zmgst43 分钟前
canal1.1.7使用canal-adapter进行mysql同步数据
java·数据库·mysql
跃ZHD1 小时前
前后端分离,Jackson,Long精度丢失
java
blammmp1 小时前
Java:数据结构-枚举
java·开发语言·数据结构
暗黑起源喵2 小时前
设计模式-工厂设计模式
java·开发语言·设计模式
WaaTong2 小时前
Java反射
java·开发语言·反射
齐 飞2 小时前
MongoDB笔记01-概念与安装
前端·数据库·笔记·后端·mongodb
狂放不羁霸2 小时前
idea | 搭建 SpringBoot 项目之配置 Maven
spring boot·maven·intellij-idea
九圣残炎2 小时前
【从零开始的LeetCode-算法】1456. 定长子串中元音的最大数目
java·算法·leetcode