ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

JCSprout 限流算法详解:漏桶算法与令牌桶算法原理及 Java 实战

JCSprout 限流算法详解:漏桶算法与令牌桶算法原理及 Java 实战 文档教程后端【免费下载链接】JCSprout‍ Java Core Sprout : basic, concurrent, algorithm项目地址https://gitcode.com/gh_mirrors/jc/JCSprout点击查看免费下载限流Rate Limiting是应对高并发大流量的经典手段其核心目标并非提升吞吐而是在流量洪峰到来时保证核心应用的可用性让系统活下来。本文以 JCSprout 仓库中的限流算法文档为主体系统讲解漏桶算法与令牌桶算法两种主流方案的原理、适用场景与取舍并结合 GuavaRateLimiter的实战代码与仓库中的分布式限流方案带你完成从单机限流到分布式限流的完整认知闭环。为什么需要限流在高并发场景下系统的处理能力存在上限数据库连接数、线程池大小、下游服务的吞吐……当请求量远超系统承载能力时排队、超时、雪崩随之而来。限流的意义在于在请求进入系统核心处理逻辑之前主动将流量控制在可承受的范围内超出部分快速失败或排队从而保证应用至少可用。限流通常有以下两种经典方案漏桶算法Leaky Bucket令牌桶算法Token Bucket二者的核心差异在于漏桶算法强制匀速放行流量令牌桶算法则允许一定程度的突发流量。下面分别展开。漏桶算法强制匀速简单但粗暴算法原理漏桶算法的思想非常直观将流量看作水流先全部注入一个桶中桶的底部有一个固定速率的漏口水流按照恒定速率流出桶外。当流量进入速率超过流出速率时桶中的水待处理请求会积累桶有容量上限一旦溢出超出的流量请求直接丢弃。也就是说无论上游的流量如何波动经过漏桶后输出的流量永远是恒定的速率溢出部分的流量只能被抛弃。优缺点分析维度说明优点实现极其简单输出速率恒定能对下游提供非常平滑、稳定的流量缺点非常粗暴无法应对突发的大流量这里无法应对突发流量包含两层含义一是桶满后新来的突发请求会被直接丢弃二是即便桶未满突发流量进入桶后也只会以恒定速率流出突发请求依然要排队等待无法被快速消化。正因为漏桶对突发流量不友好当业务存在短时脉冲式请求且希望快速处理时就需要引入下一节的令牌桶算法。令牌桶算法支持突发兼顾平滑算法原理令牌桶算法的思想与漏桶相反按照恒定的速率向桶中放入令牌令牌积累在桶中桶有容量上限。每当一个请求经过时需要消耗一个或多个令牌才能被放行当桶中的令牌为 0 时请求则会被阻塞或快速失败。因此令牌桶的限速体现在放令牌的速率上而非强制输出速率上如果请求速率长期高于放令牌速率令牌会被耗尽请求被阻塞长期平均速率被限制在放令牌速率附近但如果一段时间没有请求桶中会积累满桶令牌此时一批突发请求到来可以一次性消耗积攒的令牌快速通过——这正是漏桶算法做不到的。先消费后付款令牌桶的透支机制note 令牌桶算法支持先消费后付款比如一个请求可以获取多个甚至全部的令牌但是需要后面的请求付费。也就是说后面的请求需要等到桶中的令牌补齐之后才能继续获取。这段先消费后付款描述的是令牌桶的透支透支额度行为单个请求可以一次性消耗多个甚至桶中全部令牌即允许请求借未来的令牌额度代价是后续请求必须等待直到桶中的令牌被重新补齐后才能继续获取。这在 GuavaRateLimiter的acquire()实现中体现为acquire(n)如果所需令牌数超过当前可用令牌会计算一个等待时长并阻塞当前线程等待时间本质上是为前面透支的令牌付款。漏桶 vs 令牌桶如何选择对比项漏桶算法令牌桶算法输出流量强制恒定速率允许突发受桶容量与令牌积累限制流量控制位置控制流出速率控制令牌生成速率突发流量不支持溢出即丢弃支持短时突发实现复杂度简单略复杂需维护令牌数与上次补充时间典型应用需要严格平滑输出的场景如保护带宽大多数 API/接口限流如 Guava RateLimiter、Nginx limit_req 等令牌桶算法实战基于 Guava RateLimiterJCSprout 仓库的 pom.xml 中引入了 Guava 22.0com.google.guava:guava:22.0而 Guava 的RateLimiter正是令牌桶算法的典型实现。原文档中给出的实战代码位于一个通过 Feign 批量调用远程服务的接口方法内下面保留原代码并补充详细注释Override public BaseResponseUserResVO getUserByFeignBatch(RequestBody UserReqVO userReqVO) { // 调用远程服务 OrderNoReqVO vo new OrderNoReqVO() ; vo.setReqNo(userReqVO.getReqNo()); // 创建一个限流器令牌生成速率为每秒 2 个即平均每秒最多放行 2 个请求 RateLimiter limiter RateLimiter.create(2.0) ; // 批量调用 for (int i 0 ;i 10 ; i){ // 尝试获取一个令牌若当前令牌不足acquire() 会阻塞当前线程直到令牌补齐 // 返回值是本次等待的秒数已耗尽的等待时间可能为 0 double acquire limiter.acquire(); logger.debug(获取令牌成功!,消耗 acquire); BaseResponseOrderNoResVO orderNo orderServiceClient.getOrderNo(vo); logger.debug(远程返回:JSON.toJSONString(orderNo)); } UserRes userRes new UserRes() ; userRes.setUserId(123); userRes.setUserName(张三); userRes.setReqNo(userReqVO.getReqNo()); userRes.setCode(StatusEnum.SUCCESS.getCode()); userRes.setMessage(成功); return userRes ; }关键 API 说明RateLimiter.create(double permitsPerSecond)创建一个令牌桶限流器参数表示每秒补充的令牌数即允许的 QPS。例如create(2.0)表示平均每秒放行 2 个请求。acquire()获取一个令牌。若桶中令牌充足则立即返回若不足则阻塞当前线程直到桶中令牌补齐。返回值为double类型的等待秒数0.0表示无需等待。acquire(int permits)一次性获取多个令牌对应一个请求可以获取多个甚至全部令牌的场景后续请求会因令牌被透支而等待更长时间。tryAcquire()非阻塞版本拿不到令牌立即返回false适合快速失败策略如秒杀场景避免请求排队拖垮系统。需要说明的是该示例中限流器为方法内局部创建在实际项目中应将其提升为static字段或 Spring 单例 Bean 全局共享否则每个请求都会创建新的限流器限流将完全失效。从单机限流到分布式限流原文档在文末列出了两个延伸方向单 JVM 限流与分布式限流。需要强调GuavaRateLimiter是进程内实现限流状态令牌数只存在于当前 JVM 内存中因此只能作用于单机应用在多实例部署的微服务架构下每个实例各自限流总流量会随实例数成倍放大无法达到全局限流的效果。为此JCSprout 仓库收录了分布式限流的完整方案其思路是引入第三方组件Redis统一记录请求次数实现跨实例的全局计数。核心思想Redis Lua 原子计数分布式限流的实现要点如下每次请求时将**当前时间精确到秒**作为 Key 写入 Redis超时时间设置为 2 秒并对该 Key 执行自增当计数达到阈值时返回错误快速失败写入 Redis 的操作用Lua 脚本完成利用 Redis 单线程执行模型的原子性保证并发安全。Lua 脚本如下-- lua 下标从 1 开始 -- 限流 key local key KEYS[1] -- 限流大小 local limit tonumber(ARGV[1]) -- 获取当前流量大小 local curentLimit tonumber(redis.call(get, key) or 0) if curentLimit 1 limit then -- 达到限流大小 返回 return 0; else -- 没有达到阈值 value 1 redis.call(INCRBY, key, 1) redis.call(EXPIRE, key, 2) return curentLimit 1 endJava 侧的调用逻辑public boolean limit() { String key String.valueOf(System.currentTimeMillis() / 1000); Object result null; if (jedis instanceof Jedis) { result ((Jedis) this.jedis).eval(script, Collections.singletonList(key), Collections.singletonList(String.valueOf(limit))); } else if (jedis instanceof JedisCluster) { result ((JedisCluster) this.jedis).eval(script, Collections.singletonList(key), Collections.singletonList(String.valueOf(limit))); } else { // throw new RuntimeException(instance is error) ; return false; } if (FAIL_CODE ! (Long) result) { return true; } else { return false; } }其中FAIL_CODE 0脚本返回 0 表示达到阈值返回其他值当前计数表示放行。使用时只需在需要限流的入口处调用redisLimit.limit()并对返回值做判断即可。需要客观说明的是这套 Redis 方案本质上是基于固定时间窗口的计数器限流实现简单直观但时间窗口边界处可能出现双倍流量的突刺并且它并非令牌桶或漏桶那样的平滑限流。文档作者也明确指出这只是利用 Redis 做了一个粗暴的计数器如果想实现类似于上文中的令牌桶算法可以基于 Lua 自行实现。限流组件在秒杀架构中的落地在仓库的秒杀架构实践中限流被用于解决10 个库存、百万请求的经典问题商品库存只有 10 个99% 的请求注定失败与其让海量请求打到数据库不如用分布式限流把并发控制在可控范围内、快速失败最大程度保护数据库。该组件在 v1.0.3 版本中将 Builder 改为接收JedisConnectionFactory并通过RedisToolsConstant.SINGLE/CLUSTER显式指定 Redis 部署形态文档建议使用集群因为限流本身对 Redis 有一定压力同时提供了SpringControllerLimit(errorCode 200)等注解式接入方式。这与MD/Spike.md 中在服务层针对写请求使用请求队列再通过限流算法每秒钟放一部分请求到队列的设计思路一脉相承——先限流再排队最后放行到核心业务。总结限流是保护高并发大流量系统的利器核心诉求是保证可用性优先于吞吐。本文梳理了两条技术脉络算法层漏桶算法强制匀速输出、简单粗暴但无法应对突发流量令牌桶算法以恒定速率放令牌、支持先消费后付款的突发透支能力是绝大多数接口限流场景的首选。落地层单机场景可直接使用 GuavaRateLimiterJCSprout 的 pom.xml 已内置 Guava 22.0分布式场景则需要借助 Redis Lua 实现全局计数并可通过 Builder 模式与注解如SpringControllerLimit降低接入成本。成熟的限流方案还有很多如时间窗口、滑动窗口、信号量等本文从 JCSprout 仓库的限流算法文档出发给出了从原理到单机、再到分布式限流的完整参考路径希望对刚接触限流的朋友提供一些思路。赞分享文档教程后端【免费下载链接】JCSprout‍ Java Core Sprout : basic, concurrent, algorithm项目地址https://gitcode.com/gh_mirrors/jc/JCSprout点击查看免费下载相关推荐京东购物评价如何实现智能自动化3个技术方案彻底解决评价难题京东购物评价如何实现智能自动化3个技术方案彻底解决评价难题 你是否曾经面对京东购物后堆积如山的待评价订单感到头疼每次大促结束几十个商品等待评价手动操作不文档教程后端MaterialColorsApp最佳实践设计师与开发者的高效色彩工作流指南MaterialColorsApp最佳实践设计师与开发者的高效色彩工作流指南 MaterialColorsApp是一款专为Mac用户设计的便捷工具能够快速访Sanic限流算法终极指南如何快速实现令牌桶与漏桶算法保护你的Web应用 Sanic限流算法终极指南如何快速实现令牌桶与漏桶算法保护你的Web应用 在现代Web应用开发中Sanic作为一个高性能的Python异步Web框架后端Web框架上一篇IDM激活脚本技术解析注册表锁定实现试用期无限延长下一篇AUI开发者工具链配置从基础开发环境到完整IDE安装终极指南创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表