使用 Redis 高效实现 API 网关与服务的请求限流

在微服务架构中,对 API 网关或单个服务的请求进行速率限制至关重要,以防止恶意攻击、资源滥用并确保系统的稳定性和可用性。 Redis 凭借其高性能、原子操作和丰富的数据结构,成为实现请求限流的理想选择。

本文将详细探讨如何利用 Redis 实现 API 网关或单个服务的请求限流,深入分析各种主流算法,并提供在 Python 和 Java 环境下的具体代码示例,包括使用 Lua 脚本确保操作的原子性。

为什么选择 Redis 进行限流?

Redis 之所以成为实现限流的热门选择,主要得益于其以下几个关键特性:

  • 卓越的性能: Redis 是一款内存数据库,能够提供极快的读写速度,这对于需要实时处理大量请求的限流场景至关重要。
  • 原子操作: Redis 的 INCREXPIRE 等命令是原子性的,可以避免在并发请求下出现竞态条件,确保计数和过期的准确性。
  • 丰富的数据结构: Redis 提供了字符串、哈希、列表、有序集合等多种数据结构,可以灵活地实现各种复杂的限流算法。
  • 可扩展性: Redis 支持集群和横向扩展,能够应对高并发的请求环境。
  • 自带过期机制: 通过 EXPIRE 命令可以为键设置生存时间(TTL),轻松实现时间窗口的自动重置。

核心概念:识别请求来源

在实施限流之前,首先需要确定如何识别和区分不同的请求来源。常见的识别方式包括:

  • IP 地址: 基于客户端的 IP 地址进行限制,是简单有效的常用方法。
  • 用户 ID 或 API 密钥: 针对已认证的用户或第三方应用进行精细化限流。
  • 设备 ID: 对移动端等特定设备进行限制。

主流限流算法及其 Redis 实现

以下是几种主流的限流算法及其使用 Redis 的实现方式:

1. 固定窗口计数器 (Fixed Window Counter)

这是最简单的限流算法。它在固定的时间窗口内(例如,每分钟)统计请求次数,如果超过预设的阈值,则拒绝后续的请求,直到下一个时间窗口开始。

优点: 实现简单,容易理解。

缺点: 在时间窗口的边界处可能会出现“突刺”流量问题。例如,在窗口结束前的瞬间和新窗口开始的瞬间,可能会有两倍于限制的请求通过。

Redis 实现:

主要利用 INCREXPIRE 命令。

  • Python 示例:

    import redis
    import timer = redis.Redis()def is_rate_limited_fixed_window(user_id: str, limit: int, window: int) -> bool:key = f"rate_limit:{user_id}"current_requests = r.get(key)if current_requests is None:# 使用 pipeline 保证原子性pipe = r.pipeline()pipe.incr(key)pipe.expire(key, window)pipe.execute()return Falseif int(current_requests) >= limit:return Truer.incr(key)return False# 示例: 每位用户每60秒最多10个请求
    for i in range(15):if is_rate_limited_fixed_window("user123", 10, 60):print("请求被限制")else:print("请求成功")time.sleep(1)
    
  • 使用 Lua 脚本保证原子性:

    为了避免 GETINCR 之间的竞态条件,强烈建议使用 Lua 脚本将多个命令作为一个原子操作执行。

    -- rate_limiter.lua
    local key = KEYS[1]
    local limit = tonumber(ARGV[1])
    local window = tonumber(ARGV[2])local current = tonumber(redis.call("GET", key) or "0")if current >= limit thenreturn 1 -- 1 表示被限制
    endif current == 0 thenredis.call("INCR", key)redis.call("EXPIRE", key, window)
    elseredis.call("INCR", key)
    endreturn 0 -- 0 表示未被限制
    
    • Java (Jedis) 调用 Lua 脚本示例:
      import redis.clients.jedis.Jedis;public class RateLimiter {private final Jedis jedis;private final String script;public RateLimiter(Jedis jedis) {this.jedis = jedis;// 实际项目中应从文件加载this.script = "local key = KEYS[1]..."}public boolean isRateLimited(String key, int limit, int window) {Object result = jedis.eval(this.script, 1, key, String.valueOf(limit), String.valueOf(window));return "1".equals(result.toString());}
      }
      
2. 滑动窗口日志 (Sliding Window Log)

该算法记录每个请求的时间戳。当一个新请求到达时,会移除时间窗口之外的旧时间戳,然后统计窗口内的时间戳数量。如果数量超过限制,则拒绝请求。

优点: 限流精度高,有效解决了固定窗口的边界问题。

缺点: 需要存储所有请求的时间戳,当请求量很大时会占用较多内存。

Redis 实现:

使用有序集合 (Sorted Set),将成员 (member) 设置为唯一值(如请求 ID 或时间戳),将分数 (score) 设置为请求的时间戳。

  • Python 示例:

    import redis
    import timer = redis.Redis()def is_rate_limited_sliding_log(user_id: str, limit: int, window: int) -> bool:key = f"rate_limit_log:{user_id}"now = int(time.time() * 1000)window_start = now - window * 1000# 使用 pipeline 保证原子性pipe = r.pipeline()# 移除窗口外的数据pipe.zremrangebyscore(key, 0, window_start)# 添加当前请求pipe.zadd(key, {f"{now}:{int(time.time()*1000000)}": now}) # 保证 member 唯一# 获取窗口内的请求数pipe.zcard(key)# 设置过期时间,防止冷数据占用内存pipe.expire(key, window)results = pipe.execute()current_requests = results[2]return current_requests > limit
    
  • 使用 Lua 脚本实现滑动窗口:

    -- sliding_window.lua
    local key = KEYS[1]
    local limit = tonumber(ARGV[1])
    local window = tonumber(ARGV[2])
    local now = redis.call("TIME")
    local now_ms = (now[1] * 1000) + math.floor(now[2] / 1000)
    local window_start = now_ms - window * 1000-- 移除过期的请求记录
    redis.call("ZREMRANGEBYSCORE", key, 0, window_start)-- 获取当前窗口内的请求数
    local count = redis.call("ZCARD", key)if count >= limit thenreturn 1 -- 被限制
    end-- 添加当前请求
    redis.call("ZADD", key, now_ms, now_ms .. "-" .. count) -- member 保证唯一性
    redis.call("EXPIRE", key, window)return 0 -- 未被限制
    
3. 令牌桶算法 (Token Bucket)

该算法的核心是一个固定容量的“令牌桶”,系统会以恒定的速率向桶里放入令牌。每个请求需要从桶里获取一个令牌才能被处理。如果桶里没有令牌,请求将被拒绝或排队等待。

优点: 能够应对突发流量,只要桶内有足够的令牌,就可以一次性处理多个请求。

缺点: 实现相对复杂,需要一个独立的进程或定时任务来持续生成令牌。

Redis 实现:

可以使用 Redis 的列表 (List) 作为令牌桶。一个后台任务(或在每次请求时计算)负责向列表中添加令牌。

  • Java (Spring Boot with Bucket4j) 示例:

    Bucket4j 是一个流行的 Java 限流库,可以与 Redis 结合使用实现分布式令牌桶。

    // 依赖: bucket4j-core, jcache-api, redisson
    // 配置 RedissonClient...// 创建一个基于 Redis 的代理管理器
    ProxyManager<String> proxyManager = new JCacheProxyManager<>(jcache);// 定义令牌桶配置:每分钟补充10个令牌,桶容量为10
    BucketConfiguration configuration = BucketConfiguration.builder().addLimit(Bandwidth.simple(10, Duration.ofMinutes(1))).build();// 获取或创建令牌桶
    Bucket bucket = proxyManager.getProxy("rate-limit-bucket:user123", configuration);// 尝试消费一个令牌
    if (bucket.tryConsume(1)) {// 请求成功
    } else {// 请求被限制
    }
    

    Spring Cloud Gateway 也内置了基于 Redis 的令牌桶算法限流器。

4. 漏桶算法 (Leaky Bucket)

漏桶算法将请求看作是流入“漏桶”的水,而漏桶以固定的速率漏出水(处理请求)。如果流入的速率过快,导致桶溢出,则多余的请求将被丢弃。

优点: 能够平滑请求流量,保证服务以恒定的速率处理请求。

缺点: 无法有效利用系统空闲资源来处理突发流量。

Redis 实现:

Redis-Cell 模块提供了基于 GCRA (Generic Cell Rate Algorithm) 的漏桶算法实现。

API 网关限流 vs. 单个服务限流

限流逻辑可以部署在不同的层面,主要分为 API 网关层和单个微服务层。

API 网关限流

在 API 网关层面进行统一限流是一种常见的做法。

  • 优点:

    • 集中管理: 可以在一个地方统一配置和管理所有服务的限流规则。
    • 保护后端服务: 将恶意或超额流量挡在微服务集群之外,保护整个系统的稳定性。
  • 实现方式:

    • 利用网关自带功能: 像 Spring Cloud Gateway、Kong、KrakenD 等 API 网关都提供了基于 Redis 的限流插件或模块。
    • 自定义中间件: 在网关中编写自定义的中间件或过滤器,嵌入上述的 Redis 限流逻辑。
  • Spring Cloud Gateway 示例 (application.yml):

    spring:cloud:gateway:routes:- id: my_routeuri: lb://my-servicepredicates:- Path=/my-api/**filters:- name: RequestRateLimiterargs:redis-rate-limiter.replenishRate: 10      # 令牌桶每秒填充速率redis-rate-limiter.burstCapacity: 20     # 令牌桶容量redis-rate-limiter.requestedTokens: 1  # 每次请求消耗的令牌数key-resolver: "#{@ipKeyResolver}"     # 使用 IP 地址作为限流的 key
    
单个服务限流

将限流逻辑直接实现在单个微服务内部。

  • 优点:

    • 灵活性: 每个服务可以根据自身的负载能力和业务需求,定制更精细化的限流策略。
    • 独立性: 不依赖于特定的 API 网关。
  • 实现方式:

    • 通过 AOP (面向切面编程) 创建注解,对需要限流的 Controller 方法进行拦截。
    • 在服务的入口处,如拦截器或过滤器中,调用 Redis 进行限流判断。
  • Python (FastAPI 中间件) 示例:

    from fastapi import FastAPI, Request
    from fastapi.responses import JSONResponse
    import redisapp = FastAPI()
    r = redis.Redis()@app.middleware("http")
    async def rate_limit_middleware(request: Request, call_next):# 简单示例:使用固定窗口计数器ip = request.client.hostkey = f"rate_limit:{ip}"limit = 5window = 60# 此处应使用 Lua 脚本保证原子性current = r.incr(key)if current == 1:r.expire(key, window)if current > limit:return JSONResponse(status_code=429, content={"message": "Too Many Requests"})response = await call_next(request)return response
    

总结

使用 Redis 实现请求限流是一种高效且可扩展的方案。开发者应根据具体的业务场景和需求选择合适的限流算法。

  • 对于简单的限流需求,固定窗口计数器是一个不错的起点。
  • 为了更精确地控制流量并避免边界问题,滑动窗口日志滑动窗口计数器是更好的选择。
  • 如果需要应对突发流量,令牌桶算法则非常适用。

在实施时,务必使用 Lua 脚本来保证操作的原子性,避免在并发环境下出现数据不一致的问题。将限流逻辑部署在 API 网关层可以对整个系统起到保护作用,而部署在单个服务层则提供了更高的灵活性。在实际应用中,两者也常常结合使用,构建多层级的防护体系。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/web/91386.shtml
繁体地址,请注明出处:http://hk.pswp.cn/web/91386.shtml
英文地址,请注明出处:http://en.pswp.cn/web/91386.shtml

如若内容造成侵权/违法违规/事实不符,请联系英文站点网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

图片查重从设计到实现(7) :使用 Milvus 实现高效图片查重功能

使用 Milvus 实现高效图片查重功能本文将介绍如何利用 Milvus 向量数据库构建一个高效的图片查重系统&#xff0c;通过传入图片就能快速从已有数据中找出匹配度高的相似图片。一.什么是图片查重&#xff1f; 图片查重指的是通过算法识别出内容相同或高度相似的图片&#xff0c;…

诱导多能干细胞(iPSC)的自述

自十七年前诱导多能干细胞&#xff08;也称iPS细胞或iPSC&#xff09;技术出现以来&#xff0c;干细胞生物学和再生医学取得了巨大进展。人类iPSC已广泛用于疾病建模、药物发现和细胞疗法开发。新的病理机制已被阐明&#xff0c;源自iPSC筛选的新药正在研发中&#xff0c;并且首…

基于深度学习的医学图像分析:使用DeepLabv3+实现医学图像分割

前言 医学图像分析是计算机视觉领域中的一个重要应用&#xff0c;特别是在医学图像分割任务中&#xff0c;深度学习技术已经取得了显著的进展。医学图像分割是指从医学图像中识别和分割出特定的组织或器官&#xff0c;这对于疾病的诊断和治疗具有重要意义。近年来&#xff0c;D…

Lombok 字段魔法:用 @FieldDefaults 解锁“隐身+锁死”双重特效

前言 项目里总有这样一种神秘现象:明明只是几个字段,却堆满 private final,每次都得机械敲上一遍。有的同事一边敲一边默念“代码规范不能丢”,表情严肃得像在写遗嘱。可惜,规范虽好,手指遭殃。 于是,Lombok 悄然登场,肩扛简洁大旗,手握注解神器,@FieldDefaults 正…

小白如何自学网络安全,零基础入门到精通,看这一篇就够了!

小白如何自学网络安全&#xff0c;零基础入门到精通&#xff0c;看这一篇就够了&#xff01; 小白人群想学网安但是不知道从哪入手&#xff1f;一篇文章告诉你如何在4个月内吃透网安课程&#xff0c;掌握网安技术 一、基础阶段 1.了解网安相关基础知识 了解中华人民共和国网…

前端 vue 第三方工具包详解-小白版

恭喜你迈入Vue世界&#xff01;&#x1f604; 对于前端小白&#xff0c;掌握这些常用第三方包能极大提升开发效率和项目质量。以下是Vue生态中必备的第三方包及小白友好式用法解析&#xff1a;&#x1f9f1; 一、基础工具包&#xff08;每个项目必装&#xff09; 1. Vue Router…

解决mac下git pull、push需要输入密码

解决方法&#xff1a; 1.强制配置 SSH 自动加载钥匙串 编辑 SSH 配置文件 vi ~/.ssh/configHost *AddKeysToAgent yes # 自动将密钥添加到 ssh-agentUseKeychain yes # 明确使用钥匙串存储密码IdentityFile ~/.ssh/id_rsa # 替换为你的私钥路径2.修复 Sh…

内存网格、KV存储和Redis的概念、使用场景及异同

基本概念 内存网格 (In-Memory Data Grid - IMDG) 内存网格是一种分布式内存数据存储技术&#xff0c;具有以下特点&#xff1a;分布式架构 数据跨多个服务器节点分布存储提供线性扩展能力内存优先 主要数据存储在内存中&#xff0c;提供微秒级访问延迟支持持久化作为备份企业级…

【C++算法】87.BFS解决最短路径问题_为高尔夫比赛砍树

文章目录题目链接&#xff1a;题目描述&#xff1a;解法C 算法代码&#xff1a;题目链接&#xff1a; 675. 为高尔夫比赛砍树 题目描述&#xff1a; 解法 注意&#xff1a;砍树要从低到高砍。 砍掉1&#xff0c;从1到5到2 砍掉2&#xff0c;从2到5到3 砍掉3&#xff0c;从3到5…

JavaScript内存管理完全指南:从入门到精通

文章目录JavaScript内存管理完全指南&#xff1a;从入门到精通1. 哪些数据类型属于引用类型&#xff08;复杂数据类型&#xff09;&#xff1f;2. 为什么引用类型要存储在堆中&#xff1f;3. 引用类型的内存存储示例示例 1&#xff1a;对象&#xff08;Object&#xff09;示例 …

Linux网络-------3.应⽤层协议HTTP

1.HTTP协议 虽然我们说,应⽤层协议是我们程序猿⾃⼰定的.但实际上,已经有⼤佬们定义了⼀些现成的,⼜⾮常好⽤的应⽤层协议,供我们直接参考使⽤.HTTP(超⽂本传输协议)就是其中之⼀。 在互联⽹世界中&#xff0c;HTTP&#xff08;HyperText Transfer Protocol&#xff0c;超⽂本…

05 GWAS表型数据处理原理

表型数据处理 • 质量性状 – 二分类&#xff1a;可用0 / 1, 1 / 2 数值表示 – 多分类&#xff1a;哑变量赋值&#xff0c;0/1 • 数量性状 – 尽量符合正太分布 – 剔除异常表型值样本 – 多年多点重复观测 – 对于阈值性状&#xff0c;分级数量化或哑变量赋值 R中 shapiro.t…

【Cpolar实现内网穿透】

Cpolar实现内网穿透业务需求第一步&#xff1a;准备工作1、关闭安全软件2、下载所需软件第二步&#xff1a;Nginx的配置第三步&#xff1a;使用cpolar实现内网穿透1、进入 https://dashboard.cpolar.com/get-started 注册&#xff0c;登录&#xff0c;完成身份证的实名认证2、下…

基于 JavaWeb+MySQL 的学院党费缴费系统

基于 JavaWeb 的学院党费缴费系统第 1 章绪论1.1 项目背景当今互联网发展及其迅速&#xff0c;互联网的便利性已经遍及到各行各业&#xff0c;惠及到每一个人&#xff0c;传统的缴费方式都需要每个人前往缴费点陆续排队缴费&#xff0c;不仅浪费大量了个人时间&#xff0c;而且…

LCGL基本使用

LVGC简介 light video Graphics Library (1)纯c与语言编程,将面向对象的思想植入c语言。 (2)轻量化图形库资源,人机交互效果好,在(ios Android QT)移植性较好,但是这些平台对硬件要求较高 lcgc工程搭建 工程源码的获取 获取工程结构 https://github.com/lvgl/lv_po…

嵌入式第十六课!!!结构体与共用体

一、结构体结构体是一种数据类型&#xff0c;它的形式是这样的&#xff1a;struct 结构体名{ 结构体成员语句1&#xff1b;结构体成员语句2&#xff1b;结构体成员语句3&#xff1b;}&#xff1b;举个例子&#xff1a;struct Student {int id;char name[20];float score…

java web 实现简单下载功能

java web 实现简单下载功能 项目结构├── src\ │ ├── a.txt │ └── com\ │ └── demo\ │ └── web\ │ ├── Cookie\ │ ├── download\ │ ├── homework\ │ ├── serv…

虚幻基础:模型穿模

能帮到你的话&#xff0c;就给个赞吧 &#x1f618; 文章目录模型穿模模型之间的阻挡是否正确设置模型是角色的组件&#xff1a;角色的组件不会与场景中其他的物体发生阻挡但可以发生重叠模型穿模 模型之间的阻挡是否正确设置 模型是角色的组件&#xff1a;角色的组件不会与场…

【Linux】linux基础开发工具(二) 编译器gcc/g++、动静态库感性认识、自动化构建-make/Makefile

文章目录一、gcc/g介绍二、gcc编译选项预处理编译汇编链接三个细节三、动静态库感性认识动静态库的优缺点四、自动化构建-make/Makefile背景知识初步上手Makefilemakefile的推导过程makefile语法一、gcc/g介绍 我们之前介绍了编辑器vim&#xff0c;可以让我们在linux上linux系统…

CentOS 7 上使用 Docker 安装 Jenkins 完整教程

目录 前言 准备工作 系统要求 检查系统信息 更新系统 安装Docker 第一步:卸载旧版本Docker(如果存在) 第二步:安装必要的软件包 第三步:添加Docker官方仓库 第四步:安装Docker CE 第五步:启动Docker服务 第六步:验证Docker安装 第七步:配置Docker用户权限…