redis-aux是一款基于redisTemplate开发的工具包,目前有两个功能,布隆过滤器模块和限流模块
添加源和依赖
<repositories> <repository> <id>jitpack.io</id> <url>https://jitpack.io</url> </repository> </repositories> <dependency> <groupId>com.github.97lele</groupId> <artifactId>redis-aux</artifactId> <version>0.3.5</version> </dependency>
也可以下载源码包导入到本地的maven库。
本质为沿用guava的布隆过滤器,只是把操作字节的数组改为redis bitmap结构
在springboot程序的启动类上添加注解
@EnableBloomFilter(bloomFilterPath = "com.example.demo.entity")
其中可设置的属性有两个,一个为需要支持lambda表达式操作的实体路径,另一个为是否支持事务(其实就是为redisTemplate开启事务)
配置好要链接的redis,这里的配置可以参考redisTemplate的配置,采用lettuce作为连接客户端
spring: redis: port: 6379 host: 127.0.0.1
然后在需要使用布隆过滤器的类中注入
@Autowired private RedisBloomFilter bloomFilter;
AddCondition addCondition = AddCondition.create().keyName("test").keyPrefix("t").fpp(0.001).exceptionInsert(1000L).enableGrow(Boolean.TRUE) bloomFilter.add(addCondition,"test");
其中keyName是一定需要的,keyPrefix为key的前缀
exceptedInsertions,fpp分别为预计插入的个数,允许的错误率,有默认的配置,为1000个,0.03
enableGrow,growRate是扩容的参数,分别为是否开启扩容(默认不开启),当bitmap的字节位为1占算出来bitmap的字节数的多少倍时,进行扩容,默认为0.7,这里如果开启的话,需要错误率设置尽量低,不然会出现较为严重的误判
timeout,timeUnit为键过期的一些参数,默认不过期
public @interface BloomFilterPrefix { String prefix() default ""; } public @interface BloomFilterProperty { double fpp() default 0.03D; long exceptionInsert() default 1000L; String key() default ""; long timeout() default -1L; TimeUnit timeUnit() default TimeUnit.SECONDS; boolean enableGrow() default false; double growRate() default 0.7D; }
这里的key,keyPrefix如果不设置会对应属性名,类名,然后其他参数都有一个默认的配置
实体类的配置如下:
@BloomFilterPrefix public class TestEntity { @BloomFilterProperty(enableGrow = true,exceptionInsert = 5,timeout = 30) private String name; public String getName() { return name; } public void setName(String name) { this.name = name; } }
使用方法
bloomFilter.add(TestEntity::getName,12);
//手动构造 BaseCondition search = BaseCondition.create().keyName("test").keyPrefix("t"); bloomFilter.mightContain(search, "he") //通过Addcondition转换 BaseCondition baseCondition = addCondition.toBaseCondition();
bloomFilter.mightContain(TestEntity::getName, 15)
RedisBloomFilter还支持上述的批量操作,并且对键有重置、过期、删除(不是删除元素)的操作
通过执行lua脚本保证限流算法执行(仅仅是限流算法执行流程)的原子性,分为三种限流,滑动窗口、漏斗限流、水桶限流,主要是在切面上对执行的方法进行拦截并视情况是否执行对应的自定义fallback方法
在启动类上添加注解,delayConnect为false的话,会在启动时先执行一个ttl操作(默认不执行这个操作),enableGroup为开启支持限流组功能,默认不开启
@EnableLimiter(delayConnect = false,enableGroup = true)
在需要限流的方法 上添加限流注解,注解有三个,他们有公共的属性,fallback和passArgs,分别为限流不通过的方法以及是否传入原方法的参数。
其中during和duringUnit限定了时间范围,passCount则是该时间范围内可以通过的请求数量
public @interface WindowLimiter { int during() default 1; TimeUnit duringUnit() default TimeUnit.SECONDS; long passCount(); String fallback() default ""; boolean passArgs() default false; }
funnelRate和funnelRate 限定了漏斗挪出容量的速率,每秒可以挪出3个容量单位,那配置就是funnelRate=3,funnelRateUnit=TimeUnit.SECONDS,requestNeed是每次请求向漏斗里面加入的单位容量,默认请求消耗一个单位容量,capacity就是漏斗的容量
public @interface FunnelLimiter { double capacity(); double funnelRate(); TimeUnit funnelRateUnit() default TimeUnit.SECONDS; double requestNeed() default 1.0D; String fallback() default ""; boolean passArgs() default false; }
tokenRate和tokenRateUnit代表每单位时间内生产出多少个令牌,intiToken代表令牌桶中初始有多少个令牌,requestNeed代表单次请求消耗的令牌数,capacity为令牌桶的容量
public @interface TokenLimiter { double capacity(); double tokenRate(); TimeUnit tokenRateUnit() default TimeUnit.SECONDS; double requestNeed() default 1.0D; double initToken() default 0.0D; String fallback() default ""; boolean passArgs() default false; }
只需在方法上添加注解并填写参数即可。
//默认为秒,该配置为每秒生产0.5个令牌 @GetMapping("ha3") @TokenLimiter(capacity = 5,tokenRate = 0.5,requestNeed = 1) public String test3() { return "hihi3"; }
限流组为了支持动态配置而写的,提供的功能相对多一点
ip黑白名单配置,其中白名单不需要经过限流,黑名单直接跳到执行失败方法的fallBack,支持网段xx.xx.xx.xx/xx,192.*,192.168.21.1-192.168.32.21这三种表达方式,规则以分号分隔
需要调用限流组service来配置规则,并添加相应的拦截器,拦截器按优先级从大到小执行,需要配置拦截器上面的拦截功能才会执行,下面的例子几乎把所有配置列了出来
@Configuration public class RateLimitConfig implements InitializingBean { @Autowired private LimiterGroupService limiterGroupService; @Override public void afterPropertiesSet() { //清除原来的配置 // limiterGroupService.clear("1"); //新建 LimiteGroupConfig config = LimiteGroupConfig.of().id("1") .remark("this.application").tokenConfig( //令牌桶配置,下面表示令牌桶容量为5,初始桶为3,每1s生产3个令牌,每个请求消耗1个令牌 TokenRateConfig.of() .capacity(5.0) .initToken(3.0) .requestNeed(1.0) .tokenRate(3.0) .tokenRateUnit(TimeUnit.SECONDS) .build() ). windowConfig( //滑动窗口配置,下面表示10s内只允许5个通过 WindowRateConfig.of() .passCount(5L) .during(10L) .duringUnit(TimeUnit.SECONDS) .build()).currentMode(LimiterConstants.TOKEN_LIMITER) //漏斗配置,容纳量为10,每次请求容纳量-1,每秒增加3个容纳量 .funnelConfig(FunnelRateConfig.of() .capacity(10.0) .funnelRate(3.0) .funnelRateUnit(TimeUnit.SECONDS) .requestNeed(1.0) .build()) //黑白名单,网段 xxx.xxx.xxx./24,类似 192.168.0.0-192.168.2.1 以及 192* 分号分隔 /*.blackRule("127.0.0.1") .enableBlackList(true) .enableWhiteList(true). whiteRule("192.168.0.*") */ .blackRuleFallback("ip") //当前限流模式 .currentMode(LimiterConstants.TOKEN_LIMITER) //开启统计,是统计复用该配置下的请求数 .enableCount(true) //统计时间范围,如果没有则从第一次请求开始统计 .countDuring(1L).countDuringUnit(TimeUnit.MINUTES) //url配置,;号分割 .unableURLPrefix("/user;/qq") .enableURLPrefix("/test") //url匹配失败后的执行方法 .urlFallBack("userBlack") .build(); //保存到redis,也可以保存到本地,但这时候 limiterGroupService.save(config, true, false); //读取redis上的配置 // limiterGroupService.reload("1"); //添加对应的拦截器,不然切面中不会执行对应的逻辑,这里也可以实现自己的拦截器并添加上去 //并且可以添加对应的执行顺序,默认从大到小执行 limiterGroupService.addHandler(GroupHandlerFactory.limiteHandler().order(-5) .addHandler(GroupHandlerFactory.ipBlackHandler()) .addHandler(GroupHandlerFactory.urlPrefixHandler()); ; } }
@LimiteGroup可以作用在类或者方法上面,作用在类的话,被切面切到的方法都会被走那一套拦截链的逻辑,可以用@LimiteExclude注解取消拦截方法,如果没有fallBack方法,会抛出一个内部的运行时异常
@RestController @LimiteGroup(groupId = "1", fallback = "test") public class TestController { @GetMapping("/ok") public String ok() { return "ok"; } @GetMapping("/user") public String user() { return "user"; } @GetMapping("/user/t") @LimiteExclude public String usert() { return "usert"; } public String userBlack() { return "非法前缀访问"; } public String ip() { return "ip错误"; } public String test() { return "too much request"; } }
动态配置是通过访问内部的一些接口实现配置的改变,具体的方法有
white为true的话就是改变白名单,enable就是是否启用该名单的规则,rule表示规则的表示,grouopId表示对应的限流组
//更改ip规则 @PostMapping("/redis-aux/changeIpRule") public LimiteGroupConfig changeRule(@RequestParam("groupId") String groupId, @RequestParam(value = "rule", required = false) String rule, @RequestParam(value = "enable", required = false) Boolean enable, @RequestParam(value = "white", required = false) Boolean white)
enableUrl为无须限流的前缀,unableUrl为无法访问的前缀
//更改url匹配规则 @PostMapping("/redis-aux/changeUrlRule") public LimiteGroupConfig changeUrlRule(@RequestParam("groupId") String groupId, @RequestParam("enableUrl") String enableUrl, @RequestParam("unableUrl") String unableUrl )
mode=1为窗口限流,mode=2为令牌同模式,mode=3为漏斗模式
//更改当前限流模式 @PostMapping("/redis-aux/changeLimitMode") public LimiteGroupConfig changeMode(@RequestParam("groupId") String groupId, @RequestParam("mode") Integer mode, @RequestParam("removeOther") Boolean removeOther)
@PostMapping(“/redis-aux/changeWindowConfig”) public LimiteGroupConfig changeWindowConfig(@RequestParam(“groupId”) String groupId, @RequestParam(“passCount”) Long passCount, @RequestParam(value = “during”, required = false) Long during, @RequestParam(value = “duringUnit”, required = false) Integer mode )
@PostMapping(“/redis-aux/changeTokenConfig”) public LimiteGroupConfig changeWindowConfig(@RequestParam(“groupId”) String groupId, @RequestParam(“capacity”) Double capacity, @RequestParam(value = “initToken”, required = false) Double initToken, @RequestParam(“tokenRate”) Double tokenRate, @RequestParam(value = “requestNeed”, required = false) Double requestNeed, @RequestParam(value = “duringUnit”, required = false) Integer mode )
//更改限流规则 @PostMapping(“/redis-aux/changeFunnelConfig”) public LimiteGroupConfig changeFunnelConfig(@RequestParam(“groupId”) String groupId, @RequestParam(value = “requestNeed”, required = false) Double requestNeed, @RequestParam(“capacity”) Double capacity, @RequestParam(“funnelRate”) Double funnelRate, @RequestParam(value = “funnelRateUnit”, required = false) Integer funnelRateUnit )
统计数量
这里的统计数量功能默认单个统计是1天,也就是统计从第一请求后的24h内的qps,采用统计的数据结构为hyperloglog,统计失败和成功,有两个键,计数可能和实际不太精准,1天之后该键就会消失,下次请求再重新生成,这里设计不是很好,还望各位大佬提供一些实际生产上监控某个接口qps的思路
@GetMapping("/redis-aux/getCount/{groupId}") public Map<String, String> changeCountConfig(@PathVariable("groupId") String groupId )