
文档教程后端【免费下载链接】JCSprout Java Core Sprout : basic, concurrent, algorithm项目地址https://gitcode.com/gh_mirrors/jc/JCSprout点击查看免费下载导读本篇文章基于 JCSprout 仓库中的 分布式限流 文档展开围绕单机限流无法覆盖多实例部署这一痛点完整讲解如何借助 Redis 与 Lua 脚本实现一个跨 JVM 生效的分布式全局限流组件。读完本文你将掌握分布式限流的核心设计思路时间窗口计数器 Lua 原子性保证、组件 API 与构建器用法、面向 SpringMVC 接口的ControllerLimit注解与面向普通方法的CommonLimit注解原理以及如何在秒杀等高并发场景中落地使用。从单机限流到分布式限流为什么需要第三方组件在 限流算法 一文中已经讨论过漏桶算法与令牌桶算法这些方案可以保证单机应用在高并发大流量下的可用性。但它们的生效范围都局限在单个 JVM 进程内漏桶算法将流量放入桶中按固定速率流出流量过大时溢出的请求被丢弃无法应对突发流量令牌桶算法以恒定速率向桶中放入令牌请求经过时消耗令牌令牌耗尽时请求被阻塞支持先消费后付款如 Guava 的RateLimiter。问题在于当应用以多实例、多节点的方式部署时每个实例都持有一套独立的计数器或令牌桶单机限流的总和可以轻松超过预期的整体阈值。此时就需要一种跨 JVM 共享状态的限流方案——即分布式限流。实现分布式限流最自然的选择是引入一个所有节点都能访问的第三方组件来记录请求次数而Redis因其高性能、原子操作与集群部署能力非常适合承担这一职责。本文要介绍的组件正是围绕这一思路设计的。DEMO在 Spring Cloud 应用中接入分布式限流为了让读者直观感受接入方式文档使用了 Spring Cloud 微服务架构做演示原 DEMO 基于 springboot-cloud 项目Order 应用提供被限流的接口User 应用作为调用方模拟并发流量。第一步配置 RedisLimit Bean在 Order 应用中首先通过Configuration配置类声明RedisLimit组件限流阈值从配置项redis.limit读取底层连接取自 Spring 管理的JedisConnectionFactory示例代码针对 Redis 集群模式通过getClusterConnection()取得原生JedisCluster连接Configuration public class RedisLimitConfig { Value(${redis.limit}) private int limit; Autowired private JedisConnectionFactory jedisConnectionFactory; Bean public RedisLimit build() { RedisClusterConnection clusterConnection jedisConnectionFactory.getClusterConnection(); JedisCluster jedisCluster (JedisCluster) clusterConnection.getNativeConnection(); RedisLimit redisLimit new RedisLimit.Builder(jedisCluster) .limit(limit) .build(); return redisLimit; } }第二步在 Controller 中手工调用限流在业务接口中注入RedisLimit通过limit()方法的返回值判断当前请求是否被放行未放行时直接返回限流响应码从而快速失败、保护下游应用Autowired private RedisLimit redisLimit ; Override CheckReqNo public BaseResponseOrderNoResVO getOrderNo(RequestBody OrderNoReqVO orderNoReq) { BaseResponseOrderNoResVO res new BaseResponse(); //限流 boolean limit redisLimit.limit(); if (!limit){ res.setCode(StatusEnum.REQUEST_LIMIT.getCode()); res.setMessage(StatusEnum.REQUEST_LIMIT.getMessage()); return res ; } res.setReqNo(orderNoReq.getReqNo()); if (null orderNoReq.getAppId()){ throw new SBCException(StatusEnum.FAIL); } OrderNoResVO orderNoRes new OrderNoResVO() ; orderNoRes.setOrderId(DateUtil.getLongTime()); res.setCode(StatusEnum.SUCCESS.getCode()); res.setMessage(StatusEnum.SUCCESS.getMessage()); res.setDataBody(orderNoRes); return res ; }第三步注解式限流无需侵入业务代码为了减少对业务代码的侵入组件还提供了两种声明式注解ControllerLimit作用于RequestMapping修饰的 HTTP 接口请求达到阈值时直接返回拦截器拦截 HTTP 请求Override ControllerLimit public BaseResponseOrderNoResVO getOrderNoLimit(RequestBody OrderNoReqVO orderNoReq) { BaseResponseOrderNoResVO res new BaseResponse(); // 业务逻辑 return res ; }CommonLimit作用于普通方法调用达到阈值时抛出异常CommonLimit public void doSomething(){}第四步模拟并发验证分布式效果为了模拟真实并发User 应用开启 10 个线程通过 Feign 远程调用 Order 接口限流次数配置为 510 个并发请求中至多 5 个放行。10 个并发请求中只有 5 个能够通过其余请求被限流拦截。同时为了验证分布式效果演示环境启动了 2 个 Order 应用实例——如果限流只对单机生效两个实例各自放行 5 个请求总放行量就会达到 10 个而分布式限流下两个实例共享同一个 Redis 计数器总放行量严格控制在 5 个以内Override public BaseResponseUserResVO getUserByFeign(RequestBody UserReqVO userReq) { //调用远程服务 OrderNoReqVO vo new OrderNoReqVO(); vo.setAppId(1L); vo.setReqNo(userReq.getReqNo()); for (int i 0; i 10; i) { executorService.execute(new Worker(vo, orderServiceClient)); } UserRes userRes new UserRes(); userRes.setUserId(123); userRes.setUserName(张三); userRes.setReqNo(userReq.getReqNo()); userRes.setCode(StatusEnum.SUCCESS.getCode()); userRes.setMessage(成功); return userRes; } private static class Worker implements Runnable { private OrderNoReqVO vo; private OrderServiceClient orderServiceClient; public Worker(OrderNoReqVO vo, OrderServiceClient orderServiceClient) { this.vo vo; this.orderServiceClient orderServiceClient; } Override public void run() { BaseResponseOrderNoResVO orderNo orderServiceClient.getOrderNoCommonLimit(vo); logger.info(远程返回: JSON.toJSONString(orderNo)); } }除手写多线程模拟外也可以使用 JMeter 等专业并发测试工具进行压测。实现原理Redis 时间窗口计数器 Lua 原子脚本分布式限流的实现原理并不复杂既然要达成全局限流就需要一个所有实例共享的计数器Redis 恰好满足要求。整体思路分为三步每次请求时将**当前时间精确到秒**作为 Key 写入 RedisKey 的超时时间设置为 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 end脚本逻辑要点KEYS[1]传入限流 Key即当前秒时间戳ARGV[1]传入限流阈值limit先GET当前计数若计数 1 limit则直接返回0拒绝否则执行INCRBY key 1计数自增并执行EXPIRE key 2设置 2 秒过期时间保证下一秒自然过期、窗口自动滑动最后返回新的计数值所有判断与自增都在一次eval调用内完成天然具备原子性。Java 侧的调用逻辑则通过eval执行脚本并兼容Jedis单机与JedisCluster集群两种连接类型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; } }调用方只需对返回值进行判断返回非0即FAIL_CODE视为放行返回0视为限流。这样便实现了以秒为粒度的固定窗口计数器限流。需要说明的是这是一种粗暴的计数器实现固定时间窗口在窗口临界点可能出现瞬时双倍流量文档明确指出若想实现类似令牌桶的平滑限流可以基于 Lua 自行扩展实现。Builder 构建器清晰、可读、不易出错的 API 设计在设计组件 API 时如何优雅地构建一个限流对象是首要问题。文档对比了三种 Java 对象创建方式方式一重叠构造器Telescoping Constructorpublic A(){} public A(int a){} public A(int a,int b){}缺点显而易见参数过多时难以阅读若多个参数类型一致客户端可能颠倒传参顺序而不会产生编译警告从而引发难以预测的结果。方式二JavaBean setter 模式A a new A(); a.setA(a); a.setB(b);这种方式清晰易读但对象在构建过程中可能处于不一致状态例如只 set 了一半属性且 setter 破坏不可变性容易引发线程安全问题。方式三Builder 构建器——组件最终采用的方式public class RedisLimit { private JedisCommands jedis; private int limit 200; private static final int FAIL_CODE 0; /** * lua script */ private String script; private RedisLimit(Builder builder) { this.limit builder.limit ; this.jedis builder.jedis ; buildScript(); } /** * limit traffic * return if true */ 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; } } /** * read lua script */ private void buildScript() { script ScriptUtil.getScript(limit.lua); } /** * the builder * param T */ public static class BuilderT extends JedisCommands{ private T jedis null ; private int limit 200; public Builder(T jedis){ this.jedis jedis ; } public Builder limit(int limit){ this.limit limit ; return this; } public RedisLimit build(){ return new RedisLimit(this) ; } } }Builder 模式的细节值得注意limit字段提供默认值200非必填参数客户端使用时只需链式调用RedisLimit redisLimit new RedisLimit.Builder(jedisCluster) .limit(limit) .build();创建过程一气呵成避免了拆成多个子步骤导致的对象中间态Lua 脚本在构造时通过ScriptUtil.getScript(limit.lua)加载limit()方法每次执行时都以System.currentTimeMillis() / 1000生成当前秒作为 Key构造器类型参数T extends JedisCommands使Jedis与JedisCluster均可作为底层连接limit()方法内部按实例类型分发执行eval。这种模式在存在多个构造参数但又不全是必选字段时非常有用。作为设计延伸同一组件的分布式锁可参考 基于 Redis 的分布式锁也同步采用了 Builder 构建方式。API 使用详解组件对外最核心的 API 就是limit()方法调用方式非常直接//限流 boolean limit redisLimit.limit(); if (!limit){ //具体限流逻辑 }除手工调用外为减少侵入性、简化客户端使用组件提供了两种注解形式。ControllerLimitSpringMVC 拦截器实现该注解作用于RequestMapping修饰的接口限流命中后由拦截器直接写出限流响应。底层是 SpringMVC 的HandlerInterceptor在preHandle阶段判断当前HandlerMethod是否标注了ControllerLimit命中则调用redisLimit.limit()判断未通过时打印告警日志并返回错误响应Component public class WebIntercept extends WebMvcConfigurerAdapter { private static Logger logger LoggerFactory.getLogger(WebIntercept.class); Autowired private RedisLimit redisLimit; Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new CustomInterceptor()) .addPathPatterns(/**); } private class CustomInterceptor extends HandlerInterceptorAdapter { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if (redisLimit null) { throw new NullPointerException(redisLimit is null); } if (handler instanceof HandlerMethod) { HandlerMethod method (HandlerMethod) handler; ControllerLimit annotation method.getMethodAnnotation(ControllerLimit.class); if (annotation null) { //skip return true; } boolean limit redisLimit.limit(); if (!limit) { logger.warn(request has bean limit); response.sendError(500, request limit); return false; } } return true; } } }实现本质是 SpringMVC 拦截器其底层机制同样属于 AOP 范畴在拦截过程中检查注解并调用限流逻辑。使用前提是应用能够扫描到该类让 Spring 完成管理即在启动类上配置组件扫描ComponentScan(value com.crossoverjie.distributed.intercept)CommonLimitSpring AOP 切面实现普通方法非 Controller 接口可以使用CommonLimit其实现原理是 Spring AOP通过Pointcut匹配带有该注解的方法在Before前置通知中执行限流判断未通过时抛出运行时异常Aspect Component EnableAspectJAutoProxy(proxyTargetClass true) public class CommonAspect { private static Logger logger LoggerFactory.getLogger(CommonAspect.class); Autowired private RedisLimit redisLimit ; Pointcut(annotation(com.crossoverjie.distributed.annotation.CommonLimit)) private void check(){} Before(check()) public void before(JoinPoint joinPoint) throws Exception { if (redisLimit null) { throw new NullPointerException(redisLimit is null); } boolean limit redisLimit.limit(); if (!limit) { logger.warn(request has bean limit); throw new RuntimeException(request has bean limit) ; } } }使用CommonLimit同样需要让 Spring 扫描到切面所在包ComponentScan(value com.crossoverjie.distributed.intercept)两种注解对比如下注解适用场景拦截机制限流失败行为ControllerLimitSpringMVC 的 HTTP 接口SpringMVC HandlerInterceptor直接返回限流响应如 500/自定义错误CommonLimit任意普通方法Spring AOP 前置通知抛出RuntimeException进阶限流组件在秒杀架构中的升级实践本仓库的 秒杀架构实践 文档展示了该组件的升级版本v1.0.3在生产级场景中的落地在乐观锁更新库存 分布式限流的秒杀方案中所有请求都经过 Redis 限流因此对 Redis 资源的使用必须精打细算。升级后的构建器改为直接接收JedisConnectionFactory并在初始化时显式传入 Redis 部署形态RedisToolsConstant.SINGLE表示单机集群模式强烈推荐因为限流会给 Redis 带来一定压力Configuration public class RedisLimitConfig { private Logger logger LoggerFactory.getLogger(RedisLimitConfig.class); Value(${redis.limit}) private int limit; Autowired private JedisConnectionFactory jedisConnectionFactory; Bean public RedisLimit build() { RedisLimit redisLimit new RedisLimit.Builder(jedisConnectionFactory, RedisToolsConstant.SINGLE) .limit(limit) .build(); return redisLimit; } }对应的限流实现也在调用前后显式获取、关闭连接避免连接泄漏public boolean limit() { //get connection Object connection getConnection(); Object result limitRequest(connection); if (FAIL_CODE ! (Long) result) { return true; } else { return false; } } private Object limitRequest(Object connection) { Object result null; String key String.valueOf(System.currentTimeMillis() / 1000); if (connection instanceof Jedis){ result ((Jedis)connection).eval(script, Collections.singletonList(key), Collections.singletonList(String.valueOf(limit))); ((Jedis) connection).close(); }else { result ((JedisCluster) connection).eval(script, Collections.singletonList(key), Collections.singletonList(String.valueOf(limit))); try { ((JedisCluster) connection).close(); } catch (IOException e) { logger.error(IOException,e); } } return result; } private Object getConnection() { Object connection ; if (type RedisToolsConstant.SINGLE){ RedisConnection redisConnection jedisConnectionFactory.getConnection(); connection redisConnection.getNativeConnection(); }else { RedisClusterConnection clusterConnection jedisConnectionFactory.getClusterConnection(); connection clusterConnection.getNativeConnection() ; } return connection; }升级版同时提供了面向原生态 Spring 应用的SpringControllerLimit(errorCode 200)注解可在注解中直接指定限流时的错误码/** * 乐观锁更新库存 限流 * param sid * return */ SpringControllerLimit(errorCode 200) RequestMapping(/createOptimisticLimitOrder/{sid}) ResponseBody public String createOptimisticLimitOrder(PathVariable int sid) { logger.info(sid[{}], sid); int id 0; try { id orderService.createOptimisticOrder(sid); } catch (Exception e) { logger.error(Exception,e); } return String.valueOf(id); }在秒杀压测中接入分布式限流后数据库连接数与并发请求数都出现明显下降——因为绝大多数无效流量在到达数据库之前就被fail fast拦截掉了。这正呼应了秒杀架构的优化要点尽量将请求拦截在上游、最大程度减少请求落到 DB、尽早失败以保护应用。总结限流是高并发大流量系统中保护应用可用性的利器成熟方案众多。本文介绍的方案给出了一个清晰且可复用的思路共享状态用 Redis 作为跨 JVM 的全局计数器Key 为当前秒时间戳过期时间 2 秒形成时间窗口原子保证计数判断与自增全部收敛进一段 Lua 脚本依赖 Redis 单线程执行模型消除竞态低侵入接入既提供limit()方法式调用也提供ControllerLimitSpringMVC 拦截器与CommonLimitSpring AOP两种注解优雅的 API 设计以 Builder 构建器替代重叠构造器与 JavaBean setter兼顾可读性、不可变性与线程安全生产级考量显式区分单机/集群连接、及时关闭连接、支持自定义错误码并在秒杀等高并发场景中通过尽早失败保护数据库。需要注意的是本文方案本质是固定时间窗口计数器窗口边界处存在瞬时突刺的固有缺陷若要实现令牌桶等更平滑的算法可在本组件思路基础上基于 Lua 自行扩展。更多细节与完整源码可继续阅读仓库中的 分布式限流、限流算法、基于 Redis 的分布式锁 以及 秒杀架构实践 等文档。赞分享文档教程后端【免费下载链接】JCSprout Java Core Sprout : basic, concurrent, algorithm项目地址https://gitcode.com/gh_mirrors/jc/JCSprout点击查看免费下载相关推荐spring-boot-demo 实战基于 AOP Redis Lua 的分布式 API 限流spring boot demo 实战基于 AOP Redis Lua 的分布式 API 限流 导读 本篇文章基于 spring boot demo示例工程后端Seafile API限流实现基于Redis的分布式计数器设计Seafile API限流实现基于Redis的分布式计数器设计 在高并发场景下API限流是保护服务稳定性的关键机制。Seafile作为高性能文件同步与共享系存储数据同步人体朝向判断实战YOLOv11 关键点检测的 3 个判据人体朝向判断实战YOLOv11 关键点检测的 3 个判据 用 YOLOv11 关键点检测做人体朝向判断——分清画面里的人朝正面还是侧面——是队列计数、健身动作人工智能计算机视觉深度学习机器学习预训练上一篇OpenMAIC 交互式 Diagram 内容生成基于 user.md 模板的完整实战指南下一篇ExplorerPatcher 恢复经典任务栏三项设置让 Windows 11 回到 Win10 体验创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考