限流器

作者: guessguess | 来源:发表于2020-11-09 18:08 被阅读0次

如果是简单做一个限流器的话,利用zset也可以实现,以score作为时间戳,然后的话,value为任意(保证不重复即可),因为重点还是在于score,然后在访问请求的时候,通过zset的的zscore指令,判断出某个时间段内的成员数,就可以模拟,某个时间段,被请求了多少次,然后是否进行控制。

模拟一下隔壁老王敲门,假设score为时间戳,value的话,那么可以看出 3秒内,老王敲了3次门。

127.0.0.1:6379> ZCOUNT laowang:qiaomen 0 1
(integer) 1
127.0.0.1:6379> ZCOUNT laowang:qiaomen 1 3
(integer) 3
127.0.0.1:6379> del key laowang:qiaomen
(integer) 1
127.0.0.1:6379> ZADD laowang:qiaomen 1 "first"
(integer) 1
127.0.0.1:6379> ZADD laowang:qiaomen 2 "second"
(integer) 1
127.0.0.1:6379> ZADD laowang:qiaomen 3 "third"
(integer) 1
127.0.0.1:6379> ZCARD laowang:qiaomen
(integer) 3
127.0.0.1:6379> ZCOUNT laowang:qiaomen 1 3
(integer) 3
127.0.0.1:6379>

这样子就简单模拟了这个过程,但是存在许多问题,如浪费内存,如果不进行内存的浪费的话,就要频繁去修改这个zset,因为对我们来说,只有某个时间段的数据是有意义的。
那么剩下的一步呢?如何进行控制,当然是根据时间段统计出来的次数,多于我们限定的值,就返回异常信息,或者是拒绝请求。
如果我们用ZSET来实现的话,这里面就涉及到了,增改查,逻辑略复杂,操作涉及到的资源也比较多。

我们可以通过redis提供的CL.THROTTLE来实现。
这里首先是需要下载redis-cell的插件

先来简单使用一下这个指令
CL.THROTTLE key 容量 释放数 周期

//这个指令的意思是 容量为1 然后每次释放一个容量 周期为10秒
127.0.0.1:6379> CL.THROTTLE laowang-qiaomen 0 1 10
1) (integer) 0       //0为成功 1为失败
2) (integer) 1       //最大容量
3) (integer) 0      //剩余容量
4) (integer) -1     //多久之后有空间  第一次加进去为-1 就是整个周期
5) (integer) 10    //多久之后容器完全为空
//几秒钟之后再试
127.0.0.1:6379> CL.THROTTLE laowang-qiaomen 0 1 10
1) (integer) 1     //失败
2) (integer) 1     //最大容量还是1
3) (integer) 0     //剩余容量
4) (integer) 6     //6秒之后有空间
5) (integer) 6     //6秒之后容器完全为空,因为容量为1所以都是6秒
127.0.0.1:6379> CL.THROTTLE laowang-qiaomen 0 1 10
1) (integer) 1
2) (integer) 1
3) (integer) 0
4) (integer) 3
5) (integer) 3
127.0.0.1:6379> CL.THROTTLE laowang-qiaomen 0 1 10
1) (integer) 1
2) (integer) 1
3) (integer) 0
4) (integer) 1
5) (integer) 1
127.0.0.1:6379> CL.THROTTLE laowang-qiaomen 0 1 10
1) (integer) 0
2) (integer) 1
3) (integer) 0
4) (integer) -1
5) (integer) 10

下面贴代码
首先是限流器的内部实现

@Component
public class RequestFunnel {
    
    @Autowired
    private RedisTemplate<String, String> redisTemplate;
    
    public boolean ifCanRequest(String url, String userName, String containerNum, String releaseNum, String period) {
        String key = url + ":" + userName;
        String script = "return redis.call('CL.THROTTLE', KEYS[1], ARGV[1], ARGV[2], ARGV[3])";
        List<Long> rs = redisTemplate
                .execute((RedisCallback<List<Long>>) con -> con.eval(script.getBytes(), ReturnType.MULTI, Constants.ONE,
                        key.getBytes(), containerNum.getBytes(), releaseNum.getBytes(), period.getBytes()));
        if (Constants.ZERO == rs.get(Constants.ZERO).intValue()) {
            return true;
        }
        return false;
    }
}

然后结合切面,用于控制请求

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RequestFunnel {
    String url();
    int containerMaxNum();
    int releaseNum();
    int releasePeriod();
}

@Aspect
@Component
public class RequestFunnelAspect {
    
    @Autowired
    private RequestFunnelUtils requestFunnelUtils;
    
    @Pointcut("@annotation(com.huangzhaoji.redis.annotation.RequestFunnel)")
    public void pointCut() {
        
    };
    
    @Before(value = "pointCut()")
    public void before(JoinPoint joinPoint){
        String methodName = joinPoint.getSignature().getName();
        Object obj = joinPoint.getTarget();
        Method[] methods = obj.getClass().getMethods();
        Method method = null;
        for(Method methodItem : methods) {
            if(methodItem.getName().equals(methodName)) {
                method = methodItem;
                break;
            }
        }
        RequestFunnel requestFunnel = method.getAnnotation(RequestFunnel.class);
        String url = requestFunnel.url();
        Integer containerMaxNum = requestFunnel.containerMaxNum();
        Integer releaseNum = requestFunnel.releaseNum();
        Integer releasePeriod = requestFunnel.releasePeriod();
        //这个username其实跟id一样,可以通过不写死的方式获取的,可以通过threadLocal建立线程与请求的关系,从请求中获取到对应的userId或者名字,这样子就不用写死了
        boolean canVisit = requestFunnelUtils.ifCanRequest(url, "老王", containerMaxNum.toString(), releaseNum.toString(), releasePeriod.toString());
        if(!canVisit) {
            //throw new MyException("访问次数过多,请稍后重试");
            //如果是在实际业务场景中,这里肯定是抛一个异常,然后通过全局异常处理器捕获,最后返回自定义的异常响应信息
            System.out.println("访问次数过多,请稍后重试");
        }
    }
    
}

编写一个业务类

public interface UserService {
    public void visit();
}
@Component
public class UserServiceImpl implements UserService{
    
    
    @Override
    @RequestFunnel(url = "visit", containerMaxNum = 1, releaseNum = 1, releasePeriod = 10)
    public void visit() {
        System.out.println("visit");
    }

}

测试方法

public class RequestFunnelUtilsTest extends TestApplication{
    
    @Autowired
    private UserService userService;

    @Test
    public void testFunnel() {
        for (int i = 0; i < 10; i++) {
            userService.visit();
        }
    }
}

最后输出

visit
visit
访问次数过多,请稍后重试
visit
访问次数过多,请稍后重试
visit
访问次数过多,请稍后重试
visit
访问次数过多,请稍后重试
visit
访问次数过多,请稍后重试
visit
访问次数过多,请稍后重试
visit
访问次数过多,请稍后重试
visit
访问次数过多,请稍后重试
visit

相关文章

  • 限流降级方案

    限流算法 并发数限流 计数器并发数限流:使用共享变量实现 信号量:使用java中的Semaphore QPS限流 ...

  • Guava RateLimiter的实现

    限流 高并发系统有三大利器:缓存 、限流 、降级。对于限流的实现,有多种算法:计数器,漏桶法,令牌桶法。计数器法无...

  • RateLimiter源码解析

    计数器限流 最原始的代码 但是计数器限流无法对相邻两秒都是高qps进行限流,比如1:29:29.999有100qp...

  • 限流器

    如果是简单做一个限流器的话,利用zset也可以实现,以score作为时间戳,然后的话,value为任意(保证不重复...

  • 限流器

    坐长途大巴车到了终点站,出站的时候,出口处挤堆了好大一堆拉着箱子,背着大包小包的旅客。大概有五、六个大巴车...

  • (9)弹力设计篇之“限流设计”

    1、限流的策略 2、限流的算法:计数器、队列、漏斗和令牌桶。 3、如何基于响应时间来限流。 4、限流设计的要点 限...

  • redis+lua进行限流

    lua脚本,确保原子性 限流注解 限流处理器 redis配置 ip工具类

  • 分布式架构

    大流量限流/削峰 常见的限流算法 计数器算法池化资源技术的限流就是通过计数器算法来控制全局的总并发数。 令牌桶算法...

  • 单机限流 - 限流算法及隔离策略

    限流算法 - 计数器 计数器是一种比较简单的限流算法,用途比较广泛,在接口层面,很多地方使用这种方式限流。在一段时...

  • 限流算法简介及Guava RateLimiter令牌桶限流介绍

    参考 常用4种限流算法介绍及比较 超详细的Guava RateLimiter限流原理解析 限流算法简介 1.计数器...

网友评论

      本文标题:限流器

      本文链接:https://www.haomeiwen.com/subject/nehsvktx.html