在开发过程中,限流是一个很重要的问题,能防止系统被过多的请求冲垮。今天咱们就来聊聊 Redis 里的两种限流算法:令牌桶和漏桶,看看它们各自的应用以及对比情况。
一、限流算法基础概念
限流算法就是控制请求流量的一种手段,就好比给水管装个阀门,控制水的流量,防止水管被冲爆。在系统里,就是防止过多的请求把服务器搞崩溃。常见的限流算法有很多,咱们今天重点说令牌桶和漏桶。
令牌桶算法
令牌桶算法就像一个装令牌的桶,系统会按照一定的速率往桶里放令牌。每个请求过来的时候,都得从桶里拿一个令牌才能处理。要是桶里没令牌了,这个请求就只能等着或者被拒绝。比如说,咱们设定每秒往桶里放 100 个令牌,桶的最大容量是 1000 个。如果一个请求过来,桶里有令牌,就拿走一个处理请求;要是桶里没令牌了,这个请求就处理不了。
漏桶算法
漏桶算法就像一个有洞的桶,请求就像水一样往桶里倒。不管有多少水进来,桶都会以固定的速率把水漏出去。也就是说,请求不管多快地进来,系统都会以固定的速率处理这些请求。如果桶满了,多出来的水(请求)就会被倒掉(拒绝)。
二、Redis 实现令牌桶算法
实现思路
在 Redis 里实现令牌桶算法,主要是利用 Redis 的原子操作来保证令牌的发放和获取是线程安全的。咱们可以用 Redis 的有序集合来模拟令牌桶,每个令牌就是集合里的一个元素,元素的分数就是令牌的生成时间。
示例代码(Lua 技术栈)
-- 令牌桶算法 Lua 脚本
-- KEYS[1] 是令牌桶的键名
-- ARGV[1] 是令牌桶的最大容量
-- ARGV[2] 是令牌生成的速率(每秒生成的令牌数)
-- ARGV[3] 是当前时间戳
-- 获取当前令牌桶里的令牌数量
local current_tokens = tonumber(redis.call('zcard', KEYS[1]))
-- 计算从上次更新到现在应该生成的令牌数量
local new_tokens = math.floor((tonumber(ARGV[3]) - tonumber(redis.call('get', KEYS[1] .. ':last_update'))) * tonumber(ARGV[2]))
-- 更新令牌桶里的令牌数量
local total_tokens = math.min(tonumber(ARGV[1]), current_tokens + new_tokens)
-- 更新最后更新时间
redis.call('set', KEYS[1] .. ':last_update', ARGV[3])
-- 清空旧的令牌
redis.call('zremrangebyscore', KEYS[1], '-inf', tonumber(ARGV[3]) - (tonumber(ARGV[1]) / tonumber(ARGV[2])))
-- 生成新的令牌
for i = 1, new_tokens do
redis.call('zadd', KEYS[1], tonumber(ARGV[3]), tonumber(ARGV[3]) + i)
end
-- 尝试获取一个令牌
if total_tokens > 0 then
redis.call('zremrangebyrank', KEYS[1], 0, 0)
return 1
else
return 0
end
代码解释
这段 Lua 脚本首先获取当前令牌桶里的令牌数量,然后根据当前时间和上次更新时间计算应该生成的新令牌数量。接着更新令牌桶里的令牌数量,清空旧的令牌,生成新的令牌。最后尝试获取一个令牌,如果有令牌就返回 1,表示请求可以处理;如果没有令牌就返回 0,表示请求被拒绝。
三、Redis 实现漏桶算法
实现思路
在 Redis 里实现漏桶算法,主要是利用 Redis 的列表来模拟漏桶。请求就像元素一样添加到列表的尾部,系统会以固定的速率从列表的头部取出元素进行处理。
示例代码(Lua 技术栈)
-- 漏桶算法 Lua 脚本
-- KEYS[1] 是漏桶的键名
-- ARGV[1] 是漏桶的最大容量
-- ARGV[2] 是漏桶漏水的速率(每秒处理的请求数)
-- ARGV[3] 是当前时间戳
-- 获取当前漏桶里的请求数量
local current_requests = tonumber(redis.call('llen', KEYS[1]))
-- 计算从上次更新到现在应该处理的请求数量
local processed_requests = math.floor((tonumber(ARGV[3]) - tonumber(redis.call('get', KEYS[1] .. ':last_update'))) * tonumber(ARGV[2]))
-- 更新漏桶里的请求数量
local total_requests = math.max(0, current_requests - processed_requests)
-- 更新最后更新时间
redis.call('set', KEYS[1] .. ':last_update', ARGV[3])
-- 如果漏桶没满,添加新的请求
if total_requests < tonumber(ARGV[1]) then
redis.call('rpush', KEYS[1], 1)
return 1
else
return 0
end
代码解释
这段 Lua 脚本首先获取当前漏桶里的请求数量,然后根据当前时间和上次更新时间计算应该处理的请求数量。接着更新漏桶里的请求数量,更新最后更新时间。如果漏桶没满,就添加新的请求,返回 1 表示请求可以处理;如果漏桶满了,就返回 0 表示请求被拒绝。
四、应用场景
令牌桶算法的应用场景
- 突发流量处理:令牌桶算法允许一定程度的突发流量。比如说,一个电商网站在促销活动的时候,会有大量的用户同时访问。令牌桶算法可以在桶里有足够令牌的情况下,快速处理这些突发请求,而不会像漏桶算法那样严格限制请求速率。
- API 限流:对于一些开放的 API 接口,为了防止恶意攻击或者过度使用,我们可以使用令牌桶算法来限制每个用户的请求速率。每个用户有自己的令牌桶,根据用户的等级或者付费情况设置不同的令牌生成速率和桶的最大容量。
漏桶算法的应用场景
- 稳定流量控制:漏桶算法适合对流量进行平稳控制的场景。比如说,一个视频直播平台,为了保证服务器的稳定运行,需要以固定的速率处理用户的请求。漏桶算法可以保证每个用户的请求都以固定的速率被处理,不会出现流量忽大忽小的情况。
- 防止 DDoS 攻击:在网络安全方面,漏桶算法可以有效地防止 DDoS 攻击。当有大量的恶意请求过来时,漏桶算法会以固定的速率处理这些请求,多余的请求会被拒绝,从而保护服务器不被攻击。
五、技术优缺点
令牌桶算法的优缺点
- 优点:
- 可以处理突发流量,在桶里有足够令牌的情况下,能快速响应大量请求。
- 灵活性高,可以根据不同的业务需求调整令牌生成速率和桶的最大容量。
- 缺点:
- 实现相对复杂,需要考虑令牌的生成、过期和获取等问题。
- 对于流量的控制不够严格,可能会出现短时间内流量过大的情况。
漏桶算法的优缺点
- 优点:
- 可以保证流量的平稳,以固定的速率处理请求,不会出现流量忽大忽小的情况。
- 实现相对简单,只需要维护一个列表和一个时间戳。
- 缺点:
- 不能处理突发流量,当有大量请求突然到来时,只能按照固定的速率处理,多余的请求会被拒绝。
- 灵活性较差,不能根据业务需求动态调整处理速率。
六、注意事项
令牌桶算法注意事项
- 令牌过期问题:要注意令牌的过期时间,避免令牌一直留在桶里,导致系统处理过多的请求。
- 并发问题:在高并发场景下,要保证令牌的发放和获取是线程安全的,可以使用 Redis 的原子操作来实现。
漏桶算法注意事项
- 漏桶容量设置:漏桶的容量要根据系统的处理能力和业务需求来设置,太小会导致大量请求被拒绝,太大会占用过多的内存。
- 漏水速率设置:漏水速率要根据系统的处理能力来设置,过快会导致系统负载过高,过慢会导致请求处理不及时。
七、文章总结
令牌桶和漏桶算法都是很实用的限流算法,在不同的场景下有不同的优势。令牌桶算法适合处理突发流量,灵活性高;漏桶算法适合稳定流量控制,能保证流量的平稳。在使用 Redis 实现这两种算法时,要根据业务需求和系统特点选择合适的算法,同时要注意一些实现细节和注意事项,确保系统的稳定运行。
评论