zuul网关限流

最近项目需要实现限流的功能,项目使用的是spring cloud框架,用zuul做网管模块。准备在网管层加上限流功能。

1、使用RateLimiter+filter做统一入口限流。适用单机

Guava中开源出来一个令牌桶算法的工具类RateLimiter,使用简单,cloud已经集成该模块,直接引入。

<dependency>
		<groupId>com.marcosbarbero.cloud</groupId>
		<artifactId>spring-cloud-zuul-ratelimit</artifactId>
		<version>1.7.1.RELEASE</version>
</dependency>

直接新建一个zuulFilter,类型 pre.

@Component
public class RateLimitZuulFilter extends ZuulFilter{
	
	private static final Logger LOGGER = LoggerFactory.getLogger(RateLimitZuulFilter.class);
	
	//初始化 放入 1000令牌/s  时间窗口为 1s
	private final RateLimiter rateLimiter = RateLimiter.create(1000.0);

	@Override
	public boolean shouldFilter() {
		// 一直过滤
		return true;
	}

	@Override
	public Object run() throws ZuulException {
		
		RequestContext ctx =  RequestContext.getCurrentContext();
		HttpServletResponse response = ctx.getResponse();
		
		if(!rateLimiter.tryAcquire()) {
			response.setContentType(MediaType.TEXT_PLAIN_VALUE);
			response.setStatus(HttpStatus.TOO_MANY_REQUESTS.value());
			ctx.setSendZuulResponse(false);// 过滤该请求,不对其进行路由
			try {
				response.getWriter().write("TOO MANY REQUESTS");
			} catch (IOException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
			
		}else {
			ctx.setResponseStatusCode(200); 
			LOGGER.info("OK !!!");
		}
		
		return null;
	}

	@Override
	public String filterType() {
		return "pre";
	}

	@Override
	public int filterOrder() {
		
		return -5;
	}

}

2、使用zuul + RateLimiter 配置方式,在application.yml加简单配置就行,适用分布式

zuul:
  add-host-header: true
  routes:
    servicewel:
      path: /getway/servicewel/**
      serviceId: service-wel
    servicehi:
      path: /getway/servicehi/**
      serviceId: service-hi  
  ratelimit:
    enabled: true
    behind-proxy: true
    key-prefix: ilea-getway-key
    repository: Redis
    policies: 
      servicewel:
        limit: 5
        quota: 30
        refresh-interval: 60
        type:
          - URL
          - USER
          - ORIGIN
      servicehi:
        limit: 10
        quota: 30
        refresh-interval: 60
        type:
          - URL
          - USER
  • repository :是key值保存方式,可以选Redis、Consul、Spring Data JPA等方式,这里选择的是 Redis,所以要添加redis依赖和配置。
  • limit 单位时间内允许访问的次数
  • quota 单位时间内允许访问的总时间(单位时间窗口期内,所有的请求的总时间不能超过这个时间限制)
  • refresh-interval 单位时间设置
  • type 限流类型:
    • url类型的限流就是通过请求路径区分
    • origin是通过客户端IP地址区分
    • user是通过登录用户名进行区分,也包括匿名用户

通过用户名进行限流可以自定义key策略

@Bean
public RateLimitKeyGenerator rateLimitKeyGenerator(final RateLimitProperties properties,final RateLimitUtils rateLimitUtils) {
		//RateLimitPreFilter
		return new DefaultRateLimitKeyGenerator(properties, rateLimitUtils) {
			@Override
		    public String key(final HttpServletRequest request, final Route route, final Policy policy) {
				String name = request.getParameter("name");
		        return super.key(request, route, policy)+":"+name;
		      
		    }
		};
		
	}

3、redis计数器限流,利用redis.incrBy()方法实现计数器,最简单的实现方法,无法实现平滑。适用于分布式

public synchronized boolean access() {
      
	if(!redis.hasKey(COUNTER_KEY)) {
		redis.set(COUNTER_KEY,1,(long)2, TimeUnit.SECONDS);//时间到就重新初始化
		return true;
	}
	if(reids.hasKey(COUNTER_KEY)&&redsi.incrBy(COUNTER_KEY,(long)1) > (long)400) {
		LOGGER.info("调用频率过快");
		return false;
	}
	return true;
}



// LUA

local key = KEYS[1] --count_key
local limit = tonumber(ARGV[1])        --limit
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 1
end

4、代码实现令牌桶算法

参考https://zhuanlan.zhihu.com/p/20872901 ,大神讲的很仔细

可以在 Bucket 中存放现在的 Token 数量,然后存储上一次补充 Token 的时间戳,当用户下一次请求获取一个 Token 的时候, 根据此时的时间戳,计算从上一个时间戳开始,到现在的这个时间点所补充的所有 Token 数量,加入到 Bucket 当中。

public class RateLimiter {

    private JedisPool jedisPool;

    private long intervalInMills;
    private long limit;
    private double intervalPerPermit;

    public RateLimiter() {
        jedisPool = new JedisPool("127.0.0.1", 6379);
        intervalInMills = 10000;
        limit = 3;
        intervalPerPermit = intervalInMills * 1.0 / limit;
    }

    // 单线程操作下才能保证正确性
    // 需要这些操作原子性的话,最好使用 redis 的 lua script
    public boolean access(String userId) {

        String key = genKey(userId);

        try (Jedis jedis = jedisPool.getResource()) {
        	// 取桶
            Map<String, String> counter = jedis.hgetAll(key);

            if (counter.size() == 0) {
                TokenBucket tokenBucket = new TokenBucket(System.currentTimeMillis(), limit - 1);
                jedis.hmset(key, tokenBucket.toHash());
                return true;
            } else {
                TokenBucket tokenBucket = TokenBucket.fromHash(counter);

                //取上次添加令牌时间,求与当前时间差值,计算是否加令牌,加多少
                long lastRefillTime = tokenBucket.getLastRefillTime();
                long refillTime = System.currentTimeMillis();
                long intervalSinceLast = refillTime - lastRefillTime;

                long currentTokensRemaining;
                if (intervalSinceLast > intervalInMills) {
                	//差值大于 周期, 令牌设为最大
                    currentTokensRemaining = limit;
                } else {
                	// 根据 添加令牌速率计算应该添加多少令牌
                    long grantedTokens = (long) (intervalSinceLast / intervalPerPermit);
                    currentTokensRemaining = Math.min(grantedTokens + tokenBucket.getTokensRemaining(), limit);
                }

                tokenBucket.setLastRefillTime(refillTime);
                assert currentTokensRemaining >= 0;
                if (currentTokensRemaining == 0) {
                	//无令牌可用
                    tokenBucket.setTokensRemaining(currentTokensRemaining);
                    jedis.hmset(key, tokenBucket.toHash());
                    return false;
                } else {
                	//使用一个令牌
                    tokenBucket.setTokensRemaining(currentTokensRemaining - 1);
                    jedis.hmset(key, tokenBucket.toHash());
                    return true;
                }
            }
        }
    }

    private String genKey(String userId) {
        return "rate:limiter:" + intervalInMills + ":" + limit + ":" + userId;
    }

    
    public static class TokenBucket {
    	
    	/*
   	  	* 上一次添加时间戳
   	  	*/
   	 	private long lastRefillTime;
   	 	/*
   	 	 * 剩下的令牌数
   	 	 */
        private long tokensRemaining;

        public TokenBucket(long lastRefillTime, long tokensRemaining) {
            this.lastRefillTime = lastRefillTime;
            this.tokensRemaining = tokensRemaining;
        }

        public static TokenBucket fromHash(Map<String, String> hash) {
            long lastRefillTime = Long.parseLong(hash.get("lastRefillTime"));
            int tokensRemaining = Integer.parseInt(hash.get("tokensRemaining"));
            return new TokenBucket(lastRefillTime, tokensRemaining);
        }

        public Map<String, String> toHash() {
            Map<String, String> hash = new HashMap<>();
            hash.put("lastRefillTime", String.valueOf(lastRefillTime));
            hash.put("tokensRemaining", String.valueOf(tokensRemaining));
            return hash;
        }
    }

}

5、LUA+redis

local key, intervalPerPermit, refillTime = KEYS[1], tonumber(ARGV[1]), tonumber(ARGV[2])
local limit, interval = tonumber(ARGV[3]), tonumber(ARGV[4])
local bucket = redis.call('hgetall', key)

local currentTokens

-- table.maxn(bucket) 不存在 key值为正数的值 = 0,即bucket不存在
if table.maxn(bucket) == 0 then
	-- 设置令牌数为最大
    currentTokens = limit
    redis.call('hset', key, 'lastRefillTime', refillTime)
elseif table.maxn(bucket) == 4 then
    -- 桶存在,先计算需要添加的令牌

    local lastRefillTime, tokensRemaining = tonumber(bucket[2]), tonumber(bucket[4])

    if refillTime > lastRefillTime then
        -- 计算差值 
		-- 1.过了整个周期了,需要补到最大值
		-- 2.如果到了至少补充一个的周期了,那么需要补充部分,否则不补充
        local intervalSinceLast = refillTime - lastRefillTime
        if intervalSinceLast > interval then
            currentTokens = limit
            redis.call('hset', key, 'lastRefillTime', refillTime)
        else
            local grantedTokens = math.floor(intervalSinceLast / intervalPerPermit)
            if grantedTokens > 0 then
                -- ajust lastRefillTime, we want shift left the refill time.
                local padMillis = math.fmod(intervalSinceLast, intervalPerPermit)
                redis.call('hset', key, 'lastRefillTime', refillTime - padMillis)
            end
            currentTokens = math.min(grantedTokens + tokensRemaining, limit)
        end
    else
        -- 有别的线程已添加过
        currentTokens = tokensRemaining
    end
end

assert(currentTokens >= 0)

if currentTokens == 0 then
    -- 无令牌可用
    redis.call('hset', key, 'tokensRemaining', currentTokens)
    return 0
else
    redis.call('hset', key, 'tokensRemaining', currentTokens - 1)
    return 1
end

java中判断

public boolean access(String userId) {
		String key = genKey(userId);
		/**
		 * keys[1] = key;
		 * arvg[1] = intervalPerPermit; 每个用多少秒
		 * arvg[2] = System.currentTimeMillis() 当前时间  
		 * avrg[3] = 总令牌数
		 * avrg[4] = 周期
		 */
		long result = (long) jedis.evalsha(scriptSha1, 1, key, 
				String.valueOf(intervalPerPermit), 
				String.valueOf(System.currentTimeMillis()), 
				String.valueOf(limit),
				String.valueOf(intervalInMills));
		return result == 1L;
	}

参照博客:

https://zhuanlan.zhihu.com/p/20872901

https://blog.csdn.net/lsblsb/article/details/69486012

这两篇博客写的非常棒,给我很大帮助。谢谢大神。

猜你喜欢

转载自blog.csdn.net/SHIYUN123zw/article/details/82315252