限流器

作者: 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
    

    相关文章

      网友评论

          本文标题:限流器

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