惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

博客园 - 叶小钗
O
OpenAI News
V
V2EX
大猫的无限游戏
大猫的无限游戏
博客园 - 聂微东
S
Schneier on Security
C
CXSECURITY Database RSS Feed - CXSecurity.com
小众软件
小众软件
L
LINUX DO - 热门话题
C
Cybersecurity and Infrastructure Security Agency CISA
博客园 - Franky
Security Latest
Security Latest
S
SegmentFault 最新的问题
Project Zero
Project Zero
Spread Privacy
Spread Privacy
K
Kaspersky official blog
J
Java Code Geeks
V
Vulnerabilities – Threatpost
C
Cisco Blogs
C
CERT Recently Published Vulnerability Notes
月光博客
月光博客
T
The Exploit Database - CXSecurity.com
L
Lohrmann on Cybersecurity
人人都是产品经理
人人都是产品经理
博客园 - 三生石上(FineUI控件)
Scott Helme
Scott Helme
WordPress大学
WordPress大学
量子位
T
Threat Research - Cisco Blogs
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
宝玉的分享
宝玉的分享
Hugging Face - Blog
Hugging Face - Blog
AWS News Blog
AWS News Blog
Help Net Security
Help Net Security
Application and Cybersecurity Blog
Application and Cybersecurity Blog
Simon Willison's Weblog
Simon Willison's Weblog
S
Secure Thoughts
博客园 - 【当耐特】
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
V
Visual Studio Blog
Last Week in AI
Last Week in AI
T
Tailwind CSS Blog
腾讯CDC
Cyberwarzone
Cyberwarzone
IT之家
IT之家
GbyAI
GbyAI
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
云风的 BLOG
云风的 BLOG
T
Troy Hunt's Blog
D
Docker

博客园 - koushr

hls k8s常用命令 k8s 1.18.0安装 mysql json函数 group by的列、where的列的有效性 go基础第六篇:sync包 doris脚本 es es语法 veo ride nacos 枫叶互动 极光矩阵 富途 移卡科技 nginx高可用 k8s第一篇:k8s集群架构组件 架构设计第二篇:支付模块架构设计 可观测性体系设计第一篇:可观测性体系基本概念 架构设计第一篇:点赞模块功能设计 ES高级第一篇:倒排索引 山海星辰 python第一篇:基础语法 pulsar基础第二篇:命令行 pulsar基础第一篇:pulsar安装及基本概念 clickhouse第三篇:安装 clickhouse第二篇:MergeTree引擎 clickhouse第一篇:引擎 http2.0 gin入参多次获取 docker第二篇:docker安装常用中间件
redis高阶第一篇:令牌桶算法限流
koushr · 2024-09-15 · via 博客园 - koushr

令牌桶算法核心思想是:

1、以恒定速率向桶中添加令牌

2、桶中令牌数有上限,为桶的容量,超过容量后令牌会直接丢弃

3、请求到来时会尝试从桶中取一个令牌,取成功则通过,服务端会处理此请求,否则拒绝。

token_bucket.lua,内容如下:

redis.replicate_commands()

local tokens_key = KEYS[1]
local timestamp_key = KEYS[2]

local rate = tonumber(ARGV[1])
local capacity = tonumber(ARGV[2])
local now = tonumber(ARGV[3]) or redis.call('TIME')[1]
local requested = tonumber(ARGV[4])

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

local last_tokens = tonumber(redis.call("get", tokens_key)) or capacity
local last_refreshed = tonumber(redis.call("get", timestamp_key)) or 0

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 = allowed and filled_tokens - requested or filled_tokens

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

return { allowed and 1 or 0, new_tokens }

上面lua脚本拷贝自spring-cloud-gateway-server-xxx.jar包中META-INF目录中scripts目录中的request_rate_limiter.lua文件。

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-gateway-server</artifactId>
    <version>4.3.0</version>
</dependency>

在3.0.0版本中,还可以看到可以通过redis.log(redis.LOG_WARNING, "tokens_key " .. tokens_key)在redis中打印日志。

上面lua脚本的解释:

KEYS[1]是存放令牌数的key,KEYS[2]是存放请求时间的key

ARGV[1]是往桶中放令牌的速率,单位是个/秒,如值为5,表示每秒往桶中放5个令牌。ARGV[2]是桶的容量,ARGV[4]是当前请求要使用多少个令牌,一般都是1个。

allowed_num是请求是否可以被处理的标识,值为1表示请求可以被处理,值为0表示请求数已超过阈值,应该被拦截。

定义判断是否允许请求通过的方法,如果服务端是golang开发,则如下:

func getKeys(id, routeId string) []string {
    prefix := fmt.Sprintf("request_rate_limiter.{%v.%v}.", routeId, id)
    tokenKey := fmt.Sprintf("%vtokens", prefix)
    timestampKey := fmt.Sprintf("%vtimestamp", prefix)
    return []string{tokenKey, timestampKey}
}

func isAllowed(userId, routeId string, capacity, rate, tokens int) (bool, error) {
    result, err := rdb.Eval(ctx, script, getKeys(userId, routeId), rate, capacity, "", tokens).Result()
    if err != nil {
        return false, err
    }

    arr, _ := result.([]interface{})
    a, _ := arr[0].(int64)
    return a == 1, nil
}

这串代码是根据org.springframework.cloud.gateway.filter.ratelimit.RedisRateLimiter中的getKeys()方法和isAllowed()方法翻译过来的。

调用上述函数代码示例:

func main() {
    for i := 0; i < 10000000; i++ {
        allowed, err := isAllowed("12345", "/feed/v1/channels", 10, 5, 1) // capacity=10, rate=5 tokens/sec, 取1个
        if err != nil {
            fmt.Println("Error:", err)
            return
        }
        if allowed {
            fmt.Println(time.Now().Format(time.DateTime), "Request allowed")
        } else {
            fmt.Println(time.Now().Format(time.DateTime), "Rate limit exceeded")
        }
    }
}