美文网首页
基于Redis+Lua+Annotation的分布式应用限流

基于Redis+Lua+Annotation的分布式应用限流

作者: YAOAORAN | 来源:发表于2019-08-13 18:28 被阅读0次

为什么要做?
网站免费接口被竞品恶意请求,导致数据量和并发暴增,一开始封禁了他的IP,但是后面他又换了IP来搞我们,没办法只能被迫"应战"。

为什么不在网关层做?
也有想过用Nginx,但是我们想要的是更加细粒度的控制和更加灵活的配置。

Redis+Lua+Annotation限流实例

引入相关依赖

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-aop</artifactId>
    </dependency>
    <dependency>
        <groupId>org.apache.commons</groupId>
        <artifactId>commons-lang3</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
    </dependency>
</dependencies>

Lua脚本

local key = "rate.limit:" .. 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  --请求数+1,并设置2秒过期
  redis.call("INCRBY", key,"1")
   redis.call("expire", key,"2")
   return current + 1
end

1、通过KEYS[1] 获取传入的key参数
2、通过ARGV[1]获取传入的limit参数
3、redis.call方法,从缓存中get和key相关的值,如果为nil那么就返回0
4、接着判断缓存中记录的数值是否会大于限制大小,如果超出表示该被限流,返回0
5、如果未超过,那么该key的缓存值+1,并设置过期时间为1秒钟以后,并返回缓存值+1

注解(支持SPEL表达式)

/**
 * 分布式限流注解
 * @since 1.0.0
 */
@Target(value = {ElementType.METHOD})
@Retention(value = RetentionPolicy.RUNTIME)
public @interface RateLimit {
    /**
     * 限流唯一标示 支持spel表达式
     *
     * @return
     */
    String key() default "";
    /**
     * 限流时间 单位:秒
     *
     * @return
     */
    long time();
    /**
     * 限流次数 单位:次
     *
     * @return
     */
    int count();
}

自动装配

public class RateLimitAutoConfiguration {

    /**
     * 初始化限流脚本
     *
     * @return
     */
    @Bean
    public DefaultRedisScript<Number> redisluaScript() {
        DefaultRedisScript<Number> redisScript = new DefaultRedisScript<>();
        redisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("/redisScript/rateLimit.lua")));
        redisScript.setResultType(Number.class);
        return redisScript;
    }
}

拦截器

@Aspect
@Configuration
@Slf4j
public class LimitAspect {

    @Autowired
    private DefaultRedisScript<Number> redisluaScript;

    /**
     * 参数发现器
     */
    private static final ParameterNameDiscoverer NAME_DISCOVERER = new DefaultParameterNameDiscoverer();
    /**
     * Express语法解析器
     */
    private static final ExpressionParser PARSER = new SpelExpressionParser();

    @Around("@annotation(RateLimit)")
    public Object interceptor(ProceedingJoinPoint joinPoint) throws Throwable {
        //得到被代理的方法
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();
        Class<?> targetClass = method.getDeclaringClass();
        RateLimit rateLimit = method.getAnnotation(RateLimit.class);
        //得到被切面修饰的方法的参数列表
        Object[] args = joinPoint.getArgs();

        if (rateLimit != null) {
            //得到注解的key
            String key = rateLimit.key();
            //清除当前类的缓存
            EvaluationContext context = new MethodBasedEvaluationContext(null, method, args, NAME_DISCOVERER);
            //解析spel表达式
            String realKey = String.valueOf(PARSER.parseExpression(key).getValue(context));
            //获取请求ip
            HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
            String ipAddress = IpUtil.getIpAddr(request);
            //拼接redis-key
            StringBuffer stringBuffer = new StringBuffer();
            stringBuffer.append(ipAddress).append("-")
                    .append(targetClass.getName()).append("-")
                    .append(method.getName()).append("-")
                    .append(realKey);

            List<String> keys = Collections.singletonList(stringBuffer.toString());

            Number number = RedisUtil.getRedisTemplate().execute(redisluaScript, keys, rateLimit.count(), rateLimit.time());

            if (number != null && number.intValue() != 0 && number.intValue() <= rateLimit.count()) {
                return joinPoint.proceed();
            }
        } else {
            return joinPoint.proceed();
        }
        return RetMsg.fail("访问过于频繁,请稍后再试!");
    }
}

使用示范

    //每个用户 每60秒可以访问五次  注:subject为登录实体 包含登录信息
    @RateLimit(key = "#subject.user.id", time = 60, count = 5)
    public RetMsg getBaseInfo(Subject subject) {
      //do something
    }

本文参考:
https://segmentfault.com/a/1190000016042927

相关文章

  • 基于Redis+Lua+Annotation的分布式应用限流

    为什么要做?网站免费接口被竞品恶意请求,导致数据量和并发暴增,一开始封禁了他的IP,但是后面他又换了IP来搞我们,...

  • 技术博客

    1. 基于Redis实现分布式应用限流

  • 聊聊高并发系统之限流特技-2

    摘要:上一篇《聊聊高并发系统限流特技-1》讲了限流算法、应用级限流、分布式限流;本篇将介绍接入层限流实现。 接入层...

  • 聊聊高并发系统限流特技-2

    转载来自开涛的聊聊高并发系统限流特技-2 上一篇《聊聊高并发系统限流特技-1》讲了限流算法、应用级限流、分布式限流...

  • 基于 Redis 实现分布式应用限流

    限流的目的是通过对并发访问/请求进行限速或者一个时间窗口内的的请求进行限速来保护系统,一旦达到限制速率则可以拒绝服...

  • 分布式应用

    常见分布式应用实现方案: 1)Spring Cloud构建分布式应用 2)基于ICE构建分布式应用 3)基于消息中...

  • Redis-pipeline实现简单的限流

    今天讨论分布式应用中的限流问题,这里通过redis的pipeline实现个简单的限流。这里先简单说一下pipeli...

  • 分布式限流 - 基于redis

    1,基于redis计数器 1)普通redis incr限流。不能保证原子性image.png2)lua脚本实现计数...

  • 面试中限流问题

    保障分布式应用高可用方案中,限流是必须的,超过一定的流量,我们就拒绝提供服务,从而使得我们的服务不会挂掉,所以限流...

  • 分布式系统限流策略(二)

    前文分布式系统限流策略(一)中介绍了系统限流的原理和基础的使用场景,可以关注下我在主页观看,本篇将介绍应用接入层(...

网友评论

      本文标题:基于Redis+Lua+Annotation的分布式应用限流

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