双十一零点扛过10倍流量洪峰:Sentinel与Redis+Lua的分布式限流深度避坑指南

导读:双十一零点,流量瞬间暴涨 10 倍,数据库连接池打满,服务超时,雪崩一触即发......限流不是可选项,是微服务的生命线。今天我们来聊聊,如何在分布式环境下优雅地"丢请求",保核心系统。

在单体时代,限流很简单:Guava RateLimiter、Semaphore 或者漏桶/令牌桶算法,内存里玩得风生水起。但到了微服务、多实例部署的场景,单个节点的限流变成了一本糊涂账------节点 A 限制 100 QPS,节点 B 也限制 100 QPS,但总流量可能冲到 200 QPS,后端依然被打爆。

分布式限流登场:将限流状态集中管理(Redis、Sentinel 集群),所有节点共享同一个"流量额度",真正实现对全局入口的精准控制。

本文将从原理、方案对比、代码实战到避坑点,全面剖析企业级分布式限流的两种主流路子------Redis + Lua阿里 Sentinel

一、从"单机兵"到"集团军":为什么需要分布式限流?

先看一个典型反例:

  • 订单服务部署了 5 个 Pod
  • 单机限流 200 QPS(令牌桶)
  • 总容量 = 5 × 200 = 1000 QPS
  • 外部恶意流量 1500 QPS,分摊到每台 300 QPS
  • 每台都超限,但单机限流各自为政,总流量依然冲垮了下游数据库

分布式限流的核心目标:所有节点共享同一个限流计数器/令牌桶/漏桶状态,实现全局公平或按资源的精细化控制。

二、四大限流算法快速复习(半分钟看懂)

算法 原理 特点 适用场景
计数器 窗口内累加请求数,超阈值拒绝 简单,有"突刺"问题 粗粒度、非敏感场景
滑动窗口 将窗口分多个小格子,滑动计数 平滑,内存占用稍高 通用 API 限流
漏桶 请求入桶,恒定速率流出 强行平滑突发流量 保护下游弱处理能力
令牌桶 以固定速率放令牌,请求拿令牌通行 允许短时突发,灵活 高性能、允许突增场景

分布式限流多采用 滑动窗口令牌桶 的高性能实现。

三、两大主流方案对比:Redis+Lua vs Sentinel

维度 Redis + Lua 阿里 Sentinel(集群流控)
核心原理 Lua 脚本原子性操作 Redis 计数器/令牌桶 基于滑动窗口 + 集群 Token Server
依赖组件 Redis(必须) 可独立,集群模式需 Token Server 或 Redis
性能 单次 Redis 调用 ~0.1ms,超高并发会拉高延迟 本地限流几乎无损耗,集群限流需 RPC 调用
准确性 强一致(Redis 单机/集群保证原子性) 集群模式存在少量误差(Netty 通信延迟)
功能丰富度 需手写算法逻辑 开箱即用:QPS/线程数/冷启动/关联限流/热点参数限流
运维成本 低(Redis 已是标配) 中等(Sentinel 控制台需额外部署)
语言无关性 任何语言都能用 Java 最佳,非 Java 需自研客户端
流量分摊 全局精确 支持按调用来源、任意标识分组

一句话总结

  • Redis+Lua:轻量、通用、灵活,适合 Redis 已有、不想引入新组件的中小型团队。
  • Sentinel:功能全、生态好(Spring Cloud Alibaba 亲儿子),适合 Java 栈、需要复杂流控策略(熔断、热点、系统自适应)的大中型项目。

四、实战一:Redis + Lua 实现分布式令牌桶限流

本实战将实现一个全局限流注解 ,任何方法加上 @RedisRateLimiter 即可保护。

4.1 为什么必须用 Lua 脚本?

因为 Redis 的 INCRGETSET 等命令不是原子组合。令牌桶需要"获取令牌 + 更新剩余令牌"两步,高并发下会出现超卖。Lua 脚本在 Redis 中整体原子执行,完美解决。

4.2 编写 Lua 脚本(token_bucket.lua)

lua 复制代码
-- 令牌桶限流 Lua 脚本
-- KEYS[1] : 桶的唯一 key
-- ARGV[1] : 最大令牌容量 capacity
-- ARGV[2] : 令牌生成速率 rate (每秒几个)
-- ARGV[3] : 当前请求需要的令牌数 (通常为1)
-- ARGV[4] : 当前时间戳(秒)

local key = KEYS[1]
local capacity = tonumber(ARGV[1])
local rate = tonumber(ARGV[2])
local requested = tonumber(ARGV[3])
local now = tonumber(ARGV[4])

-- 获取桶的当前状态 {last_refresh_time, current_tokens}
local bucket = redis.call('hmget', key, 'last_time', 'tokens')
local last_time = tonumber(bucket[1]) or now
local tokens = tonumber(bucket[2]) or capacity

-- 计算应该补充的令牌数
local delta = math.max(0, now - last_time)
local filled_tokens = math.min(capacity, tokens + (delta * rate))

-- 判断是否足够
local allowed = 0
if filled_tokens >= requested then
    allowed = 1
    filled_tokens = filled_tokens - requested
end

-- 保存新状态
redis.call('hmset', key, 'last_time', now, 'tokens', filled_tokens)
-- 设置过期时间,避免闲置 key 浪费内存(2倍时间窗口)
redis.call('expire', key, 60)

return allowed

4.3 Spring Boot 中集成 Redis + Lua

① 引入依赖

xml 复制代码
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-pool2</artifactId>
</dependency>

② 加载 Lua 脚本

java 复制代码
@Component
public class RedisRateLimiter {

    private final RedisScript<Long> rateLimitScript;

    public RedisRateLimiter(RedisTemplate<String, Object> redisTemplate) {
        // 读取 classpath 下的 lua 文件
        DefaultRedisScript<Long> script = new DefaultRedisScript<>();
        script.setScriptSource(new ResourceScriptSource(new ClassPathResource("lua/token_bucket.lua")));
        script.setResultType(Long.class);
        this.rateLimitScript = script;
        this.redisTemplate = redisTemplate;
    }

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    public boolean tryAcquire(String key, long capacity, double rate, int requested) {
        List<String> keys = Collections.singletonList(key);
        Long result = redisTemplate.execute(
            rateLimitScript,
            keys,
            String.valueOf(capacity),
            String.valueOf(rate),
            String.valueOf(requested),
            String.valueOf(System.currentTimeMillis() / 1000)
        );
        return result != null && result == 1L;
    }
}

③ 自定义注解 + AOP 实现无侵入限流

java 复制代码
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface DistributedRateLimit {
    String key();               // 限流 key,支持 SpEL
    long capacity() default 100;
    double rate() default 10;   // 每秒生成令牌数
    int requested() default 1;
}

切面实现(省略部分校验代码):

java 复制代码
@Around("@annotation(rateLimit)")
public Object around(ProceedingJoinPoint joinPoint, DistributedRateLimit rateLimit) throws Throwable {
    String key = parseKey(rateLimit.key(), joinPoint); // 支持 SpEL 解析,如 #userId
    boolean allowed = redisRateLimiter.tryAcquire(key, rateLimit.capacity(), rateLimit.rate(), rateLimit.requested());
    if (!allowed) {
        throw new RateLimitException("请求过于频繁,请稍后再试");
    }
    return joinPoint.proceed();
}

④ 业务处使用

java 复制代码
@GetMapping("/order")
@DistributedRateLimit(key = "order:create:#{#userId}", capacity = 50, rate = 10)
public String createOrder(@RequestParam Long userId) {
    return "订单创建成功";
}

效果 :无论多少个实例,同一个 userId 的请求被全局限制在 10 QPS,且允许短时突发。

五、实战二:阿里 Sentinel 集群流控(更适合生产)

Sentinel 提供两种集群模式:

  • 嵌入模式(Embedded) :选一个节点作为 Token Server,其他为 Client(适合小规模)
  • 独立模式(Alone) :独立 Token Server 集群(适合大规模)

5.1 搭建 Sentinel 控制台

bash 复制代码
docker run -d --name sentinel -p 8858:8858 -p 8719:8719 bladex/sentinel-dashboard:1.8.6

访问 http://localhost:8858,默认账号 sentinel/sentinel。

5.2 Spring Boot 集成 Sentinel

① 依赖

xml 复制代码
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
<dependency>
    <groupId>com.alibaba.csp</groupId>
    <artifactId>sentinel-cluster-client-default</artifactId>
</dependency>
<dependency>
    <groupId>com.alibaba.csp</groupId>
    <artifactId>sentinel-cluster-server-default</artifactId>
</dependency>

② 配置集群流控(独立模式示例)

在 application.yml 中配置应用为 Token Client:

yaml 复制代码
spring:
  cloud:
    sentinel:
      transport:
        dashboard: localhost:8858
      cluster-client:
        server-host: your-token-server-ip   # Token Server 地址
        server-port: 18730
      flow:
        cold-factor: 3

同时在控制台配置集群流控规则(如针对 /order/create 资源的全局 QPS 阈值 = 500)。

③ 代码中无需显式调用,Sentinel 会通过拦截器自动保护 Web 接口。

5.3 Sentinel 比 Redis+Lua 强在哪里?

  • 热点参数限流 :对 userId 维度单独限流(如每个用户 10 QPS),普通 Redis 脚本需要为每个用户维护桶,可能产生海量 key。
  • 熔断降级:错误率超过阈值自动熔断,半开探测恢复。
  • 系统自适应保护:根据 CPU 负载、平均 RT 等自动调整入口流量。
  • 动态规则推送:通过 Nacos/Apollo 实现规则热更新。

六、生产避坑指南(重点!)

6.1 Redis 单点故障与网络开销

  • 问题:每次限流都要请求 Redis,网络 RTT 增加 ~0.5ms,高并发下 Redis 成为瓶颈。

  • 解法

    • 使用 Redis Cluster 做高可用。
    • 本地缓存配合批量模式(如每次请求拿 5 个令牌,消耗完再请求 Redis)。
    • 降级方案:Redis 不可用时切换到单机限流(Guava RateLimiter),并打印告警。

6.2 Sentinel 集群模式的偏差

由于 Token Server 和 Client 之间通信是异步 Netty,存在毫秒级延迟,在严格秒杀场景下可能累计误差。建议:对极端精确场景,使用 Redis+Lua 并压测验证精度。

6.3 限流 Key 的设计与热点问题

  • 不合理:limiter:user(所有用户共享一个桶)→ 一个恶意用户能刷爆全局限流。
  • 合理:limiter:user:{userId}(按用户隔离),但要警惕海量用户时 Redis 内存爆炸。
  • 解决方案:对用户限流可采用滑动窗口 + 布隆过滤器,仅对活跃用户动态创建 key,并设置 TTL。

6.4 限流后用户体验优化

  • 返回明确的限流错误码(如 429)和 Retry-After 头部。
  • 实现分级限流:VIP 用户阈值更高,普通用户阈值更低。
  • 异步排队:对于写请求,限流后可放入队列稍后处理,而不是粗暴拒绝。

七、方案选型速查表

你的情况 推荐方案
Redis 已部署,团队小,需要快速实现全局限流 Redis + Lua
已有 Sentinel 生态(Spring Cloud Alibaba),需要熔断、热点、系统自适应 Sentinel 集群流控
非 Java 栈(Go/Python),统一流量治理 Redis + Lua基于 Envoy 的全局限流(RLS)
超高并发(10万+ QPS),要求极致性能 Netflix Concurrency Limits(极限并发控制)+ 本地滑动窗口,结合 Redis 做周期性同步

八、总结

分布式限流不是单一的算法或组件,而是一套根据业务场景权衡的"流量手术刀"。本文我们从原理切入,对比了 Redis+Lua 和 Sentinel 两种主流方案,并分别给出了完整的 Spring Boot 实战代码,以及生产中那些容易踩的坑。

  • Redis+Lua:造轮子成本低,适合通用、灵活动态控制的场景。
  • Sentinel:功能航母,适合 Java 生态的复杂治理需求。

记住:限流的本质是延迟拒绝,而不是系统崩溃。优雅地丢请求,比让用户等到超时更尊重用户体验。

最后,无论你选择哪条路,压测和监控永远是最好的老师。希望这篇文章能帮你构建起你的第一条"分布式护城河"。


📢 关注 《卷毛的技术笔记》 ,专注后端硬核技术分享,拒绝套路,只聊落地的技术。

如果觉得文章对你有帮助,欢迎点赞、收藏、关注!

相关推荐
逻辑驱动的ken1 小时前
Java高频面试考点场景题27
java·开发语言·面试·职场和发展·求职招聘
北风朝向1 小时前
springboot使用@Validated校验List接口参数
spring boot·后端·list·校验·valid
Volunteer Technology1 小时前
Hadoop Federation 联邦
大数据·hadoop·分布式
万少1 小时前
公测期 0 元/月!商汤 SenseNova 免费 Token 再不领就没了
前端·javascript·后端
一氧化二氢.h1 小时前
【java】的数组列表和集合的区别是什么
java·开发语言
PersistJiao1 小时前
开发环境对比:VS Code、Cursor、IntelliJ IDEA
java·ide·intellij-idea
科研小白_1 小时前
【第二期:MATLAB点云处理基础】KD树与点云邻域搜索
java·前端·人工智能
小江的记录本1 小时前
【MySQL】《MySQL基础架构 面试核心考点问答清单》
前端·数据库·后端·sql·mysql·adb·面试
Don.TIk1 小时前
天机の学堂
java·spring boot·spring·maven·mybatis