概述
限流算法是一种在分布式系统中广泛使用的技术,用于控制对系统资源的访问速率,以保护系统免受恶意攻击或突发流量导致的过载。
在实际的业务场景中,接口限流策略的应用非常广泛,以下是一些典型的场景:
(1)API 网关限流:在微服务架构中,API 网关通常是系统对外的唯一入口,需要限制单个用户或IP在一定时间内的请求次数,以保护后端服务不受恶意请求或突发流量的冲击。
(2)分布式系统中的服务限流:在分布式系统中,各个服务之间可能会有调用关系,通过限流可以控制服务间的调用频率,避免服务间因为调用过于频繁而造成的服务过载。
(3)微服务间的接口限流:微服务架构中,服务间通过接口进行通信,对接口进行限流可以保证服务之间的通信不会因为过量的请求而变得不可用。
(4)营销活动限流:在开展营销活动时,为了防止活动页面被恶意刷票或访问量过大而崩溃,需要对接口进行限流,确保活动能够在可控的范围内进行。
(5)用户高频操作限流:对于用户频繁操作的接口,如登录、发帖、评论等,需要限制用户在短时间内完成的操作次数,防止用户恶意操作或频繁操作导致系统资源耗尽。
(6)秒杀活动限流:在秒杀活动中,为了防止用户在短时间内提交过多的秒杀请求,导致系统无法处理,需要对参与秒杀的接口进行限流。
(7)后端服务保护限流:对于一些敏感操作或计算密集型的后端服务,通过限流可以避免因请求过多而使得服务响应变慢或崩溃。
(8)防止分布式拒绝服务攻击:在遭受分布式拒绝服务(DDoS)攻击时,限流策略能够有效地减轻攻击带来的压力,确保关键业务的稳定性。
(9)用户体验优化:对于一些需要高响应速度的服务,通过限流可以避免过多的请求积压,确保用户能够获得良好的服务体验。
限流算法四种:
简单计数器、滑动窗口算法、漏桶算法、令牌桶算法
简单计数器
简单计数器是一种最基础的限流算法,它的实现原理相对直观。
工作原理:
时间窗口设定:首先设定一个固定的时间窗口,比如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 Map<String, AtomicInteger> REQUEST_COUNT = new ConcurrentHashMap<>();
// 存储每个方法的时间戳
private Map<String, 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) {
// 如果超出最大请求次数,递减计数器,并报错