1.固定时间窗口限流算法
在固定时间内进行限流,比如下午1点到下午2点只允许用户进行操作十次,那如果超过这十次就会拒绝用户进行访问。
问题:可能会出现流量突刺,如果用户正好卡在1点59分59秒进行十次访问,在2点00分00秒又进行了十次访问,那用户就在极短的时间内进行了多次访问,产生了流量突刺。
简单进行实现固定窗口限流。
统计限流次数使用常量即可。
import org.junit.jupiter.api.Test;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
public class FixedTimeWindows {
public static long recordTime = LocalDateTime.now().toEpochSecond(ZoneOffset.UTC) * 1000;
/**
* 获取限流的时间戳 毫秒 十秒内只能访问五次
*/
public static final long LIMIT_TIME = LocalDateTime.now().plusSeconds(10).toEpochSecond(ZoneOffset.UTC) * 1000 - recordTime;
/**
* 定义一小时能可以进行调用的次数
*/
public static final int LIMIT_TIMES = 5;
/**
* 存储用户调用的次数
*/
public static int userTimes = 0;
/**
* 实现固定窗口限流算法
*/
public static boolean fixedTimeWindowLimit() {
// 获取当前时间
long now = LocalDateTime.now().toEpochSecond(ZoneOffset.UTC) * 1000;
if (now - recordTime > LIMIT_TIME) {
// 更新时间限制
System.out.println("更新时间限制");
userTimes = 0;
recordTime = now;
} else {
if (userTimes >= LIMIT_TIMES) {
return false;
}
}
userTimes++;
return true;
}
public static void getNiuma() {
System.out.println("牛马");
}
@Test
void testGetNiuma() throws InterruptedException {
for (int i = 0; i <= 20; i++) {
Thread.sleep(1 * 1000);
boolean result = fixedTimeWindowLimit();
if (!result) {
System.out.println("超出调用限制");
} else {
getNiuma();
}
}
}
}
2.滑动时间窗口限流算法
设定一个单位时间:一小时内只能有十次访问,在黄色期间的一小时只能进行访问十次,当时间发生变化向后推迟了十分中,在紫色范围能只能访问十次。
这种算法巧妙的避免了流量突刺的情况,即使你分两段对我进行多次访问,只要这两段访问时间在我的单位时间单元内,那我就可以使用流量阻塞,不让你进行访问。
优点:可以解决上述流量突刺的问题。
缺点:实现相对复杂,限流效果和你的滑动单位有关,滑动单位越小,限流效果越好,但是往往很难进行选择一个特别合适的滑动单位。而且会直接暴力拒绝请求,比较影响用户的体验性。
简单实现滑动窗口限流。
统计限流次数使用数组进行滑动时间处理。
import org.junit.jupiter.api.Test;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.util.Arrays;
public class SlideTimeWindow {
/**
* 定义初始化记录的事件
*/
public static long recordTime = LocalDateTime.now().toEpochSecond(ZoneOffset.UTC) * 1000;
/**
* 获取限流的时间戳 毫秒 十秒内内只能访问五次
*/
public static final long LIMIT_TIME = LocalDateTime.now().plusSeconds(10).toEpochSecond(ZoneOffset.UTC) * 1000 - recordTime;
/**
* 分割为十个时间单位
*/
public static final int UNITS_OF_TIME = 10;
/**
* 定义一小时能可以进行调用的次数
*/
public static final int LIMIT_TIMES = 5;
/**
* 存储用户调用的次数
*/
public static int[] userTimeArr = new int[UNITS_OF_TIME];
/**
* 数组求和
*/
public static int arrSum() {
return Arrays.stream(userTimeArr).sum();
}
/**
* 根据时间进行数组清零
* @param number 时间
*/
public static void arrCleanZero(int number) {
for (int i = 0; i <= number; i++) {
userTimeArr[i] = 0;
}
}
/**
* 定义滑动窗框限流
*/
public static boolean slideTimeWindowLimit() {
// 获取当前时间
long now = LocalDateTime.now().toEpochSecond(ZoneOffset.UTC) * 1000;
// 1. 当前时间 - 上次调用记录时间 > 10秒 清零 -> 允许调用
if (now - recordTime >= LIMIT_TIME) {
userTimeArr = new int[UNITS_OF_TIME];
}
// 2. 当前时间 - 上次调用记录时间 < 10秒
if (now - recordTime < LIMIT_TIME) {
// 2.1 数组求和 > 限制次数 滚蛋
if (arrSum() >= LIMIT_TIMES) {
// 一定要更新记录时间
recordTime = now;
return false;
}
// 2.2 数组求和 < 限制次数 允许调用
}
// 3. 调用规整
// 3.0 给哪个时间加这个次数
int timeNumber = (int) Math.floor((double) (now - recordTime) / 1000);
// 处理过界情况
if (timeNumber > 10) {
timeNumber -= 10;
}
// 3.1 统一进行加1
userTimeArr[timeNumber] += 1;
// 3.2 更新记录时间
recordTime = now;
return true;
}
public static void getNiuma() {
System.out.println("牛马");
}
@Test
void testGetNiuma() throws InterruptedException {
boolean result;
for (int i = 0; i <= 10; i++) {
Thread.sleep(500);
result = slideTimeWindowLimit();
if (!result) {
System.out.println("超出调用限制");
} else {
getNiuma();
}
}
Thread.sleep(5000);
for (int i = 0; i <= 10; i++) {
Thread.sleep(500);
result = slideTimeWindowLimit();
if (!result) {
System.out.println("超出调用限制");
} else {
getNiuma();
}
}
Thread.sleep(10000);
for (int i = 0; i <= 10; i++) {
Thread.sleep(500);
result = slideTimeWindowLimit();
if (!result) {
System.out.println("超出调用限制");
} else {
getNiuma();
}
}
}
}
3.漏桶限流算法
漏桶算法就是定义一个桶,进行存储请求流量,但是出桶进行处理请求的时候,是根据固定速率出来请求进行处理。
桶有一定的容量,如果请求存储到漏桶中,超出了容量,就会进行拒绝请求。
优点:在一定程度上可以应对流量突刺,能够固定速率处理请求,保证服务器的安全。
缺点:无法迅速处理一批请求,只能一个一个按顺序来处理(固定速率的缺点),当系统出现流量高峰的时候,还这样处理速度不变,对用户的体验会非常不好。
简单实现漏桶限流算法。
import java.util.ArrayList;
import java.util.List;
public class LeakyBuckets {
/**
* 定义固定速率
*/
public static final int SPEED = 1;
/**
* 定义桶容量
*/
public static final int CAPACITY = 10;
/**
* 定义桶
*/
public static List<Integer> bucketArr = new ArrayList<>();
/**
* 定义漏桶限流
* false 执行限流 true 不执行限流
* @return
*/
public static boolean leakyBucketsLimit() {
// 1. 将请求加入到漏桶中
// 判断桶是否满了
if (bucketArr.size() >= CAPACITY) {
// 执行限流
System.out.println("============== 桶满啦 ==============");
return false;
}
// 实际过程中可以进行加入请求标识等
bucketArr.add(1);
// 2. 以固定流速进行处理请求
// 这里只进行实现单线程的 实际请求中固定速率高的时候应该开多线程
return true;
}
public static void getNiuma() {
System.out.println("牛马");
}
}
4.令牌桶限流算法
就是管理员先进行生成一批令牌,每秒生成10个令牌,当用户要进行操作前,先去拿到一个令牌,有令牌的人才有资格进行操作,能同时进行操作,拿不到令牌的就需要进行等着。
实现令牌桶算法。
import java.util.concurrent.Callable;
public class MyCallable implements Callable<String> {
private String name;
public MyCallable(String name) {
this.name = name;
}
@Override
public String call() throws Exception {
return TokenBucket.getNiuma(name);
}
}
import org.junit.jupiter.api.Test;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class TokenBucket {
/**
* 定义固定速率
*/
public static final int SPEED = 1;
/**
* 定义令牌前缀
*/
public static final String TOKEN_FRONT = "token";
/**
* 定义令牌桶
*/
public static List<String> tokenBucket = new ArrayList<>();
/**
* 生成令牌
* @param key 关键key
*/
public static String productToken(String key) {
String token = TOKEN_FRONT + key;
tokenBucket.add(token);
return token;
}
/**
* 初始化令牌
* @param number 初始化令牌数量
*/
public static void initToken(int number) {
for (int i = 0; i < number; i++) {
productToken("niuma" + i);
}
}
/**
* 校验令牌
* @param token 令牌
* @return false 限流 true 成功
*/
public static boolean tokenBucketLimit(String token) {
// 校验无令牌
if (token == null || token.isEmpty() || token.equals(" ") || !tokenBucket.contains(token)) {
return false;
}
return true;
}
public static String getNiuma(String name) {
return "线程名: " + name + ", 牛马。";
}
@Test
void testGetNiuma() throws ExecutionException, InterruptedException {
// 初始化令牌
initToken(3);
// 初始化多线程
MyCallable callable1 = new MyCallable("线程1");
MyCallable callable2 = new MyCallable("线程2");
FutureTask<String> futureTask1 = new FutureTask<>(callable1);
FutureTask<String> futureTask2 = new FutureTask<>(callable2);
// 校验令牌
if (tokenBucketLimit(TOKEN_FRONT + "niuma1")) {
new Thread(futureTask1).start();
new Thread(futureTask2).start();
System.out.println(futureTask1.get());
System.out.println(futureTask2.get());
}
if (tokenBucketLimit("duaisbdiasu")) {
new Thread(futureTask2).start();
System.out.println(futureTask2.get());
}
}
}
优点:能够并发处理同时的请求,并发性能会更高,因为在低流量的时候可以进行积攒令牌,高流量的时候,积攒了足够的令牌可以应对高并发的请求,解决了漏桶算法的问题。
需要考虑的问题:还是存在时间单位选取的问题。如果流量突刺时令牌数量超过了系统可以进行接收的流量最大范围,那服务器就挂了,如果太少就会拒绝请求,导致用户体验过差。
5.限流粒度
1.针对某个方法进行限流,即单位时间内最多允许同时XX个操作使用这个办法。
2.针对某个用户进行限流,比如单个用户单位时间内最多执行XX次操作。
3.针对某个用户X方法进行限流,比如单个用户时间内最多执行XX次这个方法。
6.四种限流算法的思考
对于针对用户限流最大的问题就是,如果此时DDOS攻击者调动大量肉鸡,对我们的系统进行进攻,我们的限流如果仅仅针对用户的调用次数进行限流,并采用对用户进行限流的算法,那么我们的系统会被大量肉鸡产生的流量突刺,会占用大量服务器资源,直接突破我们的系统。
所以我们就是不仅要对用户在操作接口时进行限流操作,更要在对每个接口进行限流。
对于每个用户的限流就是为了防止DDOS攻击者操控一个用户大量进行请求,占用我们的资源,使得其他用户无法进行张昌访问。
对于接口(全部用户)进行限流是为了防止接口遭遇流量突刺,因为如果我们仅仅对一个用户进行限流,DDOS攻击者,操控多个用户对我们发起突刺,也会沾满我们的系统,所以我们要对整个系统的接口进行限流。
而且我们可以进行监控用户的操作,如果用户的操作,并非正常行为(比如持续高输出)那我们可以立刻对其进行封锁,限制用户行为,监控的话,我们可以使用AOP(单机)或者网关(分布式)。
7.本地限流实现(单机限流)
每个服务器进行单独限流,一般适用于单体项目,就是你得项目只有一个服务器。
使用Guava RateLimiter实现单机限流。
底层使用的是令牌桶算法。
8.分布式限流实现(多机限流)
如果你的项目有多个服务器,比如微服务,那么建议使用分布式限流。
1.把用户的使用频率等数据放到一个集中的存储中进行统计,比如Redis,这样无论用户的请求落到了哪台服务器,都可以以集中的数据存储内的数据为准(Redisson)。
2.在网关中集中进行限流和统计(比如Sentinel,Spring Cloud Gateway)。
redisson底层进行限流的时候使用的也是令牌桶算法进行限流。
9.使用redisson实现限流
9.1Redisson限流实现
Redission内置了一个限流工具类,可以帮助你利用Redis来存储,来统计。
9.2Redisson的实现
9.2.1本地/远程安装Redis
Redission依赖于Redis所以需要进行安装Redis进行使用。
9.2.2引入Redisson的依赖
<!-- https://github.com/redisson/redisson#quick-start -->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.21.3</version>
</dependency>
9.2.3实现Redisson的配置
import lombok.Data;
import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
@ConfigurationProperties(prefix = "spring.redis")
@Data
public class RedissonConfig {
private Integer database;
private String host;
private Integer port;
private String username;
private String password;
@Bean
public RedissonClient getRedissonClient() {
Config config = new Config();
// 设置使用redis单机 设置数据库和地址
config.useSingleServer()
.setDatabase(database)
.setAddress("redis://" + host + ":" + port);
RedissonClient redissonClient = Redisson.create(config);
return redissonClient;
}
}
9.2.4实现限流操作
import com.yang.yangbi.common.ErrorCode;
import com.yang.yangbi.exception.BusinessException;
import org.redisson.api.RRateLimiter;
import org.redisson.api.RateIntervalUnit;
import org.redisson.api.RateType;
import org.redisson.api.RedissonClient;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
@Service
public class RedisLimitManager {
@Resource
private RedissonClient redissonClient;
/**
* 限流操作
*
* @param key 区分不同的限流器
*/
public void doRateLimit(String key) {
// 创建一个名称为user_limiter的限流器, 每秒最多访问两次
RRateLimiter rateLimiter = redissonClient.getRateLimiter(key);
rateLimiter.trySetRate(RateType.OVERALL, 2, 1, RateIntervalUnit.SECONDS);
// 每当一个操作来了之后, 请求一个令牌
boolean canOp = rateLimiter.tryAcquire(1);
if (!canOp) {
throw new BusinessException(ErrorCode.TOO_MANY_REQUEST);
}
}
}
9.2.5进行单元测试
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import javax.annotation.Resource;
@SpringBootTest
class RedisLimitManagerTest {
@Resource
private RedisLimitManager redisLimitManager;
@Test
void doRateLimit() throws InterruptedException {
String userId = "999";
for (int i = 0; i < 2; i++) {
redisLimitManager.doRateLimit(userId);
System.out.println("成功");
}
Thread.sleep(1000);
for (int i = 0; i < 5; i++) {
redisLimitManager.doRateLimit(userId);
System.out.println("成功");
}
}
}
10.结语
没想到自己居然听了一天白羊,麻了....