Redis+Lua实现分布式限流器

栏目: Lua · 发布时间: 5年前

内容简介:主要是依靠 redis + lua 来实现限流器, 使用 lua 的原因是将多条命令合并在一起作为一个原子操作, 无需过多考虑并发.计数器算法是指在一段窗口时间内允许通过的固定数量的请求, 比如10次/秒, 500次/30秒.如果设置的时间粒度越细, 那么限流会更平滑.

LastModified: 2019年6月14日10:37:39

主要是依靠 redis + lua 来实现限流器, 使用 lua 的原因是将多条命令合并在一起作为一个原子操作, 无需过多考虑并发.

计数器模式

原理

计数器算法是指在一段窗口时间内允许通过的固定数量的请求, 比如10次/秒, 500次/30秒.

如果设置的时间粒度越细, 那么限流会更平滑.

实现

所使用的 Lua 脚本

-- 计数器限流
-- 此处支持的最小单位时间是秒, 若将 expire 改成 pexpire 则可支持毫秒粒度.
-- KEYS[1]  string  限流的key
-- ARGV[1]  int     限流数
-- ARGV[2]  int     单位时间(秒)

local cnt = tonumber(redis.call("incr", KEYS[1]))

if (cnt == 1) then
    -- cnt 值为1说明之前不存在该值, 因此需要设置其过期时间
    redis.call("expire", KEYS[1], tonumber(ARGV[2]))
elseif (cnt > tonumber(ARGV[1])) then
    return -1
end 

return cnt

返回 -1 表示超过限流, 否则返回当前单位时间已通过的请求数

key 可以但不限于以下的情况

  • ip + 接口
  • user_id + 接口

优点

  • 实现简单

缺点

  • 粒度不够细的情况下, 会出现在同一个窗口时间内出现双倍请求数

注意

  • 尽量保持时间粒度精细

场景分析

eg. 1000/3s 的限流

极端情况1:

第1秒请求数 10

第2秒请求数 10

第3秒请求数 980

第4秒请求数 900

第5秒请求数 100

第6秒请求数 0

此时注意第3~5秒内的总请求数高达 1980

极端情况2:

第1秒请求数 1000

第2秒请求数 0

第3秒请求数 0

此时后续的第2~3秒会出现大量拒绝请求

令牌桶模式

原理

Redis+Lua实现分布式限流器

令牌桶的

  1. 桶中保存有令牌, 存在上限, 且一开始是满的
  2. 每次请求都要消耗令牌(可根据不同请求消耗不同数量的令牌)
  3. 每隔一段时间(固定速率)会往桶中放令牌

桶的实现还分为:

  • 可预消费

    提前预支令牌数: 前人挖坑, 后人跳

  • 不可预消费

    令牌数不够直接拒绝

实现

此处实现的不可预消费的令牌桶, 具体Lua代码:

-- 令牌桶限流: 不支持预消费, 初始桶是满的
-- KEYS[1]  string  限流的key

-- ARGV[1]  int     桶最大容量
-- ARGV[2]  int     每次添加令牌数
-- ARGV[3]  int     令牌添加间隔(秒)
-- ARGV[4]  int     当前时间戳

local bucket_capacity = tonumber(ARGV[1])
local add_token = tonumber(ARGV[2])
local add_interval = tonumber(ARGV[3])
local now = tonumber(ARGV[4])

-- 保存上一次更新桶的时间的key
local LAST_TIME_KEY = KEYS[1].."_time";         
-- 获取当前桶中令牌数
local token_cnt = redis.call("get", KEYS[1])    
-- 桶完全恢复需要的最大时长
local reset_time = math.ceil(bucket_capacity / add_token) * add_interval;

if token_cnt then   -- 令牌桶存在
    -- 上一次更新桶的时间
    local last_time = redis.call('get', LAST_TIME_KEY)
    -- 恢复倍数
    local multiple = math.floor((now - last_time) / add_interval)
    -- 恢复令牌数
    local recovery_cnt = multiple * add_token
    -- 确保不超过桶容量
    local token_cnt = math.min(bucket_capacity, token_cnt + recovery_cnt) - 1
    
    if token_cnt < 0 then
        return -1;
    end
    
    -- 重新设置过期时间, 避免key过期
    redis.call('set', KEYS[1], token_cnt, 'EX', reset_time)                     
    redis.call('set', LAST_TIME_KEY, last_time + multiple * add_interval, 'EX', reset_time)
    return token_cnt
    
else    -- 令牌桶不存在
    token_cnt = bucket_capacity - 1
    -- 设置过期时间避免key一直存在
    redis.call('set', KEYS[1], token_cnt, 'EX', reset_time);
    redis.call('set', LAST_TIME_KEY, now, 'EX', reset_time + 1);    
    return token_cnt    
end

令牌桶的关键是以下几个参数:

  • 桶最大容量
  • 每次放入的令牌数
  • 放入令牌的间隔时间

令牌桶的实现不会出现计数器模式中单位时间内双倍流量的问题.


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Agile Web Application Development with Yii 1.1 and PHP5

Agile Web Application Development with Yii 1.1 and PHP5

Jeffrey Winesett / Packt Publishing / 2010-08-27

In order to understand the framework in the context of a real-world application, we need to build something that will more closely resemble the types of applications web developers actually have to bu......一起来看看 《Agile Web Application Development with Yii 1.1 and PHP5》 这本书的介绍吧!

JSON 在线解析
JSON 在线解析

在线 JSON 格式化工具

在线进制转换器
在线进制转换器

各进制数互转换器

HSV CMYK 转换工具
HSV CMYK 转换工具

HSV CMYK互换工具