背景

令牌桶限流是一种常见的流量控制算法,用于控制系统的请求处理速率,防止系统过载。在令牌桶限流算法中,可以将请求看作是令牌,而令牌桶则表示系统的处理能力。系统在处理请求时,首先需要从令牌桶中获取令牌,如果令牌桶中没有足够的令牌,就需要等待一定时间,直到令牌桶中有足够的令牌。
具体来说,令牌桶限流算法可以通过以下方式实现:
1.系统维护一个固定容量的令牌桶,每秒钟会向桶中添加一定数量的令牌,直到桶的容量达到上限。
2.每次请求来临时,需要先从令牌桶中获取令牌,如果桶中有足够的令牌,则请求被允许通过,并从桶中移除一个令牌;如果桶中的令牌数量不足,则请求被拒绝。具体来说,令牌桶算法会维护一个令牌桶,其中包含一定数量的令牌,每个令牌代表一个可以执行操作的许可。在每个时间段内,如果有令牌可用,就可以执行一个操作,并将令牌桶中的令牌数量减少一。如果没有令牌可用,就不能执行操作,需要等待一定时间,直到令牌桶中有足够的令牌。
3.由于令牌桶的容量是有限的,因此当桶中的令牌数量达到上限时,新的令牌会被丢弃,从而限制了请求的处理速率。
令牌桶限流算法可以在多种场景中进行流量控制,例如 Web 应用程序、消息队列、数据库等。在 Web 应用程序中,可以通过令牌桶限流算法控制 API 的访问速率,防止 API 被恶意攻击或者过载。在消息队列中,可以通过令牌桶限流算法控制消息的生产和消费速率,防止消息堆积和系统崩溃。在数据库中,可以通过令牌桶限流算法控制查询和写入操作的速率,防止数据库过载和响应时间过长。

lua脚本实现令牌桶算法

Lua 脚本可以用来实现 Redis 的令牌桶限流:
1.定义 Redis 数据结构
使用 Redis 的 Hash 数据结构存储当前令牌桶的状态。在 Hash 中,rate 表示速率(每秒生成的令牌数),capacity 表示桶的容量(最多可以同时存储的令牌数),tokens 表示当前桶中的令牌数量,timestamp 表示上次更新令牌数量的时间戳。示例代码:

HSET rdb:token_bucket rate 10 capacity 100 tokens 100 timestamp 0

2.编写 Lua 脚本
编写 Lua 脚本来实现限流逻辑。在脚本中,首先读取当前时间戳和桶的状态,计算出从上次更新时间戳到当前时间应该生成的令牌数量。然后,将当前桶中的令牌数量和应该生成的令牌数量相加,得到当前桶中的令牌数量。如果当前桶中的令牌数量超过了桶的容量,将其限制为桶的容量。
然后,判断当前桶中的令牌数量是否足够执行操作。如果令牌数量足够,将当前桶中的令牌数量减去操作所需的令牌数量,并更新桶的状态。如果令牌数量不足,则返回限流的错误信息。
示例代码:

-- 读取桶的状态
local rate = tonumber(redis.call('HGET', KEYS[1], 'rate'))
local capacity = tonumber(redis.call('HGET', KEYS[1], 'capacity'))
local tokens = tonumber(redis.call('HGET', KEYS[1], 'tokens'))
local timestamp = tonumber(redis.call('HGET', KEYS[1], 'timestamp'))

-- 计算应该生成的令牌数量
local now = redis.call('TIME')
local elapsed = now[1] - timestamp
local generated = math.floor(elapsed * rate)

-- 更新令牌数量并限制桶的容量
tokens = math.min(capacity, tokens + generated)

-- 执行操作
local required = tonumber(ARGV[1])
if tokens >= required then
    tokens = tokens - required
    redis.call('HSET', KEYS[1], 'tokens', tokens)
    redis.call('HSET', KEYS[1], 'timestamp', now[1])
    return 1
else
    return 0
end

3.在应用程序中调用 Lua 脚本
在应用程序中,使用 Redis 的 EVAL 命令来调用 Lua 脚本。示例代码:

@Component
public class TokenBucketLimiter {

    @Autowired
    private RedisTemplate<String, String> redisTemplate;

    public boolean tryAcquire(String key, int tokens) {
        List<String> keys = Arrays.asList(key);
        List<String> args = Arrays.asList(Integer.toString(tokens));
        Long result = redisTemplate.execute(new DefaultRedisScript<>(
                "local rate = tonumber(redis.call('HGET', KEYS[1], 'rate')) " +
                        "local capacity = tonumber(redis.call('HGET', KEYS[1], 'capacity')) " +
                        "local tokens = tonumber(redis.call('HGET', KEYS[1], 'tokens')) " +
                        "local timestamp = tonumber(redis.call('HGET', KEYS[1], 'timestamp')) " +
                        "local now = redis.call('TIME') " +
                        "local elapsed = now[1] - timestamp " +
                        "local generated = math.floor(elapsed * rate) " +
                        "tokens = math.min(capacity, tokens + generated) " +
                        "if tokens >= tonumber(ARGV[1]) then " +
                        "    tokens = tokens - tonumber(ARGV[1]) " +
                        "    redis.call('HSET', KEYS[1], 'tokens', tokens) " +
                        "    redis.call('HSET', KEYS[1], 'timestamp', now[1]) " +
                        "    return 1 " +
                        "else " +
                        "    return 0 " +
                        "end",
                Long.class), keys, args);
        return result != null && result == 1L;
    }
}

或者如下脚本:

-- 返回码 1:通过限流 0:不通过
-- rate ARGV[1] 每秒填充速率
-- now  ARGV[2] 当前时间
-- capacity ARGV[3] 令牌桶最大数量
-- request ARGV[4] 需要令牌数量
local SUCCESS = "1"
local FAIL = "0"
local rate = tonumber(ARGV[1]) -- replenishRate 令令牌桶填充平均速率
local capacity = tonumber(ARGV[2]) -- burstCapacity 令牌桶上限
local now = tonumber(ARGV[3]) -- 机器传入的当前时间 秒
local requested = tonumber(ARGV[4]) -- 消耗令牌数量,默认取1

local fill_time = capacity/rate   -- 计算令牌桶填充满令牌需要多久时间
local ttl = math.floor(fill_time*2) -- *2 保证时间充足

local result = SUCCESS;




-- ttl 防止小于0
if ttl < 1 then
    ttl = 10
end


-- 1、获取桶内令牌剩余数量
local last_tokens = tonumber(redis.call("get", KEYS[1]))
-- 获得令牌桶剩余令牌数
if last_tokens == nil then -- 第一次时,没有数值,所以桶时满的
    last_tokens = capacity
end

-- 2、获取上次更新时间
local last_refreshed = tonumber(redis.call("get", KEYS[2]))
-- 令牌桶最后填充令牌时间
if last_refreshed == nil then
    last_refreshed = 0
end

-- 3、本次验证和上次更新时间的间隔
local delta = math.max(0, now-last_refreshed)
-- 填充令牌,计算新的令牌桶剩余令牌数 填充不超过令牌桶令牌上限。
local filled_tokens = math.min(capacity, last_tokens+(delta*rate))

-- 4、判断令牌数量是否足够
local allowed = filled_tokens >= requested
local new_tokens = filled_tokens
local allowed_num = "0"
if allowed then
    -- 若成功,令牌桶剩余令牌数(new_tokens) 减消耗令牌数( requested ),并设置获取成功( allowed_num = 1 ) 。
    new_tokens = filled_tokens - requested
    allowed_num = SUCCESS
end

-- 5、设置令牌桶剩余令牌数( new_tokens ) ,令牌桶最后填充令牌时间(now) ttl是超时时间
redis.call("setex", KEYS[1], ttl, new_tokens)
redis.call("setex", KEYS[2], ttl, now)

if not allowed then
    return FAIL
end

return SUCCESS