200行代码实现接口限流,探究开源项目eladmin接口限流原理

eladmin的接口限流是如何实现的呢?

今天闲来无事的时候,看了一下eladmin这个开源项目。发现了其中真的有非常多我们可以借鉴的地方。今天我们就来看看eladmin中接口限流这个小功能是如何实现。可能会对你之后的项目有所帮助。
这里预先给大家说一下他的实现用到了如下技术:

  • 自定义注解
  • Spring提供的Aop
  • redis+lua脚本

1、下载eladmin后端项目

这里我先把链接贴出来eladmin后端项目链接,然后我们进入这个地址,按照图中的方法,复制其中的git链接
在这里插入图片描述
接下来,我们随便找个文件夹,右键空白处,打开git bash输入如下命令

git clone 后面这里是你复制git链接

这样我们就把项目拉下来了,如下图所示:
在这里插入图片描述

2、接下来我们用idea打开项目,分析注解

当我们打开项目后,先找到Limitcontroller这个类。
在这里插入图片描述
我们会发现这个类是我做我们接口限流测试一个controller。我们会发现在test这个方法上面有这样一个注解@Limit,这个就是我们限流的主要注解,然后我们点进去这个注解,看看它定义了那些属性:

/**
 * @author jacky
 */
// 元注解,定义了这个注解只能用在方法上面
@Target(ElementType.METHOD)
//元注解,定义了只在运行时生效
@Retention(RetentionPolicy.RUNTIME)
public @interface Limit {

    // 资源名称,用于描述接口功能
    String name() default "";

    // 资源 key
    String key() default "";

    // key prefix 这个key的前缀,用于拼接key的
    String prefix() default "";

    // 时间的,单位秒
    int period();

    // 限制访问次数
    int count();

    // 限制类型(一种是根据方法名称进行限制,还有一种是根据ip进行限制)
    LimitType limitType() default LimitType.CUSTOMER;

}

上面就是我对这个注解各个属性的理解。当然定义了一个注解之后,我们肯定要去解析这个注解,不然的话,这个注解就起不了作用。

3、AOP切入解析注解

既然定义了这个注解,那么就要我们去解析他。这个注解的解析是写在LimitAspect这个类里面的

在这里插入图片描述
接下来我们来解读一下这个类

/**
 * @author /
 */
@Aspect
@Component
public class LimitAspect {

    private final RedisTemplate<Object,Object> redisTemplate;
    private static final Logger logger = LoggerFactory.getLogger(LimitAspect.class);

    public LimitAspect(RedisTemplate<Object,Object> redisTemplate) {
        this.redisTemplate = redisTemplate;
    }

    @Pointcut("@annotation(me.zhengjie.annotation.Limit)")
    public void pointcut() {
    }

    @Around("pointcut()")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
    	//拿到http请求的request,目的是为了获取ip
        HttpServletRequest request = RequestHolder.getHttpServletRequest();
        //拿到我们切入的方法,从而拿到注解,并获取注解中的属性
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method signatureMethod = signature.getMethod();
        Limit limit = signatureMethod.getAnnotation(Limit.class);
        //获取注解中的limitType,目的是判断,通过方法限流,还是通过ip限流
        LimitType limitType = limit.limitType();
        //获取key,也就是往redis里面存入的key
        String key = limit.key();
        if (StringUtils.isEmpty(key)) {
            if (limitType == LimitType.IP) {
                key = StringUtils.getIp(request);
            } else {
                key = signatureMethod.getName();
            }
        }
		//这是一个不可变的list,它是线程安全的;
        //它是高效的;
        //因为它是不可变的,就可以像 String 一样传递给第三方类库,不会发生任何安全问题。
        ImmutableList<Object> keys = ImmutableList.of(StringUtils.join(limit.prefix(), "_", key, "_", request.getRequestURI().replaceAll("/","_")));

		//这里构建lua脚本(使用lua脚本的目的是为了保证对redis一系列操作的原子性)
        String luaScript = buildLuaScript();
        RedisScript<Number> redisScript = new DefaultRedisScript<>(luaScript, Number.class);
        //执行lua脚本 , keys为所有key的集合 , limit.count()为第一个value 
        //limit.period为第二个value
        Number count = redisTemplate.execute(redisScript, keys, limit.count(), limit.period());
        if (null != count && count.intValue() <= limit.count()) {
            logger.info("第{}次访问key为 {},描述为 [{}] 的接口", count, keys, limit.name());
            //如果已访问的次数小于等于限制次数,则执行我们接口中的方法
            return joinPoint.proceed();
        } else {
        //否则抛出异常
            throw new BadRequestException("访问次数受限制");
        }
    }

    /**
     * 限流脚本
     */
    private String buildLuaScript() {
        //设置变量c
        return "local c" +
                //将redis中的key【1】读取出来,赋值给c  ,这个时候的c可以理解为【该接口已经访问的次数】
                "\nc = redis.call('get',KEYS[1])" +
                //如果c大于你输入的value (也就是访问到达限制次数了), 则直接返回c,结束
                "\nif c and tonumber(c) > tonumber(ARGV[1]) then" +
                "\nreturn c;" +
                "\nend" +
                //否则,将key【1】的值自增1(注意:当redis中没有key,但是我们执行incr key 命令,会将key创建出来,并且赋值为1)
                "\nc = redis.call('incr',KEYS[1])" +
                //如果c等于1,也就是第一次进入该接口
                "\nif tonumber(c) == 1 then" +
                //给key【1】,设置超时时间,时间为你输入的第二个value
                "\nredis.call('expire',KEYS[1],ARGV[2])" +
                "\nend" +
                "\nreturn c;";
    }
}

这里首先使用aop去切我们的@limit这个注解。当我们访问这个接口是,最先进入的不是我们的接口方法,而是我们这个切面的around方法。进入around方法之后又解析注解里面的属性,通过编写lua脚本,来对redis进行key和value的操作。不仅保证了操作的原子性,而且减少网络开销:多个请求通过脚本一次发送,减少网络延迟。

  • 6
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值