同事代码问题(第三篇)

首页 编程分享 PHP丨JAVA丨OTHER 正文

提前退休了 转载 编程分享 2024-11-16 22:13:54

简介 前言 最近我要对外提供一套接口,所以接口的调用频率肯定是要限制的。我们的项目也通过lua脚本实现了一套限流的插件。我这次简单的看了一下实现的逻辑,wtfk这是啥呀! 下面就分析一下我的同事是怎么实现的


前言

没事儿闲得慌,看看同事的代码吧。我们的项目通过lua脚本实现了一套限流的插件。我这次简单的看了一下实现的逻辑,wtfk这是啥呀! 好几个bug,而且这个代码已经在好几个项目中用了,都是cv过来cv过去。。。。。

下面就分析一下我的同事是怎么实现的,存在哪些问题。最后对限流做个小小的总结

问题代码

问题代码实现限流的方式是通过利用AOP对接标记注解的方法进行拦截,通过lua脚本操作redis来保存接口的执行次数。

注解类
仔细看注释,看注释就是令牌桶限流

public @interface RateLimiter {

    //往令牌桶放入令牌的速率
    double value() default  Double.MAX_VALUE;
    //获取令牌的超时时间
    double limit() default  Double.MAX_VALUE;
}

AOP核心实现
key值取得是当前时间秒数

RateLimiter rateLimiter = signature.getMethod().getDeclaredAnnotation(RateLimiter.class);
if(rateLimiter == null){
    //正常执行方法,执行正常业务逻辑
    return proceedingJoinPoint.proceed();
}
//获取注解上的参数,获取配置的速率
double value = rateLimiter.value();
double time = rateLimiter.limit();
//list设置lua的keys[1]
//取当前时间戳到单位秒
String key = "ip:"+ System.currentTimeMillis() / 1000;
List<String> keyList = CollUtil.newArrayList(key);
//调用脚本并执行
List result = stringRedisTemplate.execute(redisScript, keyList, String.valueOf(value),String.valueOf(time));
//lua 脚本返回 "0" 表示超出流量大小,返回1表示没有超出流量大小
if(StringUtils.equals(result.get(0).toString(),"0")){
    //服务降级
    fullback();
    return null;
}

lua脚本
看看过期时间

local key = KEYS[1] --限流KEY 
local limit = tonumber(ARGV[1]) --限流大小 
local current = tonumber(redis.call('get', key) or "0") if current + 1 > limit then 
return 0 else redis.call("INCRBY", key,"1") redis.call("expire", key,"2") return current + 1 end

看到这儿你发现了几个bug呢?????

问题分析

  1. 从注解中的第一个参数value:往令牌桶放入令牌的速率,限流实现的算法感觉是令牌桶算法,注解的参数也没看看到有设置桶容量的参数。(看来不是一个正确的漏桶算法)
  2. AOP代码中用当前时间的秒数作为存放请求次数的redis key,时间做为key,造成了不同接口的限流数据在同一秒内共享了啊。同一秒中,其他标记限流的接口进来,又是接着上一次限流次数进行计算。
  3. lua脚本,传了两个值,只用了一个限制次数,另一个时间参数没有使用。看了lua脚本呢才知道它实现的限流方式是固定时间窗口限流。

看完三个代码块,才明白它实现的是固定时间窗口的限流算法。只是注解的注释不对,被误导了呀!!!!

调整之后

  1. 修改注释,数据类型调整成int吧。直接传小数到lua脚中有数据类型问题
public @interface RateLimiter {

    //limit 时间内 最大请求次数
    int value() default  1000;
    //限制时间 秒
    int limit() default  1;
    
}
  1. AOP中可以的可以的生成规则改为方法名称,这样各个接口的限流数据就分开了。同时获取注解参数的代码改为int接收
String key = REDIS_PRE+"limitMethod:"+ proceedingJoinPoint.getSignature().getName();
//time 秒内累计请求次数
int value = redisRateLimiter.value();
//key过期时间
int time = redisRateLimiter.limit();

3.lua脚本获取时间参数 redis.call("expire", key,ARGV[1])

总结

在实际开发中遇到要限流的需求,我们应该首先考虑了解限流算法实现方式有哪些,业务场景适合哪种算法,最后是考虑用什么组件方便。

常见的限流算法有以下几种:

  1. 令牌桶算法 (Token Bucket)
  • 原理:令牌桶算法使用一个桶来存放令牌,令牌以固定的速率生成。当请求到来时,必须从桶中获取一个令牌才能被处理。如果桶空了,请求会被拒绝或等待。
  • 特点:可以处理突发流量,适合需要平滑流量的场景。
  1. 漏桶算法 (Leaky Bucket)
  • 原理:漏桶算法将请求放入一个固定容量的桶中,桶以恒定的速率“漏水”。即使请求以高峰值到达,处理也会平滑到漏水的速率。
  • 特点:控制输出速率,适合对流量有严格限制的场景。
  1. 计数器算法 (Counter)/固定窗口
  • 原理:在一定时间窗口内,记录请求的数量。一旦请求数量超过设定的阈值,后续请求会被拒绝。
  • 特点:简单易用,适合请求速率相对稳定的场景。
  1. 滑动窗口算法 (Sliding Window)
  • 原理:在一定时间窗口内维护一个请求计数器,允许在窗口内的请求数量超过阈值,但在时间窗口的不同部分可以有不同的请求数。
  • 特点:相比计数器算法,能更灵活地处理请求,适合对请求速率有动态要求的场景。
  1. 基于时间的限流
  • 原理:根据时间戳来限制请求。例如,每秒只允许处理一定数量的请求。
  • 特点:适合需要严格限制请求频率的场景。

在 Java 中,常用的限流组件主要有以下几种,它们各自实现限流的原理和机制略有不同:

常见的限流中间件

  1. Guava RateLimiter
  • 原理:基于令牌桶算法实现。RateLimiter 允许你设定一个固定的速率(如每秒生成多少个令牌),并在请求到达时判断是否可以获取到令牌。如果获取到令牌,允许请求通过;否则请求会被阻塞或延迟。
  • 特点:简单易用,适合在应用程序中直接集成。
  1. Resilience4j RateLimiter
  • 原理:同样基于令牌桶算法,支持更复杂的配置和状态管理。它不仅可以限制请求速率,还可以监控和管理请求失败的情况。
  • 特点:具有分布式环境下的灵活性和可靠性,适合微服务架构。
  1. Spring Cloud Gateway RateLimiter
  • 原理:使用 Redis 或其他存储系统来实现限流,支持基于请求的限流策略。它可以根据请求的 IP 地址、请求路径等信息进行动态限流。
  • 特点:适合微服务场景,能够根据不同条件灵活配置限流规则。
  1. Hystrix (虽然已不再维护)
  • 原理:Hystrix 的限流是通过熔断器模式实现的,尽管它主要用于服务的容错和隔离,但也可以限制请求的并发数量。
  • 特点:适合在微服务架构中使用,能够提高系统的稳定性。
  1. Bucket4j
  • 原理:基于漏桶算法实现,支持在内存和分布式环境中使用。可以配置桶的容量、漏水速率等参数。
  • 特点:灵活性高,适合复杂的限流需求。
  1. Redis 限流
  • 原理:利用 Redis 的原子自增和过期特性实现限流。通过设置一个计数器,记录在特定时间窗口内的请求数量,超过阈值则拒绝请求。
  • 特点:适合分布式系统,可以跨多个实例共享限流状态。

转载链接:https://juejin.cn/post/7436399136763576346


Tags:


本篇评论 —— 揽流光,涤眉霜,清露烈酒一口话苍茫。


    声明:参照站内规则,不文明言论将会删除,谢谢合作。


      最新评论




ABOUT ME

Blogger:袅袅牧童 | Arkin

Ido:PHP攻城狮

WeChat:nnmutong

Email:nnmutong@icloud.com

标签云