重庆英文网站建设,网站按钮确定后图片怎么做,网站文章没有被收录,怎么创建一个网站卖东西文章目录 Pre概述简单计数器原理实现测试优缺点 滑动窗口算法原理实现测试优缺点 漏桶算法原理实现测试优缺点 令牌桶算法原理实现测试优缺点 小结 Pre
深入理解分布式技术 - 限流
并发编程-25 高并发处理手段之消息队列思路 应用拆分思路 应用限流思路
SpringBoot - 优雅… 文章目录 Pre概述简单计数器原理实现测试优缺点 滑动窗口算法原理实现测试优缺点 漏桶算法原理实现测试优缺点 令牌桶算法原理实现测试优缺点 小结 Pre
深入理解分布式技术 - 限流
并发编程-25 高并发处理手段之消息队列思路 应用拆分思路 应用限流思路
SpringBoot - 优雅的实现【流控】
Spring Boot - 利用Resilience4j-RateLimiter进行流量控制和服务降级
MQ - 19 安全_限流方案的设计
每日一博 - 闲聊“突发流量”的应对之道 概述
限流算法是一种在分布式系统中广泛使用的技术用于控制对系统资源的访问速率以保护系统免受恶意攻击或突发流量导致的过载。
在实际的业务场景中接口限流策略的应用非常广泛以下是一些典型的场景
API 网关限流在微服务架构中API 网关通常是系统对外的唯一入口需要限制单个用户或IP在一定时间内的请求次数以保护后端服务不受恶意请求或突发流量的冲击。分布式系统中的服务限流在分布式系统中各个服务之间可能会有调用关系通过限流可以控制服务间的调用频率避免服务间因为调用过于频繁而造成的服务过载。微服务间的接口限流微服务架构中服务间通过接口进行通信对接口进行限流可以保证服务之间的通信不会因为过量的请求而变得不可用。营销活动限流在开展营销活动时为了防止活动页面被恶意刷票或访问量过大而崩溃需要对接口进行限流确保活动能够在可控的范围内进行。用户高频操作限流对于用户频繁操作的接口如登录、发帖、评论等需要限制用户在短时间内完成的操作次数防止用户恶意操作或频繁操作导致系统资源耗尽。秒杀活动限流在秒杀活动中为了防止用户在短时间内提交过多的秒杀请求导致系统无法处理需要对参与秒杀的接口进行限流。后端服务保护限流对于一些敏感操作或计算密集型的后端服务通过限流可以避免因请求过多而使得服务响应变慢或崩溃。防止分布式拒绝服务攻击在遭受分布式拒绝服务(DDoS)攻击时限流策略能够有效地减轻攻击带来的压力确保关键业务的稳定性。用户体验优化对于一些需要高响应速度的服务通过限流可以避免过多的请求积压确保用户能够获得良好的服务体验。 简单计数器
原理
简单计数器是一种最基础的限流算法它的实现原理相对直观。
简单计数器的工作原理如下
时间窗口设定首先设定一个固定的时间窗口比如1分钟。计数器初始化在每个时间窗口开始时将计数器重置为0。请求到达每当一个请求到达时计数器加1。判断与拒绝如果在时间窗口内计数器的值达到了设定的阈值比如1000则后续的请求会被拒绝直到当前时间窗口结束计数器被重置。 实现
package com.artisan.counter;import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;/*** author 小工匠* version 1.0* mark: show me the code , change the world*/
Retention(RetentionPolicy.RUNTIME)
Target(ElementType.METHOD)
public interface CounterRateLimit {/*** 请求数量** return*/int maxRequest();/*** 时间窗口 单位秒** return*/int timeWindow();}
package com.artisan.counter;import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;import java.lang.reflect.Method;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;/*** author 小工匠* version 1.0* mark: show me the code , change the world*/Aspect
Component
public class CounterRateLimitAspect {// 存储每个方法对应的请求次数private MapString, AtomicInteger REQUEST_COUNT new ConcurrentHashMap();// 存储每个方法的时间戳private MapString, Long REQUEST_TIMESTAMP new ConcurrentHashMap();/**** param joinPoint* return* throws Throwable*/Around(annotation(com.artisan.counter.CounterRateLimit))public Object rateLimit(ProceedingJoinPoint joinPoint) throws Throwable {// 获取注解信息MethodSignature signature (MethodSignature) joinPoint.getSignature();Method method signature.getMethod();CounterRateLimit annotation method.getAnnotation(CounterRateLimit.class);// 获取注解的参数int maxRequest annotation.maxRequest();long timeWindowInMillis TimeUnit.SECONDS.toMillis(annotation.timeWindow());// 获取方法名String methodName method.toString();// 初始化计数器和时间戳AtomicInteger count REQUEST_COUNT.computeIfAbsent(methodName, x - new AtomicInteger(0));long startTime REQUEST_TIMESTAMP.computeIfAbsent(methodName, x - System.currentTimeMillis());// 获取当前时间long currentTimeMillis System.currentTimeMillis();// 判断 如果当前时间超出时间窗口则重置if (currentTimeMillis - startTime timeWindowInMillis) {count.set(0);REQUEST_TIMESTAMP.put(methodName, currentTimeMillis);}// 原子的增加计数器并检查其值if (count.incrementAndGet() maxRequest) {// 如果超出最大请求次数递减计数器并报错count.decrementAndGet();throw new RuntimeException(Too many requests, please try again later.);}// 方法原执行return joinPoint.proceed();}
}
package com.artisan.counter;import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;/*** author 小工匠* version 1.0* mark: show me the code , change the world*/
RestController
public class CounterRateLimitController {/*** 一秒一次** return*/GetMapping(/counter)CounterRateLimit(maxRequest 2, timeWindow 2)public ResponseEntity counter() {System.out.println(Request Coming Coming....);return ResponseEntity.ok(Artisan OK);}
} 测试
启动项目 访问接口 http://localhost:8080/counter 优缺点
简单计数器算法的优点是实现简单但缺点也很明显 边界问题由于计数器是在时间窗口结束时重置的如果系统的请求量非常大可能会出现时间窗口临界点的问题即在窗口即将结束时请求量激增而在窗口开始时请求量较少导致系统资源不能被有效利用。 突增流量处理能力不足无法有效处理突增的流量因为它的限制是固定的不能根据系统的实时负载进行调整。 为了解决简单计数器的这些问题可以采用更为复杂的限流算法如滑动窗口计数器、漏桶算法、令牌桶算法等。这些算法能够更加平滑和有效地控制请求速率提高系统的稳定性和可靠性。 在示例中有一个使用了 CounterRateLimit 注解的 counter 方法。根据注解的参数这个方法在2秒钟的时间窗口内只能被调用2次。 如果在 2 秒内有更多的调用那么这些额外的调用将被限流并返回错误信息 假设1min一个时间段每个时间段内最多100个请求。有一种极端情况当10:00:58这个时刻100个请求一起过来到达阈值当10:01:02这个时刻100个请求又一起过来到达阈值。这种情况就会导致在短短的4s内已经处理完了200个请求而其他所有的时间都在限流中。 滑动窗口算法
原理
滑动窗口算法是实现限流的一种常用方法它通过维护一个时间窗口来控制单位时间内请求的数量从而保护系统免受突发流量或恶意攻击的影响。其核心原理是统计时间窗口内的请求次数并根据预设的阈值来决定是否允许新的请求通过。 从图上可以看到时间创建是一种滑动的方式前进 滑动窗口限流策略能够显著减少临界问题的影响但并不能完全消除它。滑动窗口通过跟踪和限制在一个连续的时间窗口内的请求来工作。与简单的计数器方法不同它不是在窗口结束时突然重置计数器而是根据时间的推移逐渐地移除窗口中的旧请求添加新的请求。
举个例子假设时间窗口为10s请求限制为3第一次请求在10:00:00发起第二次在10:00:05发起第三次10:00:11发起那么计数器策略的下一个窗口开始时间是10:00:11而滑动窗口是10:00:05。所以这也是滑动窗口为什么可以减少临界问题的影响但并不能完全消除它的原因。 实现
package com.artisan.slidingwindow;import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;/*** author 小工匠* version 1.0* mark: show me the code , change the world*/
Retention(RetentionPolicy.RUNTIME)
Target(ElementType.METHOD)
public interface SlidingWindowRateLimit {/*** 请求数量** return*/int maxRequest();/*** 时间窗口 单位秒** return*/int timeWindow();}
package com.artisan.slidingwindow;import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;import java.lang.reflect.Method;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.TimeUnit;/*** author 小工匠* version 1.0* mark: show me the code , change the world*/Aspect
Component
public class SlidingWindowRateLimitAspect {/*** 使用 ConcurrentHashMap 保存每个方法的请求时间戳队列*/private final ConcurrentHashMapString, ConcurrentLinkedQueueLong REQUEST_TIMES_MAP new ConcurrentHashMap();Around(annotation(com.artisan.slidingwindow.SlidingWindowRateLimit))public Object rateLimit(ProceedingJoinPoint joinPoint) throws Throwable {MethodSignature signature (MethodSignature) joinPoint.getSignature();Method method signature.getMethod();SlidingWindowRateLimit rateLimit method.getAnnotation(SlidingWindowRateLimit.class);// 允许的最大请求数int requests rateLimit.maxRequest();// 滑动窗口的大小(秒)int timeWindow rateLimit.timeWindow();// 获取方法名称字符串String methodName method.toString();// 如果不存在当前方法的请求时间戳队列则初始化一个新的队列ConcurrentLinkedQueueLong requestTimes REQUEST_TIMES_MAP.computeIfAbsent(methodName,k - new ConcurrentLinkedQueue());// 当前时间long currentTime System.currentTimeMillis();// 计算时间窗口的开始时间戳long thresholdTime currentTime - TimeUnit.SECONDS.toMillis(timeWindow);// 这一段代码是滑动窗口限流算法中的关键部分其功能是移除当前滑动窗口之前的请求时间戳。这样做是为了确保窗口内只保留最近时间段内的请求记录。// requestTimes.isEmpty() 是检查队列是否为空的条件。如果队列为空则意味着没有任何请求记录不需要进行移除操作。// requestTimes.peek() thresholdTime 是检查队列头部的时间戳是否早于滑动窗口的开始时间。如果是说明这个时间戳已经不在当前的时间窗口内应当被移除。while (!requestTimes.isEmpty() requestTimes.peek() thresholdTime) {// 移除队列头部的过期时间戳requestTimes.poll();}// 检查当前时间窗口内的请求次数是否超过限制if (requestTimes.size() requests) {// 未超过限制记录当前请求时间requestTimes.add(currentTime);return joinPoint.proceed();} else {// 超过限制抛出限流异常throw new RuntimeException(Too many requests, please try again later.);}}}
package com.artisan.slidingwindow;import com.artisan.leakybucket.LeakyBucketRateLimit;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;/*** author 小工匠* version 1.0* mark: show me the code , change the world*/
RestController
public class SlidingWindowController {GetMapping(/slidingWindow)SlidingWindowRateLimit(maxRequest 2, timeWindow 2)public ResponseEntity slidingWindow() {return ResponseEntity.ok(artisan slidingWindow );}
} 测试 优缺点
滑动窗口算法的优点是它能够比较平滑地控制流量允许一定程度的突发流量同时又能够限制平均流量。相比于固定窗口算法滑动窗口算法能够更精确地控制单位时间内的请求量因为它考虑了时间窗口内请求的分布情况而不仅仅是在窗口的开始和结束时刻的请求量。
滑动窗口算法的变种有很多如基于令牌桶和漏桶的算法这些算法在滑动窗口的基础上增加了更为复杂的令牌生成和消耗机制以实现更精细的流量控制。 漏桶算法
原理 在Leaky Bucket算法中容器有一个固定的容量类似于漏桶的容量。数据以固定的速率进入容器如果容器满了则多余的数据会溢出。容器中的数据会以恒定的速率从底部流出类似于漏桶中的水滴。如果容器中的数据不足以满足流出速率则会等待直到有足够的数据可供流出。这样就实现了对数据流的平滑控制。 实现
package com.artisan.leakybucket;import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;/*** author 小工匠* version 1.0* mark: show me the code , change the world*/
Retention(RetentionPolicy.RUNTIME)
Target({ElementType.METHOD})
public interface LeakyBucketRateLimit {/*** 桶的容量** return*/int capacity();/*** 漏斗的速率单位通常是秒** return*/int leakRate();
}
package com.artisan.leakybucket;import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;import java.lang.reflect.Method;/*** author 小工匠* version 1.0* mark: show me the code , change the world*/
Aspect
Component
public class LeakyBucketRateLimitAspect {Around(annotation(com.artisan.leakybucket.LeakyBucketRateLimit))public Object rateLimit(ProceedingJoinPoint joinPoint) throws Throwable {MethodSignature signature (MethodSignature) joinPoint.getSignature();Method method signature.getMethod();LeakyBucketRateLimit leakyBucketRateLimit method.getAnnotation(LeakyBucketRateLimit.class);int capacity leakyBucketRateLimit.capacity();int leakRate leakyBucketRateLimit.leakRate();// 方法签名作为唯一标识String methodKey method.toString();LeakyBucketLimiter limiter LeakyBucketLimiter.createLimiter(methodKey, capacity, leakRate);if (!limiter.tryAcquire()) {// 超过限制抛出限流异常throw new RuntimeException(Too many requests, please try again later.);}return joinPoint.proceed();}
}
package com.artisan.leakybucket;import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;/*** author 小工匠* version 1.0* mark: show me the code , change the world*/
public class LeakyBucketLimiter {/*** 桶的容量*/private final int capacity;/*** 漏桶的漏出速率单位时间内漏出水的数量*/private final int leakRate;/*** 当前桶中的水量*/private volatile int water 0;/*** 上次漏水的时间*/private volatile long lastLeakTime System.currentTimeMillis();/*** 漏桶容器*/private static final ConcurrentHashMapString, LeakyBucketLimiter LIMITER_MAP new ConcurrentHashMap();/*** 静态工厂方法确保相同的方法使用相同的漏桶实例** param methodKey 方法名* param capacity* param leakRate* return*/public static LeakyBucketLimiter createLimiter(String methodKey, int capacity, int leakRate) {return LIMITER_MAP.computeIfAbsent(methodKey, k - new LeakyBucketLimiter(capacity, leakRate));}private LeakyBucketLimiter(int capacity, int leakRate) {this.capacity capacity;this.leakRate leakRate;}/*** 尝试获取许可try to acquire a permit如果获取成功返回true否则返回false** return*/public boolean tryAcquire() {long currentTime System.currentTimeMillis();synchronized (this) {// 计算上次漏水到当前时间的时间间隔long leakDuration currentTime - lastLeakTime;// 如果时间间隔大于等于1秒表示漏桶已经漏出一定数量的水if (leakDuration TimeUnit.SECONDS.toMillis(1)) {// 计算漏出的水量long leakQuantity leakDuration / TimeUnit.SECONDS.toMillis(1) * leakRate;// 漏桶漏出水后更新桶中的水量但不能低于0water (int) Math.max(0, water - leakQuantity);lastLeakTime currentTime;}// 判断桶中的水量是否小于容量如果是则可以继续添加水相当于获取到令牌if (water capacity) {water;return true;}}// 如果桶满则获取令牌失败return false;}
}
package com.artisan.leakybucket;import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;/*** author 小工匠* version 1.0* mark: show me the code , change the world*/
RestController
public class LeakBucketController {/*** ** return*/GetMapping(/leakyBucket)LeakyBucketRateLimit(capacity 10, leakRate 2)public ResponseEntity leakyBucket() {return ResponseEntity.ok(leakyBucket test ok!);}
} 测试 优缺点
漏桶算法和令牌桶算法最明显的区别是令牌桶算法允许流量一定程度的突发。因为默认的令牌桶算法取走token是不需要耗费时间的也就是说假设桶内有100个token时那么可以瞬间允许100个请求通过。
令牌桶算法由于实现简单且允许某些流量的突发对用户友好所以被业界采用地较多。当然我们需要具体情况具体分析只有最合适的算法没有最优的算法。 令牌桶算法
使用Guava自带的RateLimiter实现
!-- guava --
dependencygroupIdcom.google.guava/groupIdartifactIdguava/artifactIdversion32.1.1-jre/version
/dependency原理 令牌桶算法通过一个形象的比喻来描述想象有一个桶桶里装有一定数量的令牌。系统会以固定的速率向桶中添加令牌而每个数据包在发送前都需要从桶中获取一个令牌。如果桶中有足够的令牌数据包就可以立即发送如果桶中没有令牌那么数据包就需要等待直到桶中有足够的令牌为止。 关键参数 令牌生成速率令牌被添加到桶中的速率通常以每秒多少个令牌来表示。桶容量桶中可以存放的最大令牌数如果桶已满新添加的令牌会被丢弃或忽略。初始令牌数桶在开始时的令牌数量。 算法流程 令牌添加以固定的速率向桶中添加令牌通常这个速率对应了网络接口的带宽限制。请求处理 当一个数据包到达时系统会检查桶中是否有足够的令牌。如果有足够的令牌就从桶中移除相应数量的令牌并且允许数据包通过。如果桶中没有足够的令牌数据包将被标记为等待或者被丢弃。 桶满处理如果桶已满新添加的令牌可能会被丢弃或计数超出桶容量的令牌数量以允许临时突发流量的处理。 实现
package com.artisan.tokenbucket;import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;/*** author 小工匠* version 1.0* mark: show me the code , change the world*/Retention(RetentionPolicy.RUNTIME)
Target({ElementType.METHOD})
public interface TokenBucketRateLimit {/*** 产生令牌的速率(xx 个/秒)** return*/double permitsPerSecond();
}package com.artisan.tokenbucket;import com.google.common.util.concurrent.RateLimiter;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;import java.lang.reflect.Method;
import java.util.concurrent.ConcurrentHashMap;/*** author 小工匠* version 1.0* mark: show me the code , change the world*/Aspect
Component
public class TokenBucketRateLimitAspect {// 使用ConcurrentHashMap来存储每个方法的限流器private final ConcurrentHashMapString, RateLimiter limiters new ConcurrentHashMap();// 环绕通知用于在方法执行前后添加限流逻辑Around(annotation(com.artisan.tokenbucket.TokenBucketRateLimit))public Object rateLimit(ProceedingJoinPoint joinPoint) throws Throwable {// 获取方法签名用于获取方法信息MethodSignature signature (MethodSignature) joinPoint.getSignature();// 根据方法签名获取方法对象Method method signature.getMethod();// 从方法对象中获取限流注解TokenBucketRateLimit rateLimit method.getAnnotation(TokenBucketRateLimit.class);// 获取注解中定义的每秒令牌数double permitsPerSecond rateLimit.permitsPerSecond();// 获取方法名作为限流器的唯一标识String methodName method.toString();// 如果限流器缓存中没有该方法的限流器则创建一个新的RateLimiter rateLimiter limiters.computeIfAbsent(methodName, k - RateLimiter.create(permitsPerSecond));// 尝试获取令牌如果可以获取则继续执行方法if (rateLimiter.tryAcquire()) {return joinPoint.proceed();} else {// 如果无法获取令牌则抛出异常告知用户请求过于频繁throw new RuntimeException(Too many requests, please try again later.);}}
}
package com.artisan.tokenbucket;import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;/*** author 小工匠* version 1.0* mark: show me the code , change the world*/
RestController
public class TokenBucketController {GetMapping(/tokenBucket)TokenBucketRateLimit(permitsPerSecond 0.5)public ResponseEntity tokenBucket() {return ResponseEntity.ok(artisan token bucket);}
} 测试 优缺点
平滑流量通过控制令牌生成的速率可以平滑网络流量避免突发流量导致的网络拥塞。灵活性可以应对一定的突发流量因为桶可以暂时存储超过平均流量的令牌。易于实现算法相对简单易于在硬件或软件中实现。 小结
在实施接口限流策略时应根据具体的业务场景和系统需求选择合适的限流算法和实现方式同时注意限流策略对用户体验的影响做到既能保护系统稳定运行又不会对合法用户造成过多的困扰。