美文网首页
分布式限流之Redis+Lua实现

分布式限流之Redis+Lua实现

作者: 后端老鸟 | 来源:发表于2020-05-04 00:09 被阅读0次

    【转载请注明出处】:https://www.jianshu.com/p/1163da62f78d

    分布式限流最关键的是要将限流服务做成原子化,而解决方案可以使用redis+lua或者nginx+lua技术进行实现,通过这两种技术可以实现的高并发和高性能。

    首先我们来使用redis+lua实现时间窗内某个接口的请求数限流,实现了该功能后可以改造为限流总并发/请求数和限制总资源数。Lua本身就是一种编程语言,也可以使用它实现复杂的令牌桶或漏桶算法。
    因操作是在一个lua脚本中(相当于原子操作),又因Redis是单线程模型,因此是线程安全的。

    相比Redis事务来说,Lua脚本有以下优点

    • 减少网络开销: 不使用 Lua 的代码需要向 Redis 发送多次请求,而脚本只需一次即可,减少网络传输;
    • 原子操作:Redis 将整个脚本作为一个原子执行,无需担心并发,也就无需事务;
    • 复用:脚本会永久保存 Redis 中,其他客户端可继续使用。

    下面使用SpringBoot项目来进行介绍。

    准备Lua 脚本

    req_ratelimit.lua

    local key = "req.rate.limit:" .. KEYS[1]   --限流KEY
    local limitCount = tonumber(ARGV[1])       --限流大小
    local limitTime = tonumber(ARGV[2])        --限流时间
    local current = tonumber(redis.call('get', key) or "0")
    if current + 1 > limitCount then --如果超出限流大小
        return 0
    else  --请求数+1,并设置1秒过期
        redis.call("INCRBY", key,"1")
        redis.call("expire", key,limitTime)
        return current + 1
    end
    
    • 我们通过KEYS[1] 获取传入的key参数
    • 通过ARGV[1]获取传入的limit参数
    • redis.call方法,从缓存中get和key相关的值,如果为nil那么就返回0
    • 接着判断缓存中记录的数值是否会大于限制大小,如果超出表示该被限流,返回0
    • 如果未超过,那么该key的缓存值+1,并设置过期时间为1秒钟以后,并返回缓存值+1

    准备Java项目

    pom.xml加入
    <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>
    

    Redis 配置

    spring.redis.host=127.0.0.1 
    spring.redis.port=6379 
    spring.redis.password=
    spring.redis.database=0
    # 连接池最大连接数(使用负值表示没有限制)
    spring.redis.jedis.pool.max-active=20
    # 连接池最大阻塞等待时间(使用负值表示没有限制)
    spring.redis.jedis.pool.max-wait=-1
    # 连接池中的最大空闲连接
    spring.redis.jedis.pool.max-idle=10
    # 连接池中的最小空闲连接
    spring.redis.jedis.pool.min-idle=0
    # 连接超时时间(毫秒)
    spring.redis.timeout=2000
    
    限流注解

    注解的目的,是在需要限流的方法上使用

    @Target({ElementType.TYPE, ElementType.METHOD})
    @Retention(RetentionPolicy.RUNTIME)
    public @interface RateLimiter {
    
        /**
         * 限流唯一标识
         * @return
         */
        String key() default "";
    
        /**
         * 限流时间
         * @return
         */
        int time();
    
        /**
         * 限流次数
         * @return
         */
        int count();
    
    }
    
    lua文件配置及RedisTemplate配置
    @Aspect
    @Configuration
    @Slf4j
    public class RateLimiterAspect {
    
    
        @Autowired
        private RedisTemplate<String, Serializable> redisTemplate;
    
        @Autowired
        private DefaultRedisScript<Number> redisScript;
    
        @Around("execution(* com.sunlands.zlcx.datafix.web ..*(..) )")
        public Object interceptor(ProceedingJoinPoint joinPoint) throws Throwable {
    
            MethodSignature signature = (MethodSignature) joinPoint.getSignature();
            Method method = signature.getMethod();
            Class<?> targetClass = method.getDeclaringClass();
            RateLimiter rateLimit = method.getAnnotation(RateLimiter.class);
    
            if (rateLimit != null) {
                HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
                String ipAddress = getIpAddr(request);
    
                StringBuffer stringBuffer = new StringBuffer();
                stringBuffer.append(ipAddress).append("-")
                        .append(targetClass.getName()).append("- ")
                        .append(method.getName()).append("-")
                        .append(rateLimit.key());
    
                List<String> keys = Collections.singletonList(stringBuffer.toString());
    
                Number number = redisTemplate.execute(redisScript, keys, rateLimit.count(), rateLimit.time());
    
                if (number != null && number.intValue() != 0 && number.intValue() <= rateLimit.count()) {
                    log.info("限流时间段内访问第:{} 次", number.toString());
                    return joinPoint.proceed();
                }
    
            } else {
                return joinPoint.proceed();
            }
    
            throw new RuntimeException("已经到设置限流次数");
        }
    
        public static String getIpAddr(HttpServletRequest request) {
            String ipAddress = null;
            try {
                ipAddress = request.getHeader("x-forwarded-for");
                if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
                    ipAddress = request.getHeader("Proxy-Client-IP");
                }
                if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
                    ipAddress = request.getHeader("WL-Proxy-Client-IP");
                }
                if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
                    ipAddress = request.getRemoteAddr();
                }
                // 对于通过多个代理的情况,第一个IP为客户端真实IP,多个IP按照','分割
                if (ipAddress != null && ipAddress.length() > 15) {
                    // "***.***.***.***".length()= 15
                    if (ipAddress.indexOf(",") > 0) {
                        ipAddress = ipAddress.substring(0, ipAddress.indexOf(","));
                    }
                }
            } catch (Exception e) {
                ipAddress = "";
            }
            return ipAddress;
        }
    
    
    }
    
    控制层
    @RestController
    @Slf4j
    @RequestMapping("limit")
    public class RateLimiterController {
    
        @Autowired
        private RedisTemplate redisTemplate;
    
        @GetMapping(value = "/test")
        @RateLimiter(key = "test", time = 10, count = 1)
        public ResponseEntity<Object> test() {
    
            String date = DateFormatUtils.format(new Date(), "yyyy-MM-dd HH:mm:ss.SSS");
            RedisAtomicInteger limitCounter = new RedisAtomicInteger("limitCounter", redisTemplate.getConnectionFactory());
            String str = date + " 累计访问次数:" + limitCounter.getAndIncrement();
            log.info(str);
            return ResponseEntity.ok(str);
        }
    }
    

    启动项目进行测试

    不断访问url http://127.0.0.1:8090/limit/test,效果如下:

    image.png image.png

    我这里为了简单演示是直接抛了一个RuntimeException,实际可以单独定义一个如RateLimitException,在上层直接处理这种频次限制的异常,以友好的方式返回给用户。

    【转载请注明出处】:https://www.jianshu.com/p/1163da62f78d

    相关文章

      网友评论

          本文标题:分布式限流之Redis+Lua实现

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