java-限流算法

前言

由于服务器资源的有限性,需要对请求的速度以及数量做限制,防止过多的请求导致服务器崩溃。一旦服务器接收请求的数量超过给定最大值或请求的速度大于服务器处理的速度,应当主动拒绝掉这些请求,来保证服务器自身的健康和稳定。

单机版限流

单机版限流就是对单个服务做限流。目前用得比较多的就是guava的RateLimiter限流算法了,一起看看吧。

guava

maven地址

// https://mvnrepository.com/artifact/com.google.guava/guava
implementation group: 'com.google.guava', name: 'guava', version: '31.0.1-jre'

main方法

public static void main(String[] args) throws InterruptedException {
    RateLimiter rateLimiter = RateLimiter.create(1D, Duration.ofSeconds(5));
    while (true) {
        boolean elapsedSecond = rateLimiter.tryAcquire();
        if(elapsedSecond) {
            log.info("{} 获取 {}", Thread.currentThread(), System.nanoTime());
        }
    }
}

继承关系图

guava的关系比较简单

限流器

在这里插入图片描述

可睡眠的计时器

在这里插入图片描述

普通计时器

在这里插入图片描述

滴答器

在这里插入图片描述
用于获取当前时间

组合关系图

在这里插入图片描述
一把锁和一个睡眠计时器。

流程图

在这里插入图片描述

  1. 创建一个RateLimiter对象,有两个选择,SmoothBursty和SmoothWarmingUp。
  2. 加锁设置速率。
  3. 加锁,查询最近可用许可的时间,如果不能拿许可,返回false,如果可以拿许可,预定许可并获取等待时间。
  4. 睡眠等待直到许可生效。

使用悲观锁,不知道高并发的时候性能会不会有瓶颈?

总结

  1. guava的RateLimiter分为两种,一种SmoothBursty,限流的速率始终一致;另一种是SmoothWarmingUp,有个预热时间,预热期间限流速率平滑上升,预热时间结束时,达到给定的最大值。
    在这里插入图片描述

  2. 如果WarmingUp达到最大限流速度后暂停,那么限流器又会进行一次预热。
    在这里插入图片描述

  3. 限流器有阻塞版本的acquire()和非阻塞版本的tryAcquire()

eureka

也是偶然间看到eureka也有一个限流器,用于InstanceInfoReplicator向服务器同步数据。

maven地址

// https://mvnrepository.com/artifact/com.netflix.eureka/eureka-client
runtimeOnly group: 'com.netflix.eureka', name: 'eureka-client', version: '1.10.17'

main方法

public static void main(String[] args) throws InterruptedException {
    int count = 0;
    EurekaRateLimiter rateLimiter = new EurekaRateLimiter(TimeUnit.SECONDS);
    while (true) {
        boolean elapsedSecond = rateLimiter.acquire(1, 1);
        if (elapsedSecond) {
            log.info("{} 获取 {}", Thread.currentThread(), System.nanoTime());
            ++count;
            if (count > 100) {
                Thread.sleep(15000);
                count = 0;
            }
        }
    }
}

组合关系图

在这里插入图片描述

流程图

在这里插入图片描述

  1. 创建一个RateLimiter
  2. 填充令牌桶
  3. 消费令牌

总结

  1. eureka限流器,主要有两个参数,burstSizeaverageRateburstSize决定可以释放的总大小,当达到最大释放大小后,单位时间释放的个数由averateRate速率决定。如果令牌桶(burstSize)还有令牌,则消耗令牌返回,没有令牌时,消耗令牌速率受(averageRate)限制。
    在这里插入图片描述

图中,第一个数字代表burstSize,第二个数字代表averageRate,第三个数字代表睡眠的时间,比如Eureka-50-1-60表示burstSize=50,averageRate=1(acquire/s),sleepTime=60(ms)

  1. eureka限流器采用whilecompareAndSet乐观锁来进行线程同步。

分布式限流

分布式限流是对多个服务进行限流,即多个服务共享一个速率。

redis

目前用得比较多的是采用redis+lua的方案,用redis存储限流信息,lua编写限流算法。

这套方案需要对lua脚本有所了解,在spring-cloud-gateway已有实现。

request_rate_limiter.lua

local tokens_key = KEYS[1]
local timestamp_key = KEYS[2]
--redis.log(redis.LOG_WARNING, "tokens_key " .. tokens_key)

local rate = tonumber(ARGV[1])
local capacity = tonumber(ARGV[2])
local now = tonumber(ARGV[3])
local requested = tonumber(ARGV[4])

local fill_time = capacity/rate
local ttl = math.floor(fill_time*2)

--redis.log(redis.LOG_WARNING, "rate " .. ARGV[1])
--redis.log(redis.LOG_WARNING, "capacity " .. ARGV[2])
--redis.log(redis.LOG_WARNING, "now " .. ARGV[3])
--redis.log(redis.LOG_WARNING, "requested " .. ARGV[4])
--redis.log(redis.LOG_WARNING, "filltime " .. fill_time)
--redis.log(redis.LOG_WARNING, "ttl " .. ttl)

local last_tokens = tonumber(redis.call("get", tokens_key))
if last_tokens == nil then
  last_tokens = capacity
end
--redis.log(redis.LOG_WARNING, "last_tokens " .. last_tokens)

local last_refreshed = tonumber(redis.call("get", timestamp_key))
if last_refreshed == nil then
  last_refreshed = 0
end
--redis.log(redis.LOG_WARNING, "last_refreshed " .. last_refreshed)

local delta = math.max(0, now-last_refreshed)
local filled_tokens = math.min(capacity, last_tokens+(delta*rate))
local allowed = filled_tokens >= requested
local new_tokens = filled_tokens
local allowed_num = 0
if allowed then
  new_tokens = filled_tokens - requested
  allowed_num = 1
end

--redis.log(redis.LOG_WARNING, "delta " .. delta)
--redis.log(redis.LOG_WARNING, "filled_tokens " .. filled_tokens)
--redis.log(redis.LOG_WARNING, "allowed_num " .. allowed_num)
--redis.log(redis.LOG_WARNING, "new_tokens " .. new_tokens)

if ttl > 0 then
  redis.call("setex", tokens_key, ttl, new_tokens)
  redis.call("setex", timestamp_key, ttl, now)
end

-- return { allowed_num, new_tokens, capacity, filled_tokens, requested, new_tokens }
return { allowed_num, new_tokens }

笔者也是第一次接触lua语法,lua在openresty用得比较多,如果想用好openresty,lua还是绕不过,只能硬着头皮看了。

maven地址

//lettuce
implementation group: 'io.lettuce', name: 'lettuce-core', version: '6.1.4.RELEASE'

main方法

@Slf4j
public class RateLimiter {

    private volatile StatefulRedisConnection<String, String> connection;
    private volatile String scriptSha1;
    private String uri;
    private int replenishRate;
    private int burstCapacity;
    private int requestedTokens;

    public RateLimiter(String uri, int replenishRate, int burstCapacity, int requestedTokens) {
        this.uri = uri;
        this.replenishRate = replenishRate;
        this.burstCapacity = burstCapacity;
        this.requestedTokens = requestedTokens;
        getConnection();
        getScriptSha1();
    }

    public boolean acquire(String id) {
        // How many requests per second do you want a user to be allowed to do?
        int replenishRate = this.replenishRate;

        // How much bursting do you want to allow?
        int burstCapacity = this.burstCapacity;

        // How many tokens are requested per request?
        int requestedTokens = this.requestedTokens;

        List<String> keys = getKeys(id);
        // The arguments to the LUA script. time() returns unixtime in seconds.
        List<String> scriptArgs = Arrays.asList(replenishRate + "", burstCapacity + "",
                Instant.now().getEpochSecond() + "", requestedTokens + "");

        // allowed, tokens_left = redis.eval(SCRIPT, keys, args)
        List<Long> result = execute(keys, scriptArgs);
        return result.size() > 0 && result.get(0) == 1L;
    }

    private List<String> getKeys(String id) {
        // use `{}` around keys to use Redis Key hash tags
        // this allows for using redis cluster

        // Make a unique key per user.
        String prefix = "request_rate_limiter.{" + id;

        // You need two Redis keys for Token Bucket.
        String tokenKey = prefix + "}.tokens";
        String timestampKey = prefix + "}.timestamp";
        return Arrays.asList(tokenKey, timestampKey);
    }

    private List<Long> execute(List<String> keys, List<String> scriptArgs) {
        try {
            if (StringUtils.isEmpty(getScriptSha1())) {
                return Arrays.asList(1L, -1L);
            }
            return getConnection().sync().evalsha(getScriptSha1(), ScriptOutputType.MULTI, keys.toArray(new String[0]), scriptArgs.toArray(new String[0]));
        } catch (Exception e) {
            log.info("请求限流信息报错", e);
            return Arrays.asList(1L, -1L);
        }
    }

    /**
     * DCL
     */
    private StatefulRedisConnection<String, String> getConnection() {
        StatefulRedisConnection<String, String> connection = this.connection;
        if (Objects.isNull(connection)) {
            synchronized (this) {
                connection = this.connection;
                if (Objects.isNull(connection)) {
                    RedisClient redisClient = RedisClient.create(this.uri);
                    connection = redisClient.connect();
                    this.connection = connection;
                }
            }
        }
        return connection;
    }

    private String getScriptSha1() {
        String sha1 = this.scriptSha1;
        if (Objects.isNull(sha1)) {
            synchronized (this) {
                sha1 = this.scriptSha1;
                if (Objects.isNull(sha1)) {
                    sha1 = doLoadScript();
                    this.scriptSha1 = sha1;
                }
            }
        }
        return sha1;
    }

    private String doLoadScript() {
        try (InputStream inputStream = getClass().getResourceAsStream("/request_rate_limiter.lua")) {
            if (Objects.isNull(inputStream)) {
                return "";
            }
            try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
                int len;
                byte[] buffer = new byte[256];
                while (true) {
                    len = inputStream.read(buffer);
                    if (len == -1) {
                        break;
                    }
                    baos.write(buffer, 0, len);
                }
                byte[] script = baos.toByteArray();
                try {
                    return getConnection().sync().scriptLoad(script);
                } catch (Exception e) {
                    log.info("doLoadScript报错", e);
                    return "";
                }
            }
        } catch (Exception e) {
            log.info("doLoadScript报错", e);
            return "";
        }
    }

    public static void main(String[] args) throws InterruptedException {
        int count = 0;
        RateLimiter rateLimiter = new RateLimiter("redis://:civic@localhost/10", 1, 50, 1);
        while (true) {
            boolean elapsedSecond = rateLimiter.acquire("xxx");
            if (elapsedSecond) {
                log.info("{} 获取 {}", Thread.currentThread(), System.nanoTime());
                ++count;
                if (count > 100) {
                    Thread.sleep(60000);
                    count = 0;
                }
            }
        }
    }
}

流程图

这个流程比较简单:

  1. 创建RateLimiter,初始化RedisClient,获取StatefulRedisConnection连接,加载脚本到Redis。
  2. 构建键值和参数,调用lettuce的evalSha方法执行限流lua脚本,并获取应答。
  3. 根据应答(allowed_num, new_tokens)判断是否有许可。

总结

通过测试总结以下几点规律:

  1. Redis的lua限流算法和Eureka的单机版算法表现一致。
    在这里插入图片描述

图中,第一个数字代表averageRate,第二个数字代表burstSize,第三个数字代表睡眠的时间,比如Redis-1-50-60表示averageRate=1(acquire/s),burstSize=50,sleepTime=60(ms)

  1. Redis限流器采用lua脚本的原子性进行线程同步。
  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值