分布式限流のRedis+Lua実現


【転載は出典を明記してください】:https://segmentfault.com/a/1190000022538822
分散型ストリーム制限は、ストリーム制限サービスを原子化することが最も重要であり、ソリューションはredis+luaまたはnginx+lua技術を用いて実現することができ、この2つの技術によって高同時性と高性能を実現することができる.
まずredis+luaを用いてタイムウィンドウ内のあるインタフェースのリクエスト数制限ストリームを実現し,この機能を実現すると制限ストリームの総同時/リクエスト数と制限総リソース数に改造できる.Lua自体はプログラミング言語であり,複雑なトークンバケツやドレインバケツアルゴリズムを実現するためにも使用できる.操作は1つのluaスクリプト(原子操作に相当)であり、Redisは単一スレッドモデルであるため、スレッドは安全である.
Redisトランザクションに比べて、Luaスクリプトには次のような利点があります.
  • ネットワークオーバーヘッドを削減:Luaのコードを使用しないでRedisに複数回の要求を送信する必要があり、スクリプトは1回でネットワーク伝送を削減することができる.
  • 原子操作: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に関連する値をgetし、nilの場合は0
  • を返します.
  • は、次に、キャッシュに記録する値が制限サイズより大きいか否かを判断し、この制限ストリームを超えると0
  • に戻る.
  • が超えていない場合、keyのキャッシュ値+1は、有効期限が1秒後に設定され、キャッシュ値+1
  • に戻る.
    Javaプロジェクトの準備
    pom.xml加入
    
        
            org.springframework.boot
            spring-boot-starter-web
        
        
            org.springframework.boot
            spring-boot-starter-data-redis
        
        
            org.springframework.boot
            spring-boot-starter-aop
        
        
            org.apache.commons
            commons-lang3
        
        
            org.springframework.boot
            spring-boot-starter-test
        
    

    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 redisTemplate;
    
        @Autowired
        private DefaultRedisScript 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 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 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へのアクセスを継続します.効果は次のとおりです.
    ここでは簡単なプレゼンテーションのためにRuntimeExceptionを直接投げて、実際にRateLimitExceptionのような周波数制限の異常を上層部で直接処理し、ユーザーに友好的に返すことができます.
    【転載は出典を明記してください】:https://segmentfault.com/a/1190000022538822